feat: enhance DetailSparepart component with image upload and preview functionality

This commit is contained in:
2025-11-27 11:36:23 +07:00
parent afcb85a323
commit 572042ab53
2 changed files with 88 additions and 83 deletions

View File

@@ -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,12 +236,12 @@ 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>
<Row gutter={[16, 16]}>
<Col span={12}>
<Text strong>Stock</Text>
@@ -263,20 +265,24 @@ const DetailSparepart = (props) => {
/>
</Col>
</Row>
<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>
@@ -302,7 +308,7 @@ const DetailSparepart = (props) => {
/>
</Col>
</Row>
<Row gutter={[16, 16]}>
<Col span={24}>
<Text strong>Description</Text>
@@ -316,7 +322,6 @@ const DetailSparepart = (props) => {
/>
</Col>
</Row>
</div>
)}
</Modal>

View File

@@ -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