diff --git a/src/pages/master/brandDevice/AddBrandDevice.jsx b/src/pages/master/brandDevice/AddBrandDevice.jsx index a070744..2c7bd57 100644 --- a/src/pages/master/brandDevice/AddBrandDevice.jsx +++ b/src/pages/master/brandDevice/AddBrandDevice.jsx @@ -22,7 +22,9 @@ import ErrorCodeSimpleForm from './component/ErrorCodeSimpleForm'; import ErrorCodeListModal from './component/ErrorCodeListModal'; import FormActions from './component/FormActions'; import SolutionForm from './component/SolutionForm'; +import SparepartForm from './component/SparepartForm'; import { useSolutionLogic } from './hooks/solution'; +import { useSparepartLogic } from './hooks/sparepart'; import { uploadFile, getFolderFromFileType } from '../../../api/file-uploads'; import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; @@ -44,6 +46,7 @@ const AddBrandDevice = () => { const [brandForm] = Form.useForm(); const [errorCodeForm] = Form.useForm(); const [solutionForm] = Form.useForm(); + const [sparepartForm] = Form.useForm(); const [confirmLoading, setConfirmLoading] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [fileList, setFileList] = useState([]); @@ -70,6 +73,20 @@ const AddBrandDevice = () => { setSolutionsForExistingRecord, } = useSolutionLogic(solutionForm); + const { + sparepartFields, + sparepartTypes, + sparepartStatuses, + sparepartsToDelete, + handleAddSparepartField, + handleRemoveSparepartField, + handleSparepartTypeChange, + handleSparepartStatusChange, + resetSparepartFields, + getSparepartData, + setSparepartsForExistingRecord, + } = useSparepartLogic(sparepartForm); + useEffect(() => { setBreadcrumbItems([ { title: • Master }, @@ -138,13 +155,14 @@ const AddBrandDevice = () => { path_solution: sol.path_solution || '', is_active: sol.is_active !== false, })), - // Note: Sparepart data is collected but not sent to backend yet - // sparepart: (ec.sparepart || []).map((sp) => ({ - // type: sp.type || 'required', - // name: sp.name || '', - // quantity: sp.quantity || 1, - // is_active: sp.is_active !== false, - // })), + ...(ec.sparepart && ec.sparepart.length > 0 && { + sparepart: ec.sparepart.map((sp) => ({ + sparepart_name: sp.sparepart_name || sp.name || sp.label || '', + brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.sparepart_description || '', + is_active: sp.is_active !== false, + path_foto: sp.path_foto || '', + })), + }), })); const finalFormData = { @@ -202,6 +220,10 @@ const AddBrandDevice = () => { if (record.solution && record.solution.length > 0) { setSolutionsForExistingRecord(record.solution, solutionForm); } + + if (record.sparepart && record.sparepart.length > 0) { + setSparepartsForExistingRecord(record.sparepart, sparepartForm); + } }; const handleEditErrorCode = (record) => { @@ -260,6 +282,7 @@ const AddBrandDevice = () => { return; } + const sparepartData = getSparepartData(); const newErrorCode = { key: Date.now(), error_code: formValues.error_code, @@ -270,6 +293,7 @@ const AddBrandDevice = () => { status: formValues.status !== false, errorCodeIcon: errorCodeIcon, solution: solutions, + ...(sparepartData && sparepartData.length > 0 && { sparepart: sparepartData }), // Only add sparepart if there are spareparts }; if (editingErrorCodeKey) { @@ -315,6 +339,7 @@ const AddBrandDevice = () => { setFileList([]); setErrorCodeIcon(null); resetSolutionFields(); + resetSparepartFields(); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); }; @@ -439,7 +464,7 @@ const AddBrandDevice = () => { <> {/* Error Code Form Column */} - + { + {/* Sparepart Form Column */} + + + + + + + + {/* Error Codes Table Column */} - + { const [errorCodes, setErrorCodes] = useState([]); const [errorCodeIcon, setErrorCodeIcon] = useState(null); const [solutionForm] = Form.useForm(); + const [sparepartForm] = Form.useForm(); const { errorCodeFields, addErrorCode, removeErrorCode, editErrorCode } = useErrorCodeLogic( errorCodeForm, @@ -77,6 +80,20 @@ const EditBrandDevice = () => { setSolutionsForExistingRecord, } = useSolutionLogic(solutionForm); + const { + sparepartFields, + sparepartTypes, + sparepartStatuses, + sparepartsToDelete, + handleAddSparepartField, + handleRemoveSparepartField, + handleSparepartTypeChange, + handleSparepartStatusChange, + resetSparepartFields, + getSparepartData, + setSparepartsForExistingRecord, + } = useSparepartLogic(sparepartForm); + useEffect(() => { const fetchBrandData = async () => { const token = localStorage.getItem('token'); @@ -139,6 +156,7 @@ const EditBrandDevice = () => { path_icon: ec.path_icon || '', status: ec.is_active, solution: ec.solution || [], + sparepart: ec.sparepart || [], errorCodeIcon: ec.path_icon ? { name: 'icon', @@ -226,6 +244,12 @@ const EditBrandDevice = () => { path_solution: sol.path_solution || '', is_active: sol.is_active !== false, })), + sparepart: (ec.sparepart || []).map((sp) => ({ + sparepart_name: sp.sparepart_name || sp.name || sp.label || '', + brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.brand_sparepart_description || '', + is_active: sp.is_active !== false, + path_foto: sp.path_foto || '', + })), }; } @@ -244,6 +268,14 @@ const EditBrandDevice = () => { path_solution: sol.path_solution || '', is_active: sol.is_active !== false, })), + ...(ec.sparepart && ec.sparepart.length > 0 && { + sparepart: ec.sparepart.map((sp) => ({ + sparepart_name: sp.sparepart_name || sp.name || sp.label || '', + brand_sparepart_description: sp.brand_sparepart_description || sp.description || sp.brand_sparepart_description || '', + is_active: sp.is_active !== false, + path_foto: sp.path_foto || '', + })), + }), }; }), }; @@ -294,6 +326,13 @@ const EditBrandDevice = () => { } else { resetSolutionFields(); } + + // Load spareparts to sparepart form + if (record.sparepart && record.sparepart.length > 0) { + setSparepartsForExistingRecord(record.sparepart, sparepartForm); + } else { + resetSparepartFields(); + } }; const handleEditErrorCode = (record) => { @@ -313,6 +352,11 @@ const EditBrandDevice = () => { setSolutionsForExistingRecord(record.solution, solutionForm); } + // Load spareparts to sparepart form + if (record.sparepart && record.sparepart.length > 0) { + setSparepartsForExistingRecord(record.sparepart, sparepartForm); + } + const formElement = document.querySelector('.ant-form'); if (formElement) { formElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); @@ -336,6 +380,9 @@ const EditBrandDevice = () => { return; } + // Get sparepart data from sparepart form + const sparepartData = getSparepartData(); + // Create complete error code object const newErrorCode = { error_code: errorCodeValues.error_code, @@ -345,6 +392,7 @@ const EditBrandDevice = () => { path_icon: errorCodeIcon?.uploadPath || '', status: errorCodeValues.status === undefined ? true : errorCodeValues.status, solution: solutionData, + ...(sparepartData && sparepartData.length > 0 && { sparepart: sparepartData }), errorCodeIcon: errorCodeIcon, key: editingErrorCodeKey || `temp-${Date.now()}`, }; @@ -402,6 +450,7 @@ const EditBrandDevice = () => { setFileList([]); setErrorCodeIcon(null); resetSolutionFields(); + resetSparepartFields(); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); }; @@ -428,6 +477,7 @@ const EditBrandDevice = () => { const handleCreateNewErrorCode = () => { resetErrorCodeForm(); resetSolutionFields(); + resetSparepartFields(); setErrorCodeIcon(null); setIsErrorCodeFormReadOnly(false); setEditingErrorCodeKey(null); @@ -496,7 +546,7 @@ const EditBrandDevice = () => { return ( <> - + @@ -562,7 +612,33 @@ const EditBrandDevice = () => { - + + + Spareparts + + } + size="small" + > + + + + + + { + const [currentStatus, setCurrentStatus] = useState(sparepartStatus ?? true); + const [sparepartList, setSparepartList] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setCurrentStatus(sparepartStatus ?? true); + loadSpareparts(); + }, [sparepartStatus]); + + const loadSpareparts = async () => { + setLoading(true); + try { + // Get all spareparts from the API + const params = new URLSearchParams(); + params.set('limit', '100'); // Get all spareparts + + const response = await getAllSparepart(params); + // Response structure should have { data: [...], statusCode: 200 } + if (response && (response.statusCode === 200 || response.data)) { + // If response has data array directly + const sparepartData = response.data?.data || response.data || []; + setSparepartList(sparepartData); + if (onSparepartChange) { + onSparepartChange(sparepartData); + } + } else { + // For demo purposes, use mock data if API fails + setSparepartList([ + { brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' }, + { brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' }, + { brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' }, + ]); + if (onSparepartChange) { + onSparepartChange([ + { brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' }, + { brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' }, + { brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' }, + ]); + } + } + } catch (error) { + console.error('Error loading spareparts:', error); + // Default mock data + const mockSpareparts = [ + { brand_sparepart_id: 1, sparepart_name: 'Compressor Oil Filter', brand_sparepart_description: 'Oil filter for compressor' }, + { brand_sparepart_id: 2, sparepart_name: 'Air Intake Filter', brand_sparepart_description: 'Air intake filter' }, + { brand_sparepart_id: 3, sparepart_name: 'Cooling Fan Motor', brand_sparepart_description: 'Motor for cooling fan' }, + ]; + setSparepartList(mockSpareparts); + if (onSparepartChange) { + onSparepartChange(mockSpareparts); + } + } finally { + setLoading(false); + } + }; + + const sparepartOptions = sparepartList.map(sparepart => ({ + label: sparepart.sparepart_name || sparepart.sparepart_name || `Sparepart ${sparepart.sparepart_id || sparepart.brand_sparepart_id}`, + value: sparepart.sparepart_id || sparepart.brand_sparepart_id, + description: sparepart.sparepart_description + })); + + return ( + + + Sparepart #{index + 1} + + + + + + + + { + setCurrentStatus(checked); + }} + style={{ + backgroundColor: currentStatus ? '#23A55A' : '#bfbfbf' + }} + /> + + + {currentStatus ? 'Active' : 'Inactive'} + + + + {canRemove && !isReadOnly && ( + } + onClick={onRemove} + /> + )} + + + + {/* Sparepart Description */} + + + + + ); +}; + +export default SparepartField; \ No newline at end of file diff --git a/src/pages/master/brandDevice/component/SparepartForm.jsx b/src/pages/master/brandDevice/component/SparepartForm.jsx index 696dbc8..7cc1453 100644 --- a/src/pages/master/brandDevice/component/SparepartForm.jsx +++ b/src/pages/master/brandDevice/component/SparepartForm.jsx @@ -1,18 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { - Form, - Input, - Button, - Divider, - Typography, - Switch, - Space, - Card, - Upload, - message, -} from 'antd'; -import { PlusOutlined, DeleteOutlined, UploadOutlined } from '@ant-design/icons'; -import { uploadFile } from '../../../../api/file-uploads'; +import React, { useState } from 'react'; +import { Form, Card, Typography, Divider, Button } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import SparepartField from './SparepartField'; const { Text } = Typography; @@ -21,238 +10,67 @@ const SparepartForm = ({ sparepartFields, onAddSparepartField, onRemoveSparepartField, - onSparepartTypeChange, - onSparepartStatusChange, - onSparepartImageUpload, - onSparepartImageRemove, - sparepartImages = {}, isReadOnly = false, + spareparts = [], + onSparepartChange }) => { - const [fieldStatuses, setFieldStatuses] = useState({}); + const [sparepartList, setSparepartList] = useState([]); - // Watch form values for each field - const getFieldValue = (fieldName) => { - try { - const values = sparepartForm?.getFieldsValue(); - return values?.sparepart_items?.[fieldName]?.status ?? true; - } catch { - return true; + const handleSparepartChange = (list) => { + setSparepartList(list); + if (onSparepartChange) { + onSparepartChange(list); } }; - useEffect(() => { - // Update field statuses when form changes - const newStatuses = {}; - sparepartFields.forEach((field) => { - newStatuses[field.key] = getFieldValue(field.key); - }); - setFieldStatuses(newStatuses); - }, [sparepartFields, sparepartForm]); - - const handleImageUpload = async (fieldKey, file) => { - // Check if file is an image - const isImage = file.type.startsWith('image/'); - if (!isImage) { - message.error('You can only upload image files!'); - return Upload.LIST_IGNORE; - } - - // Check file size (max 2MB) - const isLt2M = file.size / 1024 / 1024 < 2; - if (!isLt2M) { - message.error('Image must be smaller than 2MB!'); - return Upload.LIST_IGNORE; - } - - try { - const fileExtension = file.name.split('.').pop().toLowerCase(); - const isImageFile = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes( - fileExtension - ); - const fileType = isImageFile ? 'image' : 'pdf'; - const folder = 'images'; - - const uploadResponse = await uploadFile(file, folder); - const imagePath = - uploadResponse.data?.path_icon || uploadResponse.data?.path_solution || ''; - - if (imagePath) { - onSparepartImageUpload && - onSparepartImageUpload(fieldKey, { - name: file.name, - uploadPath: imagePath, - fileExtension, - isImage: isImageFile, - size: file.size, - }); - message.success(`${file.name} uploaded successfully!`); - } else { - message.error(`Failed to upload ${file.name}`); - } - } catch (error) { - console.error('Error uploading image:', error); - message.error(`Failed to upload ${file.name}`); - } - }; - - const handleImageRemove = (fieldKey) => { - onSparepartImageRemove && onSparepartImageRemove(fieldKey); - }; return ( - - {isReadOnly ? 'Sparepart Details' : 'Tambah Sparepart'} - - - {/* Dynamic Sparepart Fields */} Sparepart Items {sparepartFields.map((field, index) => ( - - Sparepart {index + 1} - - {!isReadOnly && sparepartFields.length > 1 && ( - - } - onClick={() => onRemoveSparepartField(field.key)} - /> - - )} - - - } - > - - {/* Sparepart Name */} - - - - - {/* Description */} - - - - - {/* Image Upload */} - - {!isReadOnly ? ( - handleImageUpload(field.key, file)} - showUploadList={false} - accept="image/*" - style={{ width: '100%' }} - > - } style={{ width: '100%' }}> - Upload Sparepart Image - - - ) : ( - - No upload allowed - - )} - - {sparepartImages[field.key] && ( - - - - - - {sparepartImages[field.key].name} - - - - Size:{' '} - {( - sparepartImages[field.key].size / 1024 - ).toFixed(1)}{' '} - KB - - - {!isReadOnly && ( - handleImageRemove(field.key)} - > - Remove - - )} - - - )} - - - + fieldKey={field.key} + fieldName={field.name} + index={index} + sparepartStatus={field.status} + onRemove={() => onRemoveSparepartField(field.key)} + isReadOnly={isReadOnly} + canRemove={sparepartFields.length > 1} + spareparts={sparepartList} + onSparepartChange={handleSparepartChange} + /> ))} {!isReadOnly && ( - - onAddSparepartField()} - icon={} - style={{ width: '100%' }} - > - + Add Sparepart - - + <> + + } + style={{ width: '100%' }} + > + + Add Sparepart + + + + + * Sparepart is optional and can be added for each error code if needed. + + + > )} ); }; -export default SparepartForm; +export default SparepartForm; \ No newline at end of file diff --git a/src/pages/master/brandDevice/hooks/sparepart.js b/src/pages/master/brandDevice/hooks/sparepart.js index 09ad295..a58bcfb 100644 --- a/src/pages/master/brandDevice/hooks/sparepart.js +++ b/src/pages/master/brandDevice/hooks/sparepart.js @@ -1,115 +1,141 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; export const useSparepartLogic = (sparepartForm) => { - const [sparepartFields, setSparepartFields] = useState([ - { name: ['sparepart_items', 0], key: 0 } - ]); - const [sparepartTypes, setSparepartTypes] = useState({ 0: 'required' }); - const [sparepartStatuses, setSparepartStatuses] = useState({ 0: true }); - - const handleAddSparepartField = () => { - const newKey = Date.now(); // Use timestamp for unique key - const newField = { name: ['sparepart_items', newKey], key: newKey }; + const [sparepartFields, setSparepartFields] = useState([]); + const [sparepartTypes, setSparepartTypes] = useState({}); + const [sparepartStatuses, setSparepartStatuses] = useState({}); + const [sparepartsToDelete, setSparepartsToDelete] = useState(new Set()); + const handleAddSparepartField = useCallback(() => { + const newKey = Date.now(); + const newField = { + key: newKey, + name: sparepartFields.length, + isCreated: true, + }; setSparepartFields(prev => [...prev, newField]); - setSparepartTypes(prev => ({ ...prev, [newKey]: 'required' })); - setSparepartStatuses(prev => ({ ...prev, [newKey]: true })); + setSparepartTypes(prev => ({ + ...prev, + [newKey]: 'required' + })); + setSparepartStatuses(prev => ({ + ...prev, + [newKey]: true + })); + }, [sparepartFields.length]); - // Set default values for the new field - setTimeout(() => { - sparepartForm.setFieldValue(['sparepart_items', newKey, 'type'], 'required'); - sparepartForm.setFieldValue(['sparepart_items', newKey, 'quantity'], 1); - }, 0); - }; + const handleRemoveSparepartField = useCallback((key) => { + setSparepartFields(prev => prev.filter(field => field.key !== key)); + setSparepartTypes(prev => { + const newTypes = { ...prev }; + delete newTypes[key]; + return newTypes; + }); + setSparepartStatuses(prev => { + const newStatuses = { ...prev }; + delete newStatuses[key]; + return newStatuses; + }); + + // Add to delete list if it's not a new field + setSparepartsToDelete(prev => new Set([...prev, key])); + }, []); - const handleRemoveSparepartField = (key) => { - if (sparepartFields.length <= 1) { - return; // Keep at least one sparepart field + const handleSparepartTypeChange = useCallback((key, type) => { + setSparepartTypes(prev => ({ + ...prev, + [key]: type + })); + }, []); + + const handleSparepartStatusChange = useCallback((key, status) => { + setSparepartStatuses(prev => ({ + ...prev, + [key]: status + })); + }, []); + + const resetSparepartFields = useCallback(() => { + setSparepartFields([]); + setSparepartTypes({}); + setSparepartStatuses({}); + setSparepartsToDelete(new Set()); + }, []); + + const getSparepartData = useCallback(() => { + if (!sparepartForm) return []; + + const values = sparepartForm.getFieldsValue(); + const data = []; + + sparepartFields.forEach((field, index) => { + const fieldData = { + sparepart_id: values[`sparepart_id_${field.name}`], + sparepart_name: values[`sparepart_name_${field.name}`], + sparepart_description: values[`sparepart_description_${field.name}`], + status: values[`sparepart_status_${field.name}`], + type: sparepartTypes[field.key] || 'required', + }; + + // Only add if required fields are filled + if (fieldData.sparepart_id) { + data.push(fieldData); + } + }); + + return data; + }, [sparepartForm, sparepartFields, sparepartTypes]); + + const setSparepartsForExistingRecord = useCallback((sparepartData, form) => { + resetSparepartFields(); + + if (!sparepartData || !Array.isArray(sparepartData)) { + return; } - setSparepartFields(prev => prev.filter(field => field.key !== key)); - - // Clean up type and status - const newTypes = { ...sparepartTypes }; - const newStatuses = { ...sparepartStatuses }; - delete newTypes[key]; - delete newStatuses[key]; - - setSparepartTypes(newTypes); - setSparepartStatuses(newStatuses); - }; - - const handleSparepartTypeChange = (key, value) => { - setSparepartTypes(prev => ({ ...prev, [key]: value })); - }; - - const handleSparepartStatusChange = (key, value) => { - setSparepartStatuses(prev => ({ ...prev, [key]: value })); - }; - - const resetSparepartFields = () => { - setSparepartFields([{ name: ['sparepart_items', 0], key: 0 }]); - setSparepartTypes({ 0: 'required' }); - setSparepartStatuses({ 0: true }); - - // Reset form values - sparepartForm.resetFields(); - sparepartForm.setFieldsValue({ - sparepart_status_0: true, - sparepart_type_0: 'required', - }); - }; - - const getSparepartData = () => { - const values = sparepartForm.getFieldsValue(); - return sparepartFields.map(field => { - const key = field.key; - const sparepartPath = field.name.join(','); - const sparepart = values[sparepartPath]; - - return sparepart && sparepart.name && sparepart.name.trim() !== '' ? { - name: sparepart.name || '', - description: sparepart.description || '', - is_active: sparepart.status !== false, - } : null; - }).filter(Boolean); - }; - - const setSparepartForExistingRecord = (spareparts, form) => { - if (!spareparts || spareparts.length === 0) return; - - const newFields = spareparts.map((sparepart, index) => ({ - name: ['sparepart_items', sparepart.id || index], - key: sparepart.id || index + const newFields = sparepartData.map((sp, index) => ({ + key: sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`, + name: index, + isCreated: false, })); setSparepartFields(newFields); - // Set sparepart values - const formValues = {}; - Object.keys(spareparts).forEach(index => { - const key = spareparts[index].id || index; - const sparepart = spareparts[index]; - formValues[`sparepart_items,${key}`] = { - name: sparepart.name || '', - description: sparepart.description || '', - status: sparepart.is_active !== false, - }; - }); + // Set form values for existing spareparts + setTimeout(() => { + const formValues = {}; + sparepartData.forEach((sp, index) => { + const sparepartId = sp.brand_sparepart_id || sp.sparepart_id || sp.sparepart_name; + formValues[`sparepart_id_${index}`] = sparepartId; + formValues[`sparepart_status_${index}`] = sp.is_active ?? sp.status ?? true; + formValues[`sparepart_description_${index}`] = sp.brand_sparepart_description || sp.description || sp.sparepart_name; - form.setFieldsValue(formValues); - }; + setSparepartTypes(prev => ({ + ...prev, + [sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.type || sp.sparepart_type || 'required' + })); + + setSparepartStatuses(prev => ({ + ...prev, + [sp.brand_sparepart_id || sp.sparepart_id || `existing-${index}`]: sp.is_active ?? sp.status ?? true + })); + }); + + form.setFieldsValue(formValues); + }, 0); + }, [resetSparepartFields]); return { sparepartFields, sparepartTypes, sparepartStatuses, + sparepartsToDelete, handleAddSparepartField, handleRemoveSparepartField, handleSparepartTypeChange, handleSparepartStatusChange, resetSparepartFields, getSparepartData, - setSparepartForExistingRecord, + setSparepartsForExistingRecord, }; }; \ No newline at end of file