feat: implement field auto-incrementing code

This commit is contained in:
2025-10-20 19:19:59 +07:00
parent 6d45b5bf11
commit fb3e500139
4 changed files with 428 additions and 60 deletions

View File

@@ -12,7 +12,7 @@ import {
} from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createApd, getJenisPermit, updateApd } from '../../../../api/master-apd';
import { createDevice, updateDevice } from '../../../../api/master-device';
import { createDevice, updateDevice, getAllDevice } from '../../../../api/master-device';
import { Checkbox } from 'antd';
const CheckboxGroup = Checkbox.Group;
@@ -33,6 +33,7 @@ const DetailDevice = (props) => {
};
const [FormData, setFormData] = useState(defaultData);
const [nextDeviceCode, setNextDeviceCode] = useState('Auto-fill');
const [jenisPermit, setJenisPermit] = useState([]);
const [checkedList, setCheckedList] = useState([]);
@@ -215,12 +216,55 @@ const DetailDevice = (props) => {
});
};
const generateNextDeviceCode = async () => {
try {
const params = new URLSearchParams({ limit: 10000 });
const response = await getAllDevice(params);
if (response && response.data && response.data.data) {
const devices = response.data.data;
if (devices.length === 0) {
setNextDeviceCode('DVC001');
return;
}
// Extract numeric part from device codes and find the maximum
const deviceNumbers = devices
.map((device) => {
const match = device.device_code?.match(/dvc(\d+)/i);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => !isNaN(num));
const maxNumber = deviceNumbers.length > 0 ? Math.max(...deviceNumbers) : 0;
const nextNumber = maxNumber + 1;
// Format with leading zeros (DVC001, DVC002, etc.)
const nextCode = `DVC${String(nextNumber).padStart(3, '0')}`;
setNextDeviceCode(nextCode);
} else {
setNextDeviceCode('DVC001');
}
} catch (error) {
console.error('Error generating next device code:', error);
setNextDeviceCode('Auto-fill');
}
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Only call getDataJenisPermit if permitDefault is enabled
if (props.permitDefault) {
getDataJenisPermit();
if (props.showModal) {
// Only call getDataJenisPermit if permitDefault is enabled
if (props.permitDefault) {
getDataJenisPermit();
}
// Generate next device code only for add mode
if (props.actionMode === 'add' && !props.selectedData) {
generateNextDeviceCode();
}
}
if (props.selectedData != null) {
@@ -234,7 +278,7 @@ const DetailDevice = (props) => {
} else {
// navigate('/signin'); // Uncomment if useNavigate is imported
}
}, [props.showModal]);
}, [props.showModal, props.actionMode]);
return (
<Modal
@@ -332,17 +376,21 @@ const DetailDevice = (props) => {
disabled
/>
</div>
{/* <div style={{ marginBottom: 12 }}>
{/* Device Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Device Code</Text>
<Text style={{ color: 'red' }}> *</Text>
<Input
name="device_code"
value={FormData.device_code}
onChange={handleInputChange}
placeholder="Enter Device Code"
readOnly={props.readOnly}
value={FormData.device_code || nextDeviceCode}
placeholder={nextDeviceCode}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: FormData.device_code ? '#000000' : '#bfbfbf'
}}
/>
</div> */}
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Device Name</Text>
<Text style={{ color: 'red' }}> *</Text>

View File

@@ -9,7 +9,7 @@ import {
Divider,
} from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section';
import { createPlantSection, updatePlantSection, getAllPlantSection } from '../../../../api/master-plant-section';
const { Text } = Typography;
@@ -24,6 +24,7 @@ const DetailPlantSection = (props) => {
};
const [FormData, setFormData] = useState(defaultData);
const [nextPlantSectionCode, setNextPlantSectionCode] = useState('Auto-fill');
const handleInputChange = (e) => {
const { name, value } = e.target;
@@ -103,14 +104,56 @@ const DetailPlantSection = (props) => {
});
};
const generateNextPlantSectionCode = async () => {
try {
const params = new URLSearchParams({ limit: 10000 });
const response = await getAllPlantSection(params);
if (response && response.data && response.data.data) {
const sections = response.data.data;
if (sections.length === 0) {
setNextPlantSectionCode('SUB001');
return;
}
// Extract numeric part from plant section codes and find the maximum
const sectionNumbers = sections
.map((section) => {
const match = section.sub_section_code?.match(/sub(\d+)/i);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => !isNaN(num));
const maxNumber = sectionNumbers.length > 0 ? Math.max(...sectionNumbers) : 0;
const nextNumber = maxNumber + 1;
// Format with leading zeros (SUB001, SUB002, etc.)
const nextCode = `SUB${String(nextNumber).padStart(3, '0')}`;
setNextPlantSectionCode(nextCode);
} else {
setNextPlantSectionCode('SUB001');
}
} catch (error) {
console.error('Error generating next plant section code:', error);
setNextPlantSectionCode('Auto-fill');
}
};
useEffect(() => {
if (props.showModal) {
// Generate next plant section code only for add mode
if (props.actionMode === 'add' && !props.selectedData) {
generateNextPlantSectionCode();
}
}
if (props.selectedData) {
setFormData(props.selectedData);
} else {
setFormData(defaultData);
}
}, [props.showModal, props.selectedData]);
}, [props.showModal, props.selectedData, props.actionMode]);
return (
<Modal
@@ -184,16 +227,21 @@ const DetailPlantSection = (props) => {
</div>
<Divider style={{ margin: '12px 0' }} />
{props.actionMode !== 'add' && (
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Section Code</Text>
<Input
name="sub_section_code"
value={FormData.sub_section_code}
disabled
/>
</div>
)}
{/* Plant Section Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Section Code</Text>
<Input
name="sub_section_code"
value={FormData.sub_section_code || nextPlantSectionCode}
placeholder={nextPlantSectionCode}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: FormData.sub_section_code ? '#000000' : '#bfbfbf'
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Sub Section Name</Text>

View File

@@ -26,12 +26,19 @@ const DetailTag = (props) => {
unit: '',
is_active: true,
is_alarm: false,
is_report: false,
is_history: false,
lim_low_crash: '',
lim_low: '',
lim_high: '',
lim_high_crash: '',
device_id: null,
sub_section_id: null,
};
const [FormData, setFormData] = useState(defaultData);
const [nextTagCode, setNextTagCode] = useState('Auto-fill');
const handleCancel = () => {
props.setSelectedData(null);
@@ -120,13 +127,13 @@ const DetailTag = (props) => {
return;
}
// Validasi data type harus Diskrit atau Analog
const validDataTypes = ['Diskrit', 'Analog'];
// 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 "Diskrit" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`,
message: `Data Type harus "Discrete" atau "Analog". Nilai "${FormData.data_type}" tidak valid. Silakan pilih dari dropdown.`,
});
setConfirmLoading(false);
return;
@@ -153,6 +160,17 @@ const DetailTag = (props) => {
return;
}
// Plant Sub Section validation
if (!FormData.sub_section_id) {
NotifOk({
icon: 'warning',
title: 'Peringatan',
message: 'Plant Sub Section harus dipilih',
});
setConfirmLoading(false);
return;
}
// Prepare payload berdasarkan backend validation schema
const payload = {
tag_name: FormData.tag_name.trim(),
@@ -161,9 +179,25 @@ const DetailTag = (props) => {
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),
};
// 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 !== '' && 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_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);
@@ -184,14 +218,17 @@ const DetailTag = (props) => {
// 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;
NotifOk({
icon: 'success',
title: 'Berhasil',
message:
response.message ||
`Data Tag "${response.data?.tag_name || FormData.tag_name}" berhasil ${
FormData.tag_id ? 'diubah' : 'ditambahkan'
}.`,
message: `Data Tag "${tagDisplay}" berhasil ${
FormData.tag_id ? 'diubah' : 'ditambahkan'
}.`,
});
props.setActionMode('list');
@@ -252,6 +289,20 @@ const DetailTag = (props) => {
});
};
const handleReportToggle = (isChecked) => {
setFormData({
...FormData,
is_report: isChecked,
});
};
const handleHistoryToggle = (isChecked) => {
setFormData({
...FormData,
is_history: isChecked,
});
};
const loadDevices = async () => {
setLoadingDevices(true);
try {
@@ -314,6 +365,42 @@ 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) {
@@ -322,6 +409,11 @@ const DetailTag = (props) => {
loadDevices();
loadPlantSubSections();
loadUnits();
// Generate next tag code only for add mode
if (props.actionMode === 'add' && !props.selectedData) {
generateNextTagCode();
}
}
if (props.selectedData != null) {
@@ -335,6 +427,12 @@ const DetailTag = (props) => {
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 || '',
@@ -347,7 +445,7 @@ const DetailTag = (props) => {
} else {
// navigate('/signin'); // Uncomment if useNavigate is imported
}
}, [props.showModal]);
}, [props.showModal, props.actionMode]);
return (
<Modal
@@ -360,6 +458,7 @@ const DetailTag = (props) => {
} Tag`}
open={props.showModal}
onCancel={handleCancel}
width={800}
footer={[
<>
<ConfigProvider
@@ -413,19 +512,6 @@ const DetailTag = (props) => {
disabled
/>
</div>
{/* Tag Code hanya ditampilkan saat EDIT atau PREVIEW */}
{(props.actionMode === 'edit' || props.actionMode === 'preview') && (
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Code</Text>
<Input
name="tag_code"
value={FormData.tag_code}
onChange={handleInputChange}
placeholder="Auto Generate"
disabled
/>
</div>
)}
{/* Status dan Alarm dalam satu baris */}
<div style={{ marginBottom: 12 }}>
<div
@@ -500,6 +586,94 @@ const DetailTag = (props) => {
</div>
</div>
</div>
{/* Report dan History dalam satu baris */}
<div style={{ marginBottom: 12 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
gap: '16px',
}}
>
{/* Report Toggle */}
<div style={{ flex: 1 }}>
<div>
<Text strong>Report</Text>
<Text style={{ color: 'red' }}> *</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_report === true
? '#23A55A'
: '#bfbfbf',
}}
checked={FormData.is_report === true}
onChange={handleReportToggle}
/>
</div>
<div>
<Text>{FormData.is_report === true ? 'Yes' : 'No'}</Text>
</div>
</div>
</div>
{/* History Toggle */}
<div style={{ flex: 1 }}>
<div>
<Text strong>History</Text>
<Text style={{ color: 'red' }}> *</Text>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<Switch
disabled={props.readOnly}
style={{
backgroundColor:
FormData.is_history === true
? '#23A55A'
: '#bfbfbf',
}}
checked={FormData.is_history === true}
onChange={handleHistoryToggle}
/>
</div>
<div>
<Text>{FormData.is_history === true ? 'Yes' : 'No'}</Text>
</div>
</div>
</div>
</div>
</div>
{/* Tag Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Code</Text>
<Input
name="tag_code"
value={FormData.tag_code || nextTagCode}
placeholder={nextTagCode}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: FormData.tag_code ? '#000000' : '#bfbfbf',
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Tag Number</Text>
<Text style={{ color: 'red' }}> *</Text>
@@ -532,7 +706,7 @@ const DetailTag = (props) => {
onChange={(value) => handleSelectChange('data_type', value)}
disabled={props.readOnly}
>
<Select.Option value="Diskrit">Diskrit</Select.Option>
<Select.Option value="Discrete">Discrete</Select.Option>
<Select.Option value="Analog">Analog</Select.Option>
</Select>
</div>
@@ -562,8 +736,58 @@ const DetailTag = (props) => {
))}
</Select>
</div>
{/* Limit Fields */}
<div style={{ marginBottom: 12 }}>
<Text strong>Limit Low Crash</Text>
<Input
name="lim_low_crash"
value={FormData.lim_low_crash}
onChange={handleInputChange}
placeholder="Enter Limit Low Crash"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit Low</Text>
<Input
name="lim_low"
value={FormData.lim_low}
onChange={handleInputChange}
placeholder="Enter Limit Low"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit High</Text>
<Input
name="lim_high"
value={FormData.lim_high}
onChange={handleInputChange}
placeholder="Enter Limit High"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Limit High Crash</Text>
<Input
name="lim_high_crash"
value={FormData.lim_high_crash}
onChange={handleInputChange}
placeholder="Enter Limit High Crash"
readOnly={props.readOnly}
type="number"
step="any"
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Plant Sub Section</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
style={{ width: '100%' }}
placeholder="Select Plant Sub Section"

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Modal, Input, Typography, Button, ConfigProvider, Switch } from 'antd';
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createUnit, updateUnit } from '../../../../api/master-unit';
import { createUnit, updateUnit, getAllUnit } from '../../../../api/master-unit';
const { Text } = Typography;
@@ -16,6 +16,7 @@ const DetailUnit = (props) => {
};
const [FormData, setFormData] = useState(defaultData);
const [nextUnitCode, setNextUnitCode] = useState('Auto-fill');
const handleCancel = () => {
props.setSelectedData(null);
@@ -117,9 +118,52 @@ const DetailUnit = (props) => {
});
};
const generateNextUnitCode = async () => {
try {
const params = new URLSearchParams({ limit: 10000 });
const response = await getAllUnit(params);
if (response && response.data && response.data.data) {
const units = response.data.data;
if (units.length === 0) {
setNextUnitCode('UNT001');
return;
}
// Extract numeric part from unit codes and find the maximum
const unitNumbers = units
.map((unit) => {
const match = unit.unit_code?.match(/unt(\d+)/i);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => !isNaN(num));
const maxNumber = unitNumbers.length > 0 ? Math.max(...unitNumbers) : 0;
const nextNumber = maxNumber + 1;
// Format with leading zeros (UNT001, UNT002, etc.)
const nextCode = `UNT${String(nextNumber).padStart(3, '0')}`;
setNextUnitCode(nextCode);
} else {
setNextUnitCode('UNT001');
}
} catch (error) {
console.error('Error generating next unit code:', error);
setNextUnitCode('Auto-fill');
}
};
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
if (props.showModal) {
// Generate next unit code only for add mode
if (props.actionMode === 'add' && !props.selectedData) {
generateNextUnitCode();
}
}
if (props.selectedData != null) {
// Only set fields that are in defaultData
const filteredData = {
@@ -133,7 +177,7 @@ const DetailUnit = (props) => {
setFormData(defaultData);
}
}
}, [props.showModal]);
}, [props.showModal, props.actionMode]);
return (
<Modal
@@ -222,17 +266,21 @@ const DetailUnit = (props) => {
</div>
</div>
</div>
{/* Unit Code - Display only for edit/preview */}
{FormData.unit_code && (
<div style={{ marginBottom: 12 }}>
<Text strong>Unit Code</Text>
<Input
name="unit_code"
value={FormData.unit_code}
disabled
/>
</div>
)}
{/* Unit Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}>
<Text strong>Unit Code</Text>
<Input
name="unit_code"
value={FormData.unit_code || nextUnitCode}
placeholder={nextUnitCode}
disabled
style={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
color: FormData.unit_code ? '#000000' : '#bfbfbf'
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Name</Text>
<Text style={{ color: 'red' }}> *</Text>