From afcb85a32380d0f73489c8fc224c1bd6e164dce3 Mon Sep 17 00:00:00 2001 From: Iqbal Rizqi Kurniawan Date: Wed, 26 Nov 2025 17:16:09 +0700 Subject: [PATCH] feat: add image upload functionality and stock update feature in sparepart components --- src/components/Global/TableList.jsx | 2 + .../sparepart/component/DetailSparepart.jsx | 267 +++++++++------- .../sparepart/component/ListSparepart.jsx | 1 + .../sparepart/component/SparepartCardList.jsx | 289 ++++++++++++------ 4 files changed, 344 insertions(+), 215 deletions(-) diff --git a/src/components/Global/TableList.jsx b/src/components/Global/TableList.jsx index 5a47021..ecaaf85 100644 --- a/src/components/Global/TableList.jsx +++ b/src/components/Global/TableList.jsx @@ -21,6 +21,7 @@ const TableList = memo(function TableList({ firstLoad = true, columnDynamic = false, cardComponent, // New prop for custom card component + onStockUpdate, // Prop to pass to card component }) { const [gridLoading, setGridLoading] = useState(false); @@ -166,6 +167,7 @@ const TableList = memo(function TableList({ showPreviewModal={showPreviewModal} showEditModal={showEditModal} showDeleteDialog={showDeleteDialog} + onStockUpdate={onStockUpdate} /> ) : ( diff --git a/src/pages/master/sparepart/component/DetailSparepart.jsx b/src/pages/master/sparepart/component/DetailSparepart.jsx index 7dd4e38..e1e7f77 100644 --- a/src/pages/master/sparepart/component/DetailSparepart.jsx +++ b/src/pages/master/sparepart/component/DetailSparepart.jsx @@ -1,7 +1,9 @@ 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 { createSparepart, updateSparepart } from '../../../../api/sparepart'; +import { uploadFile } from '../../../../api/file-uploads'; import { validateRun } from '../../../../Utils/validate'; const { Text } = Typography; @@ -9,6 +11,7 @@ const { TextArea } = Input; const DetailSparepart = (props) => { const [confirmLoading, setConfirmLoading] = useState(false); + const [imageFile, setImageFile] = useState(null); const defaultData = { sparepart_id: '', @@ -18,7 +21,8 @@ const DetailSparepart = (props) => { sparepart_item_type: '', sparepart_unit: '', sparepart_merk: '', - sparepart_stok: '', + sparepart_stok: '0', + image_url: '', }; const [formData, setFormData] = useState(defaultData); @@ -26,18 +30,14 @@ const DetailSparepart = (props) => { const handleCancel = () => { props.setSelectedData(null); props.setActionMode('list'); + setImageFile(null); }; const handleSave = async () => { setConfirmLoading(true); - // Daftar aturan validasi const validationRules = [ { 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 ( @@ -53,33 +53,42 @@ const DetailSparepart = (props) => { return; 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 = { sparepart_name: formData.sparepart_name, - sparepart_description: formData.sparepart_description, - sparepart_model: formData.sparepart_model, 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_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 ? await updateSparepart(formData.sparepart_id, payload) : await createSparepart(payload); - // Check if response is successful if (response && (response.statusCode === 200 || response.statusCode === 201)) { - const sparepartName = response.data?.sparepart_name || formData.sparepart_name; - NotifOk({ icon: 'success', title: 'Berhasil', - message: `Data Sparepart "${sparepartName}" berhasil ${ + message: `Data Sparepart berhasil ${ formData.sparepart_id ? 'diubah' : 'ditambahkan' }.`, }); - props.setActionMode('list'); + setImageFile(null); } else { NotifAlert({ icon: 'error', @@ -107,28 +116,44 @@ const DetailSparepart = (props) => { }); }; - const handleFieldChange = (name, value) => { + const handleSelectChange = (name, value) => { setFormData({ ...formData, [name]: value, }); }; - const handleStatusToggle = (event) => { - const isChecked = event; - setFormData({ - ...formData, - is_active: isChecked ? true : false, - }); - }; - useEffect(() => { if (props.selectedData) { setFormData(props.selectedData); } else { setFormData(defaultData); } + 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, + }; return ( { > {formData && (
- + + + Sparepart Name + * + + + + Item Type + + + -
- Sparepart Name - * - -
+ + + Stock + + + + Unit + + + -
- Sparepart Description -