From f52d61da628b38810d6b56a3a4877537465d8d7d Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Wed, 22 Oct 2025 15:58:17 +0700 Subject: [PATCH] feat: enhance DetailTag component with improved validation, UI updates, and refactored state management --- src/pages/master/tag/component/DetailTag.jsx | 841 +++++++++---------- 1 file changed, 379 insertions(+), 462 deletions(-) diff --git a/src/pages/master/tag/component/DetailTag.jsx b/src/pages/master/tag/component/DetailTag.jsx index 2bf55ea..6ec67fc 100644 --- a/src/pages/master/tag/component/DetailTag.jsx +++ b/src/pages/master/tag/component/DetailTag.jsx @@ -1,10 +1,11 @@ -import { useEffect, useState } from 'react'; -import { Modal, Input, Typography, Button, ConfigProvider, Switch, Select } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider, Select } from 'antd'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { createTag, updateTag, getAllTag } from '../../../../api/master-tag'; import { getAllDevice } from '../../../../api/master-device'; import { getAllPlantSection } from '../../../../api/master-plant-section'; import { getAllUnit } from '../../../../api/master-unit'; +import { validateRun } from '../../../../Utils/validate'; const { Text } = Typography; @@ -37,8 +38,7 @@ const DetailTag = (props) => { sub_section_id: null, }; - const [FormData, setFormData] = useState(defaultData); - const [nextTagCode, setNextTagCode] = useState('Auto-fill'); + const [formData, setformData] = useState(defaultData); const handleCancel = () => { props.setSelectedData(null); @@ -48,29 +48,26 @@ const DetailTag = (props) => { const handleSave = async () => { setConfirmLoading(true); - // Validasi required fields untuk CREATE - if (!FormData.tag_name || FormData.tag_name.trim() === '') { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Tag Name Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } + // Daftar aturan validasi + const validationRules = [ + { field: 'tag_name', label: 'Tag Name', required: true }, + { field: 'tag_number', label: 'Tag Number', required: true }, + ]; - if (!FormData.tag_number || FormData.tag_number === '') { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Tag Number Tidak Boleh Kosong', - }); - setConfirmLoading(false); + if ( + validateRun(formData, validationRules, (errorMessages) => { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: errorMessages, + }); + setConfirmLoading(false); + }) + ) return; - } // Validasi format number untuk tag_number - const tagNumberInt = parseInt(FormData.tag_number); + const tagNumberInt = parseInt(formData.tag_number); if (isNaN(tagNumberInt)) { NotifOk({ icon: 'warning', @@ -83,16 +80,15 @@ const DetailTag = (props) => { // Validasi duplicate tag_number try { - const params = new URLSearchParams({ limit: 10000 }); // Get all tags + const params = new URLSearchParams({ limit: 10000 }); const response = await getAllTag(params); if (response && response.data && response.data.data) { const existingTags = response.data.data; - // Check if tag_number already exists (exclude current tag when editing) const isDuplicate = existingTags.some((tag) => { const isSameNumber = parseInt(tag.tag_number) === tagNumberInt; - const isDifferentTag = FormData.tag_id ? tag.tag_id !== FormData.tag_id : true; + const isDifferentTag = formData.tag_id ? tag.tag_id !== formData.tag_id : true; return isSameNumber && isDifferentTag; }); @@ -108,7 +104,7 @@ const DetailTag = (props) => { } } catch (error) { console.error('Error checking duplicate tag number:', error); - NotifAlert({ + NotifOk({ icon: 'error', title: 'Error', message: 'Gagal memvalidasi Tag Number. Silakan coba lagi.', @@ -117,189 +113,145 @@ const DetailTag = (props) => { return; } - if (!FormData.data_type || FormData.data_type.trim() === '') { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Data Type Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - // Validasi data type harus Discrete atau Analog - const validDataTypes = ['Discrete', 'Analog']; - if (!validDataTypes.includes(FormData.data_type)) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: `Data Type harus "Discrete" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`, - }); - setConfirmLoading(false); - return; - } - - if (!FormData.unit || FormData.unit.trim() === '') { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Kolom Unit Tidak Boleh Kosong', - }); - setConfirmLoading(false); - return; - } - - // Device validation - if (!FormData.device_id) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Device harus dipilih', - }); - setConfirmLoading(false); - return; - } - - // Plant Sub Section validation - if (!FormData.sub_section_id) { - NotifOk({ - icon: 'warning', - title: 'Peringatan', - message: 'Plant Sub Section harus dipilih', - }); - setConfirmLoading(false); - return; + // Validasi data type hanya jika diisi + if (formData.data_type && formData.data_type.trim() !== '') { + const validDataTypes = ['Discrete', 'Analog']; + if (!validDataTypes.includes(formData.data_type)) { + NotifOk({ + icon: 'warning', + title: 'Peringatan', + message: `Data Type harus "Discrete" atau "Analog". Nilai "${formData.data_type}" tidak valid. Silakan pilih dari dropdown.`, + }); + setConfirmLoading(false); + return; + } } // Prepare payload berdasarkan backend validation schema const payload = { - tag_name: FormData.tag_name.trim(), - tag_number: parseInt(FormData.tag_number), - data_type: FormData.data_type, - unit: FormData.unit.trim(), - is_active: FormData.is_active, - is_alarm: FormData.is_alarm, - is_report: FormData.is_report, - is_history: FormData.is_history, - device_id: parseInt(FormData.device_id), + tag_name: formData.tag_name.trim(), + tag_number: parseInt(formData.tag_number), + is_active: formData.is_active, + is_alarm: formData.is_alarm, + is_report: formData.is_report, + is_history: formData.is_history, }; + // Add data_type only if it has a value + if (formData.data_type && formData.data_type.trim() !== '') { + payload.data_type = formData.data_type; + } + + // Add unit only if it has a value + if (formData.unit && formData.unit.trim() !== '') { + payload.unit = formData.unit.trim(); + } + + // Add device_id only if it's selected + if (formData.device_id) { + payload.device_id = parseInt(formData.device_id); + } + // Add limit fields only if they have values - if (FormData.lim_low_crash !== '' && FormData.lim_low_crash !== null) { - payload.lim_low_crash = parseFloat(FormData.lim_low_crash); + if (formData.lim_low_crash !== '' && formData.lim_low_crash !== null) { + payload.lim_low_crash = parseFloat(formData.lim_low_crash); } - if (FormData.lim_low !== '' && FormData.lim_low !== null) { - payload.lim_low = parseFloat(FormData.lim_low); + if (formData.lim_low !== '' && formData.lim_low !== null) { + payload.lim_low = parseFloat(formData.lim_low); } - if (FormData.lim_high !== '' && FormData.lim_high !== null) { - payload.lim_high = parseFloat(FormData.lim_high); + if (formData.lim_high !== '' && formData.lim_high !== null) { + payload.lim_high = parseFloat(formData.lim_high); } - if (FormData.lim_high_crash !== '' && FormData.lim_high_crash !== null) { - payload.lim_high_crash = parseFloat(FormData.lim_high_crash); + if (formData.lim_high_crash !== '' && formData.lim_high_crash !== null) { + payload.lim_high_crash = parseFloat(formData.lim_high_crash); } // Add sub_section_id only if it's selected - if (FormData.sub_section_id) { - payload.sub_section_id = parseInt(FormData.sub_section_id); + if (formData.sub_section_id) { + payload.sub_section_id = parseInt(formData.sub_section_id); } - // Debug logging - try { - let response; + const response = + props.actionMode === 'edit' + ? await updateTag(formData.tag_id, payload) + : await createTag(payload); - if (FormData.tag_id) { - // Update existing tag - response = await updateTag(FormData.tag_id, payload); - } else { - // Create new tag - response = await createTag(payload); - } - - // Check if response is successful if (response && (response.statusCode === 200 || response.statusCode === 201)) { - // response.data is already an object (converted in master-tag.jsx API) - const tagCode = response.data?.tag_code || ''; - const tagName = response.data?.tag_name || FormData.tag_name || ''; - const tagDisplay = tagCode ? `${tagCode} - ${tagName}` : tagName; + const action = props.actionMode === 'edit' ? 'diubah' : 'ditambahkan'; NotifOk({ icon: 'success', title: 'Berhasil', - message: `Data Tag "${tagDisplay}" berhasil ${ - FormData.tag_id ? 'diubah' : 'ditambahkan' - }.`, + message: `Data Tag berhasil ${action}.`, }); props.setActionMode('list'); } else { - NotifAlert({ + NotifOk({ icon: 'error', title: 'Gagal', message: response?.message || 'Terjadi kesalahan saat menyimpan data.', }); } } catch (error) { - console.error('Save Tag Error:', error); - console.error('Error details:', error); - NotifAlert({ + NotifOk({ icon: 'error', title: 'Error', - message: error.message || 'Terjadi kesalahan pada server. Coba lagi nanti.', + message: error.message || 'Terjadi kesalahan pada server.', }); + } finally { + setConfirmLoading(false); } - - setConfirmLoading(false); }; const handleInputChange = (e) => { const { name, value } = e.target; - setFormData({ - ...FormData, + setformData({ + ...formData, [name]: value, }); }; const handleSelectChange = (name, value) => { - setFormData({ - ...FormData, + setformData({ + ...formData, [name]: value, }); }; const handleDeviceChange = (deviceId) => { - const selectedDevice = deviceList.find((device) => device.device_id === deviceId); - setFormData({ - ...FormData, + setformData({ + ...formData, device_id: deviceId, }); }; - const handleStatusToggle = (isChecked) => { - setFormData({ - ...FormData, - is_active: isChecked, + const handleStatusToggle = (checked) => { + setformData({ + ...formData, + is_active: checked, }); }; - const handleAlarmToggle = (isChecked) => { - setFormData({ - ...FormData, - is_alarm: isChecked, + const handleAlarmToggle = (checked) => { + setformData({ + ...formData, + is_alarm: checked, }); }; - const handleReportToggle = (isChecked) => { - setFormData({ - ...FormData, - is_report: isChecked, + const handleReportToggle = (checked) => { + setformData({ + ...formData, + is_report: checked, }); }; - const handleHistoryToggle = (isChecked) => { - setFormData({ - ...FormData, - is_history: isChecked, + const handleHistoryToggle = (checked) => { + setformData({ + ...formData, + is_history: checked, }); }; @@ -309,12 +261,9 @@ const DetailTag = (props) => { const params = new URLSearchParams({ limit: 1000 }); const response = await getAllDevice(params); - if (response && response.data && response.data.data) { - const devices = response.data.data; - - // Filter hanya device yang active (is_active === true) + if (response && response.data) { + const devices = response.data; const activeDevices = devices.filter((device) => device.is_active === true); - setDeviceList(activeDevices); } } catch (error) { @@ -330,9 +279,8 @@ const DetailTag = (props) => { const params = new URLSearchParams({ limit: 1000 }); const response = await getAllPlantSection(params); - if (response && response.data && response.data.data) { - // Filter hanya plant sub section yang active - const activePlantSubSections = response.data.data.filter( + if (response && response.data) { + const activePlantSubSections = response.data.filter( (section) => section.is_active === true ); setPlantSubSectionList(activePlantSubSections); @@ -350,12 +298,9 @@ const DetailTag = (props) => { const params = new URLSearchParams({ limit: 1000 }); const response = await getAllUnit(params); - if (response && response.data && response.data.data) { - const units = response.data.data; - - // Filter hanya unit yang active (is_active === true) + if (response && response.data) { + const units = response.data; const activeUnits = units.filter((unit) => unit.is_active === true); - setUnitList(activeUnits); } } catch (error) { @@ -365,87 +310,20 @@ const DetailTag = (props) => { } }; - const generateNextTagCode = async () => { - try { - const params = new URLSearchParams({ limit: 10000 }); - const response = await getAllTag(params); - - if (response && response.data && response.data.data) { - const tags = response.data.data; - - if (tags.length === 0) { - setNextTagCode('TAG001'); - return; - } - - // Extract numeric part from tag codes and find the maximum - const tagNumbers = tags - .map((tag) => { - const match = tag.tag_code?.match(/tag(\d+)/i); - return match ? parseInt(match[1], 10) : 0; - }) - .filter((num) => !isNaN(num)); - - const maxNumber = tagNumbers.length > 0 ? Math.max(...tagNumbers) : 0; - const nextNumber = maxNumber + 1; - - // Format with leading zeros (TAG001, TAG002, etc.) - const nextCode = `TAG${String(nextNumber).padStart(3, '0')}`; - setNextTagCode(nextCode); - } else { - setNextTagCode('TAG001'); - } - } catch (error) { - console.error('Error generating next tag code:', error); - setNextTagCode('Auto-fill'); - } - }; - useEffect(() => { - const token = localStorage.getItem('token'); - if (token) { - if (props.showModal) { - // Load devices, plant sub sections, and units when modal opens - loadDevices(); - loadPlantSubSections(); - loadUnits(); - - // Generate next tag code only for add mode - if (props.actionMode === 'add' && !props.selectedData) { - generateNextTagCode(); - } - } - - if (props.selectedData != null) { - // Only set fields that are in defaultData to avoid sending extra fields - const filteredData = { - tag_id: props.selectedData.tag_id || '', - tag_code: props.selectedData.tag_code || '', - tag_name: props.selectedData.tag_name || '', - tag_number: props.selectedData.tag_number || '', - data_type: props.selectedData.data_type || '', - unit: props.selectedData.unit || '', - is_active: props.selectedData.is_active ?? true, - is_alarm: props.selectedData.is_alarm ?? false, - is_report: props.selectedData.is_report ?? false, - is_history: props.selectedData.is_history ?? false, - lim_low_crash: props.selectedData.lim_low_crash ?? '', - lim_low: props.selectedData.lim_low ?? '', - lim_high: props.selectedData.lim_high ?? '', - lim_high_crash: props.selectedData.lim_high_crash ?? '', - device_id: props.selectedData.device_id || null, - device_code: props.selectedData.device_code || '', - device_name: props.selectedData.device_name || '', - sub_section_id: props.selectedData.sub_section_id || null, - }; - setFormData(filteredData); - } else { - setFormData(defaultData); - } - } else { - // navigate('/signin'); // Uncomment if useNavigate is imported + if (props.showModal) { + // Load devices, plant sub sections, and units when modal opens + loadDevices(); + loadPlantSubSections(); + loadUnits(); } - }, [props.showModal, props.actionMode]); + + if (props.selectedData) { + setformData(props.selectedData); + } else { + setformData(defaultData); + } + }, [props.showModal, props.selectedData, props.actionMode]); return ( { onCancel={handleCancel} width={800} footer={[ - <> + - + { )} - , + , ]} > - {FormData && ( + {formData && (
- {/* Status dan Alarm dalam satu baris */} +
+
+ Status +
+
+
+ +
+
+ {formData.is_active ? 'Active' : 'Inactive'} +
+
+
+ {/* Alarm, Report, dan History dalam satu baris */}
{ gap: '16px', }} > - {/* Status Toggle */} -
-
- Status -
-
-
- -
-
- - {FormData.is_active === true ? 'Active' : 'Inactive'} - -
-
-
{/* Alarm Toggle */}
@@ -571,30 +430,19 @@ const DetailTag = (props) => { disabled={props.readOnly} style={{ backgroundColor: - FormData.is_alarm === true + formData.is_alarm === true ? '#23A55A' : '#bfbfbf', }} - checked={FormData.is_alarm === true} + checked={formData.is_alarm === true} onChange={handleAlarmToggle} />
- {FormData.is_alarm === true ? 'Yes' : 'No'} + {formData.is_alarm === true ? 'Yes' : 'No'}
-
-
- {/* Report dan History dalam satu baris */} -
-
{/* Report Toggle */}
@@ -613,16 +461,16 @@ const DetailTag = (props) => { disabled={props.readOnly} style={{ backgroundColor: - FormData.is_report === true + formData.is_report === true ? '#23A55A' : '#bfbfbf', }} - checked={FormData.is_report === true} + checked={formData.is_report === true} onChange={handleReportToggle} />
- {FormData.is_report === true ? 'Yes' : 'No'} + {formData.is_report === true ? 'Yes' : 'No'}
@@ -644,204 +492,273 @@ const DetailTag = (props) => { disabled={props.readOnly} style={{ backgroundColor: - FormData.is_history === true + formData.is_history === true ? '#23A55A' : '#bfbfbf', }} - checked={FormData.is_history === true} + checked={formData.is_history === true} onChange={handleHistoryToggle} />
- {FormData.is_history === true ? 'Yes' : 'No'} + {formData.is_history === true ? 'Yes' : 'No'}
+ + {/* Tag Code - Auto Increment & Read Only */}
Tag Code
+ {/* Tag Number dan Tag Name dalam satu baris */}
- Tag Number - * - -
-
- Tag Name - * - -
-
- Data Type - * - -
-
- Unit - * - + {/* Tag Number */} +
+ Tag Number + * + +
+ {/* Tag Name */} +
+ Tag Name + * + +
+
- {/* Limit Fields */} + {/* Plant Sub Section dan Device dalam satu baris */}
- Limit Low Crash - -
-
- Limit Low - -
-
- Limit High - -
-
- Limit High Crash - -
-
- Plant Sub Section - * - + handleSelectChange('sub_section_id', value) + } + disabled={props.readOnly} + loading={loadingPlantSubSections} + showSearch + allowClear + optionFilterProp="children" + filterOption={(input, option) => { + const text = option.children; + if (!text) return false; + return text.toLowerCase().includes(input.toLowerCase()); + }} > - {section.sub_section_name || ''} - - ))} - + {plantSubSectionList.map((section) => ( + + {section.sub_section_name || ''} + + ))} + +
+ {/* Device */} +
+ Device + + +
+ + {/* Data Type dan Unit dalam satu baris */}
- Device - * - + {/* Data Type */} +
+ Data Type + +
+ {/* Unit */} +
+ Unit + +
+
+ + {/* Limit Low Crash dan Limit Low dalam satu baris */} +
+
+ {/* Limit Low Crash */} +
+ Limit Low Crash + +
+ {/* Limit Low */} +
+ Limit Low + +
+
+
+ {/* Limit High dan Limit High Crash dalam satu baris */} +
+
+ {/* Limit High */} +
+ Limit High + +
+ {/* Limit High Crash */} +
+ Limit High Crash + +
+
- {/* Device ID hidden - value dari dropdown */} - )}