feat: enhance DetailSparepart component with image upload and preview functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Input, Select, Divider, Typography, Switch, Button, ConfigProvider, Upload, message, Row, Col } from 'antd';
|
import { Modal, Input, Select, Divider, Typography, Button, ConfigProvider, Upload, Row, Col, Image } from 'antd';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { PlusOutlined } 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 { uploadFile } from '../../../../api/file-uploads';
|
||||||
@@ -9,9 +9,20 @@ import { validateRun } from '../../../../Utils/validate';
|
|||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const getBase64 = (file) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
const DetailSparepart = (props) => {
|
const DetailSparepart = (props) => {
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
const [imageFile, setImageFile] = useState(null);
|
const [fileList, setFileList] = useState([]);
|
||||||
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState('');
|
||||||
|
const [previewTitle, setPreviewTitle] = useState('');
|
||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
sparepart_id: '',
|
sparepart_id: '',
|
||||||
@@ -30,38 +41,48 @@ const DetailSparepart = (props) => {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
props.setSelectedData(null);
|
props.setSelectedData(null);
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
setImageFile(null);
|
setFileList([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePreviewCancel = () => setPreviewOpen(false);
|
||||||
|
|
||||||
|
const handlePreview = async (file) => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = await getBase64(file.originFileObj);
|
||||||
|
}
|
||||||
|
setPreviewImage(file.url || file.preview);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = ({ fileList: newFileList }) => setFileList(newFileList);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setConfirmLoading(true);
|
setConfirmLoading(true);
|
||||||
|
|
||||||
const validationRules = [
|
const validationRules = [{ field: 'sparepart_name', label: 'Sparepart Name', required: true }];
|
||||||
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
validateRun(formData, validationRules, (errorMessages) => {
|
validateRun(formData, validationRules, (errorMessages) => {
|
||||||
NotifOk({
|
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||||
icon: 'warning',
|
|
||||||
title: 'Peringatan',
|
|
||||||
message: errorMessages,
|
|
||||||
});
|
|
||||||
setConfirmLoading(false);
|
setConfirmLoading(false);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let imageUrl = formData.image_url; // Keep existing image url if not changed
|
let imageUrl = formData.image_url;
|
||||||
|
const newFile = fileList.length > 0 ? fileList[0] : null;
|
||||||
|
|
||||||
if (imageFile) {
|
if (newFile && newFile.originFileObj) {
|
||||||
const uploadResponse = await uploadFile(imageFile, 'images');
|
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||||
if (uploadResponse && uploadResponse.file_url) {
|
if (uploadResponse && uploadResponse.file_url) {
|
||||||
imageUrl = uploadResponse.file_url;
|
imageUrl = uploadResponse.file_url;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Image upload failed or did not return a URL.');
|
throw new Error('Image upload failed or did not return a URL.');
|
||||||
}
|
}
|
||||||
|
} else if (fileList.length === 0 && formData.sparepart_id) {
|
||||||
|
imageUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -72,7 +93,7 @@ const DetailSparepart = (props) => {
|
|||||||
sparepart_merk: formData.sparepart_merk,
|
sparepart_merk: formData.sparepart_merk,
|
||||||
sparepart_model: formData.sparepart_model,
|
sparepart_model: formData.sparepart_model,
|
||||||
sparepart_description: formData.sparepart_description,
|
sparepart_description: formData.sparepart_description,
|
||||||
sparepart_unit: formData.sparepart_unit, // This field was in the old form, keep it
|
sparepart_unit: formData.sparepart_unit,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = formData.sparepart_id
|
const response = formData.sparepart_id
|
||||||
@@ -83,12 +104,10 @@ const DetailSparepart = (props) => {
|
|||||||
NotifOk({
|
NotifOk({
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Berhasil',
|
title: 'Berhasil',
|
||||||
message: `Data Sparepart berhasil ${
|
message: `Data Sparepart berhasil ${formData.sparepart_id ? 'diubah' : 'ditambahkan'}.`,
|
||||||
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
|
||||||
}.`,
|
|
||||||
});
|
});
|
||||||
props.setActionMode('list');
|
props.setActionMode('list');
|
||||||
setImageFile(null);
|
setFileList([]);
|
||||||
} else {
|
} else {
|
||||||
NotifAlert({
|
NotifAlert({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@@ -110,59 +129,45 @@ const DetailSparepart = (props) => {
|
|||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData({
|
setFormData({ ...formData, [name]: value });
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange = (name, value) => {
|
const handleSelectChange = (name, value) => {
|
||||||
setFormData({
|
setFormData({ ...formData, [name]: value });
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.selectedData) {
|
if (props.selectedData) {
|
||||||
setFormData(props.selectedData);
|
setFormData(props.selectedData);
|
||||||
|
if (props.selectedData.image_url) {
|
||||||
|
setFileList([
|
||||||
|
{
|
||||||
|
uid: '-1',
|
||||||
|
name: props.selectedData.image_url.split('/').pop(),
|
||||||
|
status: 'done',
|
||||||
|
url: props.selectedData.image_url,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setFileList([]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setFormData(defaultData);
|
setFormData(defaultData);
|
||||||
|
setFileList([]);
|
||||||
}
|
}
|
||||||
setImageFile(null);
|
|
||||||
}, [props.showModal, props.selectedData, props.actionMode]);
|
}, [props.showModal, props.selectedData, props.actionMode]);
|
||||||
|
|
||||||
const uploadProps = {
|
const uploadButton = (
|
||||||
beforeUpload: file => {
|
<div>
|
||||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
<PlusOutlined />
|
||||||
if (!isJpgOrPng) {
|
<div style={{ marginTop: 8 }}>Upload</div>
|
||||||
message.error('You can only upload JPG/PNG file!');
|
</div>
|
||||||
}
|
);
|
||||||
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
|
||||||
title={`${
|
title={`${
|
||||||
props.actionMode === 'add'
|
props.actionMode === 'add' ? 'Tambah' : props.actionMode === 'preview' ? 'Preview' : 'Edit'
|
||||||
? 'Tambah'
|
|
||||||
: props.actionMode === 'preview'
|
|
||||||
? 'Preview'
|
|
||||||
: 'Edit'
|
|
||||||
} Sparepart`}
|
} Sparepart`}
|
||||||
open={props.showModal}
|
open={props.showModal}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
@@ -178,7 +183,6 @@ const DetailSparepart = (props) => {
|
|||||||
defaultBorderColor: '#23A55A',
|
defaultBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
defaultHoverColor: '#23A55A',
|
||||||
defaultHoverBorderColor: '#23A55A',
|
defaultHoverBorderColor: '#23A55A',
|
||||||
defaultHoverColor: '#23A55A',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -187,9 +191,7 @@ const DetailSparepart = (props) => {
|
|||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
token: {
|
token: { colorBgContainer: '#209652' },
|
||||||
colorBgContainer: '#209652',
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Button: {
|
Button: {
|
||||||
defaultBg: '#23a55a',
|
defaultBg: '#23a55a',
|
||||||
@@ -234,8 +236,8 @@ const DetailSparepart = (props) => {
|
|||||||
disabled={props.readOnly}
|
disabled={props.readOnly}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<Select.Option value="air dryer">air dryer</Select.Option>
|
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
|
||||||
<Select.Option value="compressor">compressor</Select.Option>
|
<Select.Option value="Compressor">Compressor</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -267,16 +269,20 @@ const DetailSparepart = (props) => {
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Text strong>Foto</Text>
|
<Text strong>Foto</Text>
|
||||||
<div>
|
<Upload
|
||||||
<Upload {...uploadProps}>
|
listType="picture-card"
|
||||||
<Button icon={<UploadOutlined />} disabled={props.readOnly}>Select File</Button>
|
fileList={fileList}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
onChange={handleChange}
|
||||||
|
beforeUpload={() => false}
|
||||||
|
maxCount={1}
|
||||||
|
disabled={props.readOnly}
|
||||||
|
>
|
||||||
|
{fileList.length >= 1 ? null : uploadButton}
|
||||||
</Upload>
|
</Upload>
|
||||||
{formData.image_url && !imageFile && (
|
<Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handlePreviewCancel}>
|
||||||
<Text type="secondary" style={{ display: 'block', marginTop: '8px' }}>
|
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
||||||
Current image: <a href={formData.image_url} target="_blank" rel="noopener noreferrer">{formData.image_url.split('/').pop()}</a>
|
</Modal>
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -316,7 +322,6 @@ const DetailSparepart = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const SparepartCardList = ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'flex-start',
|
||||||
padding: '16px 8px',
|
padding: '16px 8px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
@@ -100,7 +100,7 @@ const SparepartCardList = ({
|
|||||||
<Tag
|
<Tag
|
||||||
color="blue"
|
color="blue"
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '12px',
|
marginBottom: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.sparepart_item_type}
|
{item.sparepart_item_type}
|
||||||
@@ -162,7 +162,7 @@ const SparepartCardList = ({
|
|||||||
{item[header]}
|
{item[header]}
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Available: {item.sparepart_stok || '0'}
|
Available Stock: {item.sparepart_stok || '0'}
|
||||||
</Text>
|
</Text>
|
||||||
<Divider style={{ margin: '8px 0' }} />
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
|
||||||
@@ -171,16 +171,16 @@ const SparepartCardList = ({
|
|||||||
icon={<MinusOutlined />}
|
icon={<MinusOutlined />}
|
||||||
onClick={() => handleQuantityChange(item.sparepart_id, quantity - 1)}
|
onClick={() => handleQuantityChange(item.sparepart_id, quantity - 1)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
type="text"
|
style={{ width: 28, height: 28 }}
|
||||||
/>
|
/>
|
||||||
<Text strong style={{ padding: '0 16px', fontSize: '16px' }}>{quantity}</Text>
|
<Text strong style={{ padding: '0 8px', fontSize: '16px' }}>{quantity}</Text>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => handleQuantityChange(item.sparepart_id, quantity + 1)}
|
onClick={() => handleQuantityChange(item.sparepart_id, quantity + 1)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
type="text"
|
style={{ width: 28, height: 28 }}
|
||||||
/>
|
/>
|
||||||
<Text type="secondary" style={{ marginLeft: '8px' }}>{item.sparepart_unit || 'pcs'}</Text>
|
<Text type="secondary">{item.sparepart_unit ? ` / ${item.sparepart_unit}` : ' / pcs'}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user