feat: add image upload functionality and stock update feature in sparepart components
This commit is contained in:
@@ -21,6 +21,7 @@ const TableList = memo(function TableList({
|
|||||||
firstLoad = true,
|
firstLoad = true,
|
||||||
columnDynamic = false,
|
columnDynamic = false,
|
||||||
cardComponent, // New prop for custom card component
|
cardComponent, // New prop for custom card component
|
||||||
|
onStockUpdate, // Prop to pass to card component
|
||||||
}) {
|
}) {
|
||||||
const [gridLoading, setGridLoading] = useState(false);
|
const [gridLoading, setGridLoading] = useState(false);
|
||||||
|
|
||||||
@@ -166,6 +167,7 @@ const TableList = memo(function TableList({
|
|||||||
showPreviewModal={showPreviewModal}
|
showPreviewModal={showPreviewModal}
|
||||||
showEditModal={showEditModal}
|
showEditModal={showEditModal}
|
||||||
showDeleteDialog={showDeleteDialog}
|
showDeleteDialog={showDeleteDialog}
|
||||||
|
onStockUpdate={onStockUpdate}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row gutter={24} style={{ marginTop: '16px' }}>
|
<Row gutter={24} style={{ marginTop: '16px' }}>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Input, Divider, Typography, Switch, Button, ConfigProvider, message } from 'antd';
|
import { Modal, Input, Select, Divider, Typography, Switch, Button, ConfigProvider, Upload, message, Row, Col } from 'antd';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
||||||
|
import { uploadFile } from '../../../../api/file-uploads';
|
||||||
import { validateRun } from '../../../../Utils/validate';
|
import { validateRun } from '../../../../Utils/validate';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -9,6 +11,7 @@ const { TextArea } = Input;
|
|||||||
|
|
||||||
const DetailSparepart = (props) => {
|
const DetailSparepart = (props) => {
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [imageFile, setImageFile] = useState(null);
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
sparepart_id: '',
|
sparepart_id: '',
|
||||||
@@ -18,7 +21,8 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_item_type: '',
|
sparepart_item_type: '',
|
||||||
sparepart_unit: '',
|
sparepart_unit: '',
|
||||||
sparepart_merk: '',
|
sparepart_merk: '',
|
||||||
sparepart_stok: '',
|
sparepart_stok: '0',
|
||||||
|
image_url: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState(defaultData);
|
const [formData, setFormData] = useState(defaultData);
|
||||||
@@ -26,18 +30,14 @@ const DetailSparepart = (props) => {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
|
setImageFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
// Daftar aturan validasi
|
|
||||||
const validationRules = [
|
const validationRules = [
|
||||||
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
||||||
{ field: 'sparepart_model', label: 'Sparepart Model', required: true },
|
|
||||||
{ field: 'sparepart_unit', label: 'Sparepart Unit', required: true },
|
|
||||||
{ field: 'sparepart_merk', label: 'Sparepart Merk', required: true },
|
|
||||||
{ field: 'sparepart_stok', label: 'Sparepart Stok', required: true },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -53,33 +53,42 @@ const DetailSparepart = (props) => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let imageUrl = formData.image_url; // Keep existing image url if not changed
|
||||||
|
|
||||||
|
if (imageFile) {
|
||||||
|
const uploadResponse = await uploadFile(imageFile, 'images');
|
||||||
|
if (uploadResponse && uploadResponse.file_url) {
|
||||||
|
imageUrl = uploadResponse.file_url;
|
||||||
|
} else {
|
||||||
|
throw new Error('Image upload failed or did not return a URL.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
sparepart_name: formData.sparepart_name,
|
sparepart_name: formData.sparepart_name,
|
||||||
sparepart_description: formData.sparepart_description,
|
|
||||||
sparepart_model: formData.sparepart_model,
|
|
||||||
sparepart_item_type: formData.sparepart_item_type,
|
sparepart_item_type: formData.sparepart_item_type,
|
||||||
sparepart_unit: formData.sparepart_unit,
|
sparepart_stok: formData.sparepart_stok || '0',
|
||||||
|
image_url: imageUrl,
|
||||||
sparepart_merk: formData.sparepart_merk,
|
sparepart_merk: formData.sparepart_merk,
|
||||||
sparepart_stok: formData.sparepart_stok,
|
sparepart_model: formData.sparepart_model,
|
||||||
|
sparepart_description: formData.sparepart_description,
|
||||||
|
sparepart_unit: formData.sparepart_unit, // This field was in the old form, keep it
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = formData.sparepart_id
|
const response = formData.sparepart_id
|
||||||
? await updateSparepart(formData.sparepart_id, payload)
|
? await updateSparepart(formData.sparepart_id, payload)
|
||||||
: await createSparepart(payload);
|
: await createSparepart(payload);
|
||||||
|
|
||||||
// Check if response is successful
|
|
||||||
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
if (response && (response.statusCode === 200 || response.statusCode === 201)) {
|
||||||
const sparepartName = response.data?.sparepart_name || formData.sparepart_name;
|
|
||||||
|
|
||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `Data Sparepart "${sparepartName}" berhasil ${
|
message: `Data Sparepart berhasil ${
|
||||||
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
||||||
}.`,
|
}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
|
setImageFile(null);
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -107,28 +116,44 @@ const DetailSparepart = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFieldChange = (name, value) => {
|
const handleSelectChange = (name, value) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStatusToggle = (event) => {
|
|
||||||
const isChecked = event;
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
is_active: isChecked ? true : false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
} else {
|
} else {
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
}
|
}
|
||||||
|
setImageFile(null);
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|
||||||
|
const uploadProps = {
|
||||||
|
beforeUpload: file => {
|
||||||
|
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||||
|
if (!isJpgOrPng) {
|
||||||
|
message.error('You can only upload JPG/PNG file!');
|
||||||
|
}
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
if (!isLt2M) {
|
||||||
|
message.error('Image must smaller than 2MB!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isJpgOrPng && isLt2M) {
|
||||||
|
setImageFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Prevent auto-upload
|
||||||
|
},
|
||||||
|
onRemove: () => {
|
||||||
|
setImageFile(null);
|
||||||
|
},
|
||||||
|
maxCount: 1,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -187,99 +212,111 @@ const DetailSparepart = (props) => {
|
|||||||
>
|
>
|
||||||
{formData && (
|
{formData && (
|
||||||
<div>
|
<div>
|
||||||
<div hidden>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart ID</Text>
|
<Col span={12}>
|
||||||
<Input
|
<Text strong>Sparepart Name</Text>
|
||||||
name="sparepart_id"
|
<Text style={{ color: 'red' }}> *</Text>
|
||||||
value={formData.sparepart_id}
|
<Input
|
||||||
onChange={handleInputChange}
|
name="sparepart_name"
|
||||||
disabled
|
value={formData.sparepart_name}
|
||||||
/>
|
onChange={handleInputChange}
|
||||||
</div>
|
placeholder="Enter Sparepart Name"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Item Type</Text>
|
||||||
|
<Select
|
||||||
|
name="sparepart_item_type"
|
||||||
|
value={formData.sparepart_item_type}
|
||||||
|
onChange={(value) => handleSelectChange('sparepart_item_type', value)}
|
||||||
|
placeholder="Select Item Type"
|
||||||
|
disabled={props.readOnly}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Select.Option value="air dryer">air dryer</Select.Option>
|
||||||
|
<Select.Option value="compressor">compressor</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Name</Text>
|
<Col span={12}>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text strong>Stock</Text>
|
||||||
<Input
|
<Input
|
||||||
name="sparepart_name"
|
name="sparepart_stok"
|
||||||
value={formData.sparepart_name}
|
value={formData.sparepart_stok}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder="Enter Sparepart Name"
|
placeholder="Initial stock quantity"
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
type="number"
|
||||||
</div>
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Unit</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_unit"
|
||||||
|
value={formData.sparepart_unit}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="e.g., pcs, box, roll"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Description</Text>
|
<Col span={24}>
|
||||||
<TextArea
|
<Text strong>Foto</Text>
|
||||||
name="sparepart_description"
|
<div>
|
||||||
value={formData.sparepart_description}
|
<Upload {...uploadProps}>
|
||||||
onChange={handleInputChange}
|
<Button icon={<UploadOutlined />} disabled={props.readOnly}>Select File</Button>
|
||||||
placeholder="Enter Sparepart Description (Optional)"
|
</Upload>
|
||||||
readOnly={props.readOnly}
|
{formData.image_url && !imageFile && (
|
||||||
rows={4}
|
<Text type="secondary" style={{ display: 'block', marginTop: '8px' }}>
|
||||||
/>
|
Current image: <a href={formData.image_url} target="_blank" rel="noopener noreferrer">{formData.image_url.split('/').pop()}</a>
|
||||||
</div>
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Brand</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_merk"
|
||||||
|
value={formData.sparepart_merk}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Brand (Optional)"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text strong>Model</Text>
|
||||||
|
<Input
|
||||||
|
name="sparepart_model"
|
||||||
|
value={formData.sparepart_model}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter Model (Optional)"
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Row gutter={[16, 16]}>
|
||||||
<Text strong>Sparepart Model</Text>
|
<Col span={24}>
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
<Text strong>Description</Text>
|
||||||
<Input
|
<TextArea
|
||||||
name="sparepart_model"
|
name="sparepart_description"
|
||||||
value={formData.sparepart_model}
|
value={formData.sparepart_description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder="Enter Sparepart Model"
|
placeholder="Enter Description (Optional)"
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
rows={3}
|
||||||
</div>
|
/>
|
||||||
|
</Col>
|
||||||
<div style={{ marginBottom: 12 }}>
|
</Row>
|
||||||
<Text strong>Sparepart Item Type</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_item_type"
|
|
||||||
value={formData.sparepart_item_type}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Item Type"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Sparepart Unit</Text>
|
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_unit"
|
|
||||||
value={formData.sparepart_unit}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Unit"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Sparepart Merk</Text>
|
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_merk"
|
|
||||||
value={formData.sparepart_merk}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Merk"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Text strong>Sparepart Stok</Text>
|
|
||||||
<Text style={{ color: 'red' }}> *</Text>
|
|
||||||
<Input
|
|
||||||
name="sparepart_stok"
|
|
||||||
value={formData.sparepart_stok}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter Sparepart Stok"
|
|
||||||
readOnly={props.readOnly}
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ const ListSparepart = memo(function ListSparepart(props) {
|
|||||||
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
columns={columns(showPreviewModal, showEditModal, showDeleteDialog)}
|
||||||
triger={trigerFilter}
|
triger={trigerFilter}
|
||||||
cardComponent={SparepartCardList} // Pass the custom component here
|
cardComponent={SparepartCardList} // Pass the custom component here
|
||||||
|
onStockUpdate={doFilter}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,120 +1,209 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, Button, Row, Col, Typography, Divider, Tag } from 'antd';
|
import dayjs from 'dayjs';
|
||||||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { Card, Button, Row, Col, Typography, Divider, Tag, Space, InputNumber, Input } from 'antd';
|
||||||
|
import { EditOutlined, DeleteOutlined, PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
||||||
|
import { updateSparepart } from '../../../../api/sparepart';
|
||||||
|
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
const SparepartCardList = ({
|
const SparepartCardList = ({
|
||||||
data,
|
data,
|
||||||
header,
|
header,
|
||||||
showPreviewModal, // Keep prop in case it's needed elsewhere, but icon is removed
|
showPreviewModal,
|
||||||
showEditModal,
|
showEditModal,
|
||||||
showDeleteDialog,
|
showDeleteDialog,
|
||||||
fieldColor,
|
fieldColor,
|
||||||
cardColor
|
cardColor,
|
||||||
|
onStockUpdate, // Prop to refresh the list
|
||||||
}) => {
|
}) => {
|
||||||
|
const [updateQuantities, setUpdateQuantities] = useState({});
|
||||||
|
const [loadingQuantities, setLoadingQuantities] = useState({});
|
||||||
|
|
||||||
|
const handleQuantityChange = (id, value) => {
|
||||||
|
const newQuantities = { ...updateQuantities };
|
||||||
|
newQuantities[id] = value;
|
||||||
|
setUpdateQuantities(newQuantities);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateStock = async (item) => {
|
||||||
|
const quantityToAdd = updateQuantities[item.sparepart_id] || 0;
|
||||||
|
if (quantityToAdd === 0) {
|
||||||
|
NotifAlert({ icon: 'info', title: 'Info', message: 'Please change the quantity first.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStock = Number(item.sparepart_stok) + quantityToAdd;
|
||||||
|
if (newStock < 0) {
|
||||||
|
NotifAlert({ icon: 'error', title: 'Error', message: 'Stock cannot be negative.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingQuantities(prev => ({ ...prev, [item.sparepart_id]: true }));
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...item,
|
||||||
|
sparepart_stok: newStock,
|
||||||
|
};
|
||||||
|
// Remove unnecessary keys if the API doesn't like them
|
||||||
|
delete payload.key;
|
||||||
|
delete payload.id;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateSparepart(item.sparepart_id, payload);
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
NotifOk({ icon: 'success', title: 'Success', message: 'Stock updated successfully.' });
|
||||||
|
if (onStockUpdate) {
|
||||||
|
onStockUpdate();
|
||||||
|
}
|
||||||
|
handleQuantityChange(item.sparepart_id, 0); // Reset quantity
|
||||||
|
} else {
|
||||||
|
NotifAlert({ icon: 'error', title: 'Failed', message: response.message || 'Failed to update stock.' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
NotifAlert({ icon: 'error', title: 'Error', message: error.message || 'An error occurred.' });
|
||||||
|
} finally {
|
||||||
|
setLoadingQuantities(prev => ({ ...prev, [item.sparepart_id]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
<Row gutter={[16, 16]} style={{ marginTop: '16px' }}>
|
||||||
{data.map((item) => (
|
{data.map((item) => {
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={item.sparepart_id || item.key}>
|
const quantity = updateQuantities[item.sparepart_id] || 0;
|
||||||
<Card
|
const isLoading = loadingQuantities[item.sparepart_id] || false;
|
||||||
style={{
|
|
||||||
borderRadius: '8px',
|
return (
|
||||||
overflow: 'hidden',
|
<Col xs={24} sm={12} md={8} lg={6} key={item.sparepart_id || item.key}>
|
||||||
border: `1px solid ${fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'}`,
|
<Card
|
||||||
}}
|
style={{
|
||||||
bodyStyle={{ padding: 0 }}
|
borderRadius: '8px',
|
||||||
>
|
overflow: 'hidden',
|
||||||
<Row>
|
border: `1px solid ${fieldColor ? item[fieldColor] : cardColor || '#E0E0E0'}`,
|
||||||
<Col span={8}>
|
}}
|
||||||
<div
|
bodyStyle={{ padding: 0 }}
|
||||||
style={{
|
>
|
||||||
position: 'relative',
|
<Row>
|
||||||
backgroundColor: '#f0f0f0',
|
<Col span={8}>
|
||||||
width: '100%',
|
<div
|
||||||
height: '100%',
|
style={{
|
||||||
}}
|
display: 'flex',
|
||||||
>
|
flexDirection: 'column',
|
||||||
{item.sparepart_item_type && (
|
alignItems: 'center',
|
||||||
<Tag
|
justifyContent: 'center',
|
||||||
color="blue"
|
padding: '16px 8px',
|
||||||
style={{
|
height: '100%',
|
||||||
position: 'absolute',
|
|
||||||
top: '8px',
|
|
||||||
left: '8px',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.sparepart_item_type}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
<div style={{ paddingTop: '30px', height: '100%', boxSizing: 'border-box' }}>
|
|
||||||
<img
|
|
||||||
src={item.image_url || "https://via.placeholder.com/150"}
|
|
||||||
alt={item[header]}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'cover'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col span={16}>
|
|
||||||
<div style={{ padding: '16px', position: 'relative', height: '100%' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
display: 'flex',
|
|
||||||
gap: '8px'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showEditModal && (
|
{item.sparepart_item_type && (
|
||||||
<Button
|
<Tag
|
||||||
icon={<EditOutlined />}
|
color="blue"
|
||||||
key="edit"
|
style={{
|
||||||
onClick={() => showEditModal(item)}
|
marginBottom: '12px',
|
||||||
size="small"
|
}}
|
||||||
/>
|
>
|
||||||
|
{item.sparepart_item_type}
|
||||||
|
</Tag>
|
||||||
)}
|
)}
|
||||||
{showDeleteDialog && (
|
<div style={{
|
||||||
<Button
|
backgroundColor: '#f0f0f0',
|
||||||
icon={<DeleteOutlined />}
|
width: '100%',
|
||||||
key="delete"
|
padding: '8px',
|
||||||
onClick={() => showDeleteDialog(item)}
|
borderRadius: '4px',
|
||||||
size="small"
|
display: 'flex',
|
||||||
danger
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
aspectRatio: '1 / 1',
|
||||||
|
}}>
|
||||||
|
<img
|
||||||
|
src={item.image_url || "https://via.placeholder.com/150"}
|
||||||
|
alt={item[header]}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Title level={5} style={{ margin: 0, marginBottom: '8px', paddingRight: '60px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
</Col>
|
||||||
{item[header]}
|
<Col span={16}>
|
||||||
</Title>
|
<div style={{ padding: '16px', position: 'relative', height: '100%' }}>
|
||||||
<Text type="secondary">
|
<div
|
||||||
Available: {item.sparepart_stok || '0'}
|
style={{
|
||||||
</Text>
|
position: 'absolute',
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
top: 8,
|
||||||
<Button
|
right: 8,
|
||||||
type="primary"
|
display: 'flex',
|
||||||
size="small"
|
gap: '8px'
|
||||||
style={{ marginBottom: '8px' }}
|
}}
|
||||||
onClick={() => showEditModal(item)}
|
>
|
||||||
>
|
{showEditModal && (
|
||||||
Update Stock
|
<Button
|
||||||
</Button>
|
style={{ color: '#faad14', borderColor: '#faad14' }}
|
||||||
<br />
|
icon={<EditOutlined />}
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
key="edit"
|
||||||
Last updated: {item.updated_at || 'N/A'}
|
onClick={() => showEditModal(item)}
|
||||||
</Text>
|
size="small"
|
||||||
</div>
|
/>
|
||||||
</Col>
|
)}
|
||||||
</Row>
|
{showDeleteDialog && (
|
||||||
</Card>
|
<Button
|
||||||
</Col>
|
icon={<DeleteOutlined />}
|
||||||
))}
|
key="delete"
|
||||||
|
onClick={() => showDeleteDialog(item)}
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Title level={5} style={{ margin: 0, marginBottom: '8px', paddingRight: '60px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
|
{item[header]}
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary">
|
||||||
|
Available: {item.sparepart_stok || '0'}
|
||||||
|
</Text>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
|
<Space align="center" style={{ marginBottom: '8px', display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
icon={<MinusOutlined />}
|
||||||
|
onClick={() => handleQuantityChange(item.sparepart_id, quantity - 1)}
|
||||||
|
disabled={isLoading}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Text strong style={{ padding: '0 16px', fontSize: '16px' }}>{quantity}</Text>
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => handleQuantityChange(item.sparepart_id, quantity + 1)}
|
||||||
|
disabled={isLoading}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<Text type="secondary" style={{ marginLeft: '8px' }}>{item.sparepart_unit || 'pcs'}</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type={quantity === 0 ? 'default' : 'primary'}
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={() => handleUpdateStock(item)}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Update Stock
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px', marginTop: '8px', display: 'inline-block' }}>
|
||||||
|
Last updated: {item.updated_at ? dayjs(item.updated_at).format('DD MMM YYYY') : 'N/A'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user