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 { Modal, Input, Select, Divider, Typography, Switch, Button, ConfigProvider, Upload, message, Row, Col } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Modal, Input, Select, Divider, Typography, Button, ConfigProvider, Upload, Row, Col, Image } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
|
||||
import { createSparepart, updateSparepart } from '../../../../api/sparepart';
|
||||
import { uploadFile } from '../../../../api/file-uploads';
|
||||
@@ -9,9 +9,20 @@ import { validateRun } from '../../../../Utils/validate';
|
||||
const { Text } = Typography;
|
||||
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 [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 = {
|
||||
sparepart_id: '',
|
||||
@@ -30,38 +41,48 @@ const DetailSparepart = (props) => {
|
||||
const handleCancel = () => {
|
||||
props.setSelectedData(null);
|
||||
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 () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
const validationRules = [
|
||||
{ field: 'sparepart_name', label: 'Sparepart Name', required: true },
|
||||
];
|
||||
const validationRules = [{ field: 'sparepart_name', label: 'Sparepart Name', required: true }];
|
||||
|
||||
if (
|
||||
validateRun(formData, validationRules, (errorMessages) => {
|
||||
NotifOk({
|
||||
icon: 'warning',
|
||||
title: 'Peringatan',
|
||||
message: errorMessages,
|
||||
});
|
||||
NotifOk({ icon: 'warning', title: 'Peringatan', message: errorMessages });
|
||||
setConfirmLoading(false);
|
||||
})
|
||||
)
|
||||
return;
|
||||
|
||||
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) {
|
||||
const uploadResponse = await uploadFile(imageFile, 'images');
|
||||
if (newFile && newFile.originFileObj) {
|
||||
const uploadResponse = await uploadFile(newFile.originFileObj, 'images');
|
||||
if (uploadResponse && uploadResponse.file_url) {
|
||||
imageUrl = uploadResponse.file_url;
|
||||
} else {
|
||||
throw new Error('Image upload failed or did not return a URL.');
|
||||
}
|
||||
} else if (fileList.length === 0 && formData.sparepart_id) {
|
||||
imageUrl = '';
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@@ -72,7 +93,7 @@ const DetailSparepart = (props) => {
|
||||
sparepart_merk: formData.sparepart_merk,
|
||||
sparepart_model: formData.sparepart_model,
|
||||
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
|
||||
@@ -83,12 +104,10 @@ const DetailSparepart = (props) => {
|
||||
NotifOk({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
message: `Data Sparepart berhasil ${
|
||||
formData.sparepart_id ? 'diubah' : 'ditambahkan'
|
||||
}.`,
|
||||
message: `Data Sparepart berhasil ${formData.sparepart_id ? 'diubah' : 'ditambahkan'}.`,
|
||||
});
|
||||
props.setActionMode('list');
|
||||
setImageFile(null);
|
||||
setFileList([]);
|
||||
} else {
|
||||
NotifAlert({
|
||||
icon: 'error',
|
||||
@@ -110,59 +129,45 @@ const DetailSparepart = (props) => {
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
const handleSelectChange = (name, value) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
setFormData({ ...formData, [name]: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (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 {
|
||||
setFormData(defaultData);
|
||||
setFileList([]);
|
||||
}
|
||||
setImageFile(null);
|
||||
}, [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,
|
||||
};
|
||||
const uploadButton = (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`${
|
||||
props.actionMode === 'add'
|
||||
? 'Tambah'
|
||||
: props.actionMode === 'preview'
|
||||
? 'Preview'
|
||||
: 'Edit'
|
||||
props.actionMode === 'add' ? 'Tambah' : props.actionMode === 'preview' ? 'Preview' : 'Edit'
|
||||
} Sparepart`}
|
||||
open={props.showModal}
|
||||
onCancel={handleCancel}
|
||||
@@ -178,7 +183,6 @@ const DetailSparepart = (props) => {
|
||||
defaultBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
defaultHoverBorderColor: '#23A55A',
|
||||
defaultHoverColor: '#23A55A',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -187,9 +191,7 @@ const DetailSparepart = (props) => {
|
||||
</ConfigProvider>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorBgContainer: '#209652',
|
||||
},
|
||||
token: { colorBgContainer: '#209652' },
|
||||
components: {
|
||||
Button: {
|
||||
defaultBg: '#23a55a',
|
||||
@@ -234,8 +236,8 @@ const DetailSparepart = (props) => {
|
||||
disabled={props.readOnly}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Select.Option value="air dryer">air dryer</Select.Option>
|
||||
<Select.Option value="compressor">compressor</Select.Option>
|
||||
<Select.Option value="Air Dryer">Air Dryer</Select.Option>
|
||||
<Select.Option value="Compressor">Compressor</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -267,16 +269,20 @@ const DetailSparepart = (props) => {
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Text strong>Foto</Text>
|
||||
<div>
|
||||
<Upload {...uploadProps}>
|
||||
<Button icon={<UploadOutlined />} disabled={props.readOnly}>Select File</Button>
|
||||
</Upload>
|
||||
{formData.image_url && !imageFile && (
|
||||
<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>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
beforeUpload={() => false}
|
||||
maxCount={1}
|
||||
disabled={props.readOnly}
|
||||
>
|
||||
{fileList.length >= 1 ? null : uploadButton}
|
||||
</Upload>
|
||||
<Modal open={previewOpen} title={previewTitle} footer={null} onCancel={handlePreviewCancel}>
|
||||
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
|
||||
</Modal>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -316,7 +322,6 @@ const DetailSparepart = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -91,7 +91,7 @@ const SparepartCardList = ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '16px 8px',
|
||||
height: '100%',
|
||||
}}
|
||||
@@ -100,7 +100,7 @@ const SparepartCardList = ({
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{
|
||||
marginBottom: '12px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
{item.sparepart_item_type}
|
||||
@@ -162,7 +162,7 @@ const SparepartCardList = ({
|
||||
{item[header]}
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
Available: {item.sparepart_stok || '0'}
|
||||
Available Stock: {item.sparepart_stok || '0'}
|
||||
</Text>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
|
||||
@@ -171,16 +171,16 @@ const SparepartCardList = ({
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() => handleQuantityChange(item.sparepart_id, quantity - 1)}
|
||||
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
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleQuantityChange(item.sparepart_id, quantity + 1)}
|
||||
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>
|
||||
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user