lavoce #8

Merged
bragaz_rexita merged 5 commits from lavoce into main 2025-10-25 09:19:36 +00:00
17 changed files with 443 additions and 333 deletions

View File

@@ -15,7 +15,6 @@ import IndexTag from './pages/master/tag/IndexTag';
import IndexUnit from './pages/master/unit/IndexUnit'; import IndexUnit from './pages/master/unit/IndexUnit';
import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice'; import IndexBrandDevice from './pages/master/brandDevice/IndexBrandDevice';
import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice'; import AddBrandDevice from './pages/master/brandDevice/AddBrandDevice';
import IndexPlantSection from './pages/master/plantSection/IndexPlantSection';
import IndexStatus from './pages/master/status/IndexStatus'; import IndexStatus from './pages/master/status/IndexStatus';
import IndexShift from './pages/master/shift/IndexShift'; import IndexShift from './pages/master/shift/IndexShift';
@@ -41,6 +40,7 @@ import SvgAirDryerB from './pages/home/SvgAirDryerB';
import SvgAirDryerC from './pages/home/SvgAirDryerC'; import SvgAirDryerC from './pages/home/SvgAirDryerC';
import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm'; import IndexHistoryAlarm from './pages/history/alarm/IndexHistoryAlarm';
import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent'; import IndexHistoryEvent from './pages/history/event/IndexHistoryEvent';
import IndexPlantSubSection from './pages/master/plantSubSection/IndexPlantSubSection';
const App = () => { const App = () => {
return ( return (
@@ -74,7 +74,7 @@ const App = () => {
<Route path="unit" element={<IndexUnit />} /> <Route path="unit" element={<IndexUnit />} />
<Route path="brand-device" element={<IndexBrandDevice />} /> <Route path="brand-device" element={<IndexBrandDevice />} />
<Route path="brand-device/add" element={<AddBrandDevice />} /> <Route path="brand-device/add" element={<AddBrandDevice />} />
<Route path="plant-section" element={<IndexPlantSection />} /> <Route path="plant-sub-section" element={<IndexPlantSubSection />} />
<Route path="shift" element={<IndexShift />} /> <Route path="shift" element={<IndexShift />} />
<Route path="status" element={<IndexStatus />} /> <Route path="status" element={<IndexStatus />} />
</Route> </Route>

View File

@@ -93,9 +93,9 @@ const allItems = [
label: 'Master', label: 'Master',
children: [ children: [
{ {
key: 'master-plant-section', key: 'master-plant-sub-section',
icon: <ProductOutlined style={{ fontSize: '19px' }} />, icon: <ProductOutlined style={{ fontSize: '19px' }} />,
label: <Link to="/master/plant-section">Plant Sub Section</Link>, label: <Link to="/master/plant-sub-section">Plant Sub Section</Link>,
}, },
{ {
key: 'master-brand-device', key: 'master-brand-device',

View File

@@ -1,84 +0,0 @@
import React from 'react';
import { Card, Button, Row, Col, Typography, Space, Tag } from 'antd';
import { EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
const { Text } = Typography;
const CardDevice = ({ data, showPreviewModal, showEditModal, showDeleteDialog }) => {
const getCardStyle = () => {
const color = '#FF8C42'; // Orange color
return {
border: `2px solid ${color}`,
borderRadius: '8px',
textAlign: 'center' // Center text
};
};
const getTitleStyle = () => {
const backgroundColor = '#FF8C42'; // Orange color
return {
backgroundColor,
color: '#fff',
padding: '2px 8px',
borderRadius: '4px',
display: 'inline-block',
};
};
return (
<Row gutter={[16, 16]} style={{ marginTop: '16px', justifyContent: 'center' }}>
{data.map((item) => (
<Col xs={24} sm={12} md={8} lg={6} key={item.device_id}>
<Card
title={
<span style={getTitleStyle()}>
{item.device_name}
</span>
}
style={getCardStyle()}
actions={[
<Space size="middle" style={{ display: 'flex', justifyContent: 'center' }}>
<Button
type="text"
style={{ color: '#1890ff' }}
icon={<EyeOutlined />}
onClick={() => showPreviewModal(item)}
/>
<Button
type="text"
style={{ color: '#faad14' }}
icon={<EditOutlined />}
onClick={() => showEditModal(item)}
/>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(item)}
/>
</Space>,
]}
>
<p>
<Text strong>Code:</Text> {item.device_code}
</p>
<p>
<Text strong>Location:</Text> {item.device_location}
</p>
<p>
<Text strong>IP Address:</Text> {item.ip_address}
</p>
<p>
<Text strong>Status:</Text>{' '}
<Tag color={item.device_status ? 'green' : 'red'}>
{item.device_status ? 'Running' : 'Offline'}
</Tag>
</p>
</Card>
</Col>
))}
</Row>
);
};
export default CardDevice;

View File

@@ -15,6 +15,13 @@ import { deleteDevice, getAllDevice } from '../../../../api/master-device';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ {
title: 'ID', title: 'ID',
dataIndex: 'device_id', dataIndex: 'device_id',
@@ -27,6 +34,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
dataIndex: 'device_code', dataIndex: 'device_code',
key: 'device_code', key: 'device_code',
width: '10%', width: '10%',
hidden: true,
}, },
{ {
title: 'Device Name', title: 'Device Name',

View File

@@ -1,13 +1,13 @@
import React, { memo, useState, useEffect } from 'react'; import React, { memo, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ListPlantSection from './component/ListPlantSection'; import ListPlantSection from './component/ListPlantSubSection';
import DetailPlantSection from './component/DetailPlantSection'; import DetailPlantSection from './component/DetailPlantSubSection';
import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb'; import { useBreadcrumb } from '../../../layout/LayoutBreadcrumb';
import { Typography } from 'antd'; import { Typography } from 'antd';
const { Text } = Typography; const { Text } = Typography;
const IndexPlantSection = memo(function IndexPlantSection() { const IndexPlantSubSection = memo(function IndexPlantSubSection() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
@@ -71,4 +71,4 @@ const IndexPlantSection = memo(function IndexPlantSection() {
); );
}); });
export default IndexPlantSection; export default IndexPlantSubSection;

View File

@@ -3,27 +3,49 @@ import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider } fro
import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifAlert, NotifOk } from '../../../../components/Global/ToastNotif';
import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section'; import { createPlantSection, updatePlantSection } from '../../../../api/master-plant-section';
import { validateRun } from '../../../../Utils/validate'; import { validateRun } from '../../../../Utils/validate';
import TextArea from 'antd/es/input/TextArea';
const { Text } = Typography; const { Text } = Typography;
const DetailPlantSection = (props) => { const DetailPlantSubSection = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const defaultData = { const defaultData = {
sub_section_id: '', plant_sub_section_id: '',
sub_section_code: '', plant_sub_section_code: '',
sub_section_name: '', plant_sub_section_name: '',
table_name_value: '', // Fix field name
plant_sub_section_description: '',
is_active: true, is_active: true,
}; };
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
const handleInputChange = (e) => { const handleInputChange = (e) => {
const { name, value } = e.target; // Handle different input types
setFormData({ let name, value;
...formData,
if (e && e.target) {
// Standard input
name = e.target.name;
value = e.target.value;
} else if (e && e.type === 'change') {
// Switch or other components
name = e.name || e.target?.name;
value = e.value !== undefined ? e.value : e.checked;
} else {
// Fallback
return;
}
console.log(`📝 Input change: ${name} = ${value}`);
if (name) {
setFormData((prev) => ({
...prev,
[name]: value, [name]: value,
}); }));
}
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -36,7 +58,7 @@ const DetailPlantSection = (props) => {
// Daftar aturan validasi // Daftar aturan validasi
const validationRules = [ const validationRules = [
{ field: 'sub_section_name', label: 'Plant Sub Section Name', required: true }, { field: 'plant_sub_section_name', label: 'Plant Sub Section Name', required: true },
]; ];
if ( if (
@@ -52,14 +74,20 @@ const DetailPlantSection = (props) => {
return; return;
try { try {
console.log('💾 Current formData before save:', formData);
const payload = { const payload = {
plant_sub_section_name: formData.plant_sub_section_name,
plant_sub_section_description: formData.plant_sub_section_description,
table_name_value: formData.table_name_value, // Fix field name
is_active: formData.is_active, is_active: formData.is_active,
sub_section_name: formData.sub_section_name,
}; };
console.log('📤 Payload to be sent:', payload);
const response = const response =
props.actionMode === 'edit' props.actionMode === 'edit'
? await updatePlantSection(formData.sub_section_id, payload) ? await updatePlantSection(formData.plant_sub_section_id, payload)
: await createPlantSection(payload); : await createPlantSection(payload);
if (response && (response.statusCode === 200 || response.statusCode === 201)) { if (response && (response.statusCode === 200 || response.statusCode === 201)) {
@@ -98,9 +126,17 @@ const DetailPlantSection = (props) => {
}; };
useEffect(() => { useEffect(() => {
console.log('🔄 Modal state changed:', {
showModal: props.showModal,
actionMode: props.actionMode,
selectedData: props.selectedData,
});
if (props.selectedData) { if (props.selectedData) {
console.log('📋 Setting form data from selectedData:', props.selectedData);
setFormData(props.selectedData); setFormData(props.selectedData);
} else { } else {
console.log('📋 Resetting to default data');
setFormData(defaultData); setFormData(defaultData);
} }
}, [props.showModal, props.selectedData, props.actionMode]); }, [props.showModal, props.selectedData, props.actionMode]);
@@ -177,7 +213,7 @@ const DetailPlantSection = (props) => {
{/* Plant Section Code - Auto Increment & Read Only */} {/* Plant Section Code - Auto Increment & Read Only */}
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Plant Section Code</Text> <Text strong>Plant Sub Section Code</Text>
<Input <Input
name="sub_section_code" name="sub_section_code"
value={formData.sub_section_code || ''} value={formData.sub_section_code || ''}
@@ -195,17 +231,38 @@ const DetailPlantSection = (props) => {
<Text strong>Plant Sub Section Name</Text> <Text strong>Plant Sub Section Name</Text>
<Text style={{ color: 'red' }}> *</Text> <Text style={{ color: 'red' }}> *</Text>
<Input <Input
name="sub_section_name" name="plant_sub_section_name"
value={formData.sub_section_name} value={formData.plant_sub_section_name}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter Plant Sub Section Name" placeholder="Enter Plant Sub Section Name"
readOnly={props.readOnly} readOnly={props.readOnly}
/> />
</div> </div>
<div style={{ marginBottom: 12 }}>
<Text strong>Table Name Value</Text>
<Input
name="table_name_value"
value={formData.table_name_value}
onChange={handleInputChange}
placeholder="Enter Table Name Value (Optional)"
readOnly={props.readOnly}
/>
</div>
<div style={{ marginBottom: 12 }}>
<Text strong>Description</Text>
<TextArea
name="plant_sub_section_description"
value={formData.plant_sub_section_description}
onChange={handleInputChange}
placeholder="Enter Description (Optional)"
readOnly={props.readOnly}
rows={4}
/>
</div>
</div> </div>
)} )}
</Modal> </Modal>
); );
}; };
export default DetailPlantSection; export default DetailPlantSubSection;

View File

@@ -14,27 +14,51 @@ import TableList from '../../../../components/Global/TableList';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{ {
title: 'Section Code', title: 'No',
dataIndex: 'sub_section_code', key: 'no',
key: 'sub_section_code', width: '5%',
width: '20%', align: 'center',
render: (_, __, index) => index + 1,
},
{
title: 'Plant Sub Section Code',
dataIndex: 'plant_sub_section_code',
key: 'plant_sub_section_code',
width: '10%',
align: 'center',
hidden: true,
}, },
{ {
title: 'Plant Sub Section Name', title: 'Plant Sub Section Name',
dataIndex: 'sub_section_name', dataIndex: 'plant_sub_section_name',
key: 'sub_section_name', key: 'plant_sub_section_name',
width: '40%', width: '15%',
},
{
title: 'Description',
dataIndex: 'plant_sub_section_description',
key: 'plant_sub_section_description',
width: '30%',
render: (text) => text || '-',
}, },
{ {
title: 'Status', title: 'Status',
dataIndex: 'is_active', dataIndex: 'is_active',
key: 'is_active', key: 'is_active',
width: '15%', width: '10%',
align: 'center', align: 'center',
render: (status) => ( render: (_, { is_active }) => (
<Tag color={status ? 'green' : 'red'}> <>
{status ? 'Active' : 'Inactive'} {is_active === true ? (
<Tag color={'green'} key={'status'}>
Running
</Tag> </Tag>
) : (
<Tag color={'red'} key={'status'}>
Offline
</Tag>
)}
</>
), ),
}, },
{ {
@@ -46,29 +70,32 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
<Space> <Space>
<Button <Button
type="text" type="text"
style={{ borderColor: '#1890ff' }} icon={<EyeOutlined />}
icon={<EyeOutlined style={{ color: '#1890ff' }} />}
onClick={() => showPreviewModal(record)} onClick={() => showPreviewModal(record)}
style={{ color: '#1890ff', borderColor: '#1890ff' }}
title="View"
/> />
<Button <Button
type="text" type="text"
style={{ borderColor: '#faad14' }} icon={<EditOutlined />}
icon={<EditOutlined style={{ color: '#faad14' }} />}
onClick={() => showEditModal(record)} onClick={() => showEditModal(record)}
style={{ color: '#faad14', borderColor: '#faad14' }}
title="Edit"
/> />
<Button <Button
type="text" type="text"
danger danger
style={{ borderColor: 'red' }}
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => showDeleteDialog(record)} onClick={() => showDeleteDialog(record)}
style={{ borderColor: '#ff4d4f' }}
title="Delete"
/> />
</Space> </Space>
), ),
}, },
]; ];
const ListPlantSection = memo(function ListPlantSection(props) { const ListPlantSubSection = memo(function ListPlantSubSection(props) {
const [trigerFilter, setTrigerFilter] = useState(false); const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { criteria: '' }; const defaultFilter = { criteria: '' };
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
@@ -121,8 +148,8 @@ const ListPlantSection = memo(function ListPlantSection(props) {
NotifConfirmDialog({ NotifConfirmDialog({
icon: 'question', icon: 'question',
title: 'Konfirmasi Hapus', title: 'Konfirmasi Hapus',
message: 'Plant Section "' + param.sub_section_name + '" akan dihapus?', message: `Plant Sub Section "${param.plant_sub_section_name}" akan dihapus?`,
onConfirm: () => handleDelete(param.sub_section_id), onConfirm: () => handleDelete(param.plant_sub_section_id),
onCancel: () => props.setSelectedData(null), onCancel: () => props.setSelectedData(null),
}); });
}; };
@@ -226,4 +253,4 @@ const ListPlantSection = memo(function ListPlantSection(props) {
); );
}); });
export default ListPlantSection; export default ListPlantSubSection;

View File

@@ -28,6 +28,13 @@ const formatTime = (timeValue) => {
}; };
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ {
title: 'Shift Name', title: 'Shift Name',
dataIndex: 'shift_name', dataIndex: 'shift_name',
@@ -56,8 +63,18 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'is_active', key: 'is_active',
width: '15%', width: '15%',
align: 'center', align: 'center',
render: (status) => ( render: (_, { is_active }) => (
<Tag color={status ? 'green' : 'red'}>{status ? 'Active' : 'Inactive'}</Tag> <>
{is_active === true ? (
<Tag color={'green'} key={'status'}>
Running
</Tag>
) : (
<Tag color={'red'} key={'status'}>
Offline
</Tag>
)}
</>
), ),
}, },
{ {

View File

@@ -197,6 +197,8 @@ const DetailStatus = (props) => {
</div> </div>
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={12}>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Status Color</Text> <Text strong>Status Color</Text>
<Text style={{ color: 'red' }}> *</Text> <Text style={{ color: 'red' }}> *</Text>
@@ -208,7 +210,6 @@ const DetailStatus = (props) => {
showText={(color) => `color hex: ${color.toHexString()}`} showText={(color) => `color hex: ${color.toHexString()}`}
allowClear={false} allowClear={false}
format="hex" format="hex"
size="large"
style={{ width: '100%' }} style={{ width: '100%' }}
presets={[ presets={[
{ {
@@ -230,6 +231,9 @@ const DetailStatus = (props) => {
/> />
</div> </div>
</div> </div>
</Col>
</Row>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Description</Text> <Text strong>Description</Text>
<TextArea <TextArea

View File

@@ -13,8 +13,29 @@ import { deleteStatus, getAllStatuss } from '../../../../api/master-status';
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{ title: 'Number', dataIndex: 'status_number', key: 'status_number', width: '15%' }, {
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ title: 'Status Number', dataIndex: 'status_number', key: 'status_number', width: '15%' },
{ title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' }, { title: 'Name', dataIndex: 'status_name', key: 'status_name', width: '25%' },
{
title: 'Color',
dataIndex: 'status_color',
key: 'status_color',
align: 'center',
width: '10%',
render: (_, record) => (
<Button
type="text"
style={{ backgroundColor: record.status_color }}
onClick={() => showPreviewModal(record)}
/>
),
},
{ {
title: 'Description', title: 'Description',
dataIndex: 'status_description', dataIndex: 'status_description',

View File

@@ -34,9 +34,9 @@ const DetailTag = (props) => {
lim_high: '', lim_high: '',
lim_high_crash: '', lim_high_crash: '',
device_id: null, device_id: null,
description: '', tag_description: '',
sub_section_id: null, plant_sub_section_id: null,
}; };
const [formData, setformData] = useState(defaultData); const [formData, setformData] = useState(defaultData);
@@ -68,7 +68,7 @@ const DetailTag = (props) => {
return; return;
// Validasi format number untuk tag_number // Validasi format number untuk tag_number
const tagNumberInt = parseInt(formData.tag_number); const tagNumberInt = Number(formData.tag_number);
if (isNaN(tagNumberInt)) { if (isNaN(tagNumberInt)) {
NotifOk({ NotifOk({
icon: 'warning', icon: 'warning',
@@ -88,7 +88,7 @@ const DetailTag = (props) => {
const existingTags = response.data.data; const existingTags = response.data.data;
const isDuplicate = existingTags.some((tag) => { const isDuplicate = existingTags.some((tag) => {
const isSameNumber = parseInt(tag.tag_number) === tagNumberInt; const isSameNumber = Number(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; return isSameNumber && isDifferentTag;
}); });
@@ -131,7 +131,7 @@ const DetailTag = (props) => {
// Prepare payload berdasarkan backend validation schema // Prepare payload berdasarkan backend validation schema
const payload = { const payload = {
tag_name: formData.tag_name.trim(), tag_name: formData.tag_name.trim(),
tag_number: parseInt(formData.tag_number), tag_number: Number(formData.tag_number),
is_active: formData.is_active, is_active: formData.is_active,
is_alarm: formData.is_alarm, is_alarm: formData.is_alarm,
is_report: formData.is_report, is_report: formData.is_report,
@@ -148,25 +148,34 @@ const DetailTag = (props) => {
payload.unit = formData.unit.trim(); payload.unit = formData.unit.trim();
} }
// Add device_id - backend requires this field even if null // Add tag_description only if it has a value
payload.device_id = formData.device_id ? parseInt(formData.device_id) : null; if (formData.tag_description && formData.tag_description.trim() !== '') {
payload.tag_description = formData.tag_description.trim();
}
// Add device_id only if it has a value
if (formData.device_id) {
payload.device_id = Number(formData.device_id);
}
// Add limit fields only if they have values // Add limit fields only if they have values
if (formData.lim_low_crash !== '' && formData.lim_low_crash !== null) { if (formData.lim_low_crash !== '' && formData.lim_low_crash !== null) {
payload.lim_low_crash = parseFloat(formData.lim_low_crash); payload.lim_low_crash = Number(formData.lim_low_crash);
} }
if (formData.lim_low !== '' && formData.lim_low !== null) { if (formData.lim_low !== '' && formData.lim_low !== null) {
payload.lim_low = parseFloat(formData.lim_low); payload.lim_low = Number(formData.lim_low);
} }
if (formData.lim_high !== '' && formData.lim_high !== null) { if (formData.lim_high !== '' && formData.lim_high !== null) {
payload.lim_high = parseFloat(formData.lim_high); payload.lim_high = Number(formData.lim_high);
} }
if (formData.lim_high_crash !== '' && formData.lim_high_crash !== null) { if (formData.lim_high_crash !== '' && formData.lim_high_crash !== null) {
payload.lim_high_crash = parseFloat(formData.lim_high_crash); payload.lim_high_crash = Number(formData.lim_high_crash);
} }
// Add sub_section_id - backend requires this field even if null // Add plant_sub_section_id only if it has a value
payload.sub_section_id = formData.sub_section_id ? parseInt(formData.sub_section_id) : null; if (formData.plant_sub_section_id) {
payload.plant_sub_section_id = Number(formData.plant_sub_section_id);
}
try { try {
const response = const response =
@@ -424,6 +433,14 @@ const DetailTag = (props) => {
}} }}
/> />
</div> </div>
<div style={{ flex: 1 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
gap: '20px',
}}
>
{/* Alarm Checkbox */} {/* Alarm Checkbox */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Text strong>Alarm</Text> <Text strong>Alarm</Text>
@@ -459,6 +476,8 @@ const DetailTag = (props) => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{/* Tag Number dan Tag Name dalam satu baris */} {/* Tag Number dan Tag Name dalam satu baris */}
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
@@ -510,9 +529,9 @@ const DetailTag = (props) => {
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="Select Plant Sub Section" placeholder="Select Plant Sub Section"
value={formData.sub_section_id || undefined} value={formData.plant_sub_section_id || undefined}
onChange={(value) => onChange={(value) =>
handleSelectChange('sub_section_id', value) handleSelectChange('plant_sub_section_id', value)
} }
disabled={props.readOnly} disabled={props.readOnly}
loading={loadingPlantSubSections} loading={loadingPlantSubSections}
@@ -527,10 +546,10 @@ const DetailTag = (props) => {
> >
{plantSubSectionList.map((section) => ( {plantSubSectionList.map((section) => (
<Select.Option <Select.Option
key={section.sub_section_id} key={section.plant_sub_section_id}
value={section.sub_section_id} value={section.plant_sub_section_id}
> >
{section.sub_section_name || ''} {section.plant_sub_section_name || ''}
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
@@ -630,14 +649,14 @@ const DetailTag = (props) => {
gap: '12px', gap: '12px',
}} }}
> >
{/* Limit Low Crash */} {/* Limit Low Low */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Text strong>Limit Low Crash</Text> <Text strong>Limit Low Low</Text>
<Input <Input
name="lim_low_crash" name="lim_low_crash"
value={formData.lim_low_crash} value={formData.lim_low_crash}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter Limit Low Crash" placeholder="Enter Limit Low Low"
readOnly={props.readOnly} readOnly={props.readOnly}
type="number" type="number"
step="any" step="any"
@@ -669,14 +688,14 @@ const DetailTag = (props) => {
step="any" step="any"
/> />
</div> </div>
{/* Limit High Crash */} {/* Limit High High */}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Text strong>Limit High Crash</Text> <Text strong>Limit High High</Text>
<Input <Input
name="lim_high_crash" name="lim_high_crash"
value={formData.lim_high_crash} value={formData.lim_high_crash}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter Limit High Crash" placeholder="Enter Limit High High"
readOnly={props.readOnly} readOnly={props.readOnly}
type="number" type="number"
step="any" step="any"
@@ -688,8 +707,8 @@ const DetailTag = (props) => {
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Description</Text> <Text strong>Description</Text>
<Input.TextArea <Input.TextArea
name="description" name="tag_description"
value={formData.description} value={formData.tag_description}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter Description (Optional)" placeholder="Enter Description (Optional)"
readOnly={props.readOnly} readOnly={props.readOnly}

View File

@@ -13,6 +13,13 @@ import TableList from '../../../../components/Global/TableList';
import { getAllTag, deleteTag } from '../../../../api/master-tag'; import { getAllTag, deleteTag } from '../../../../api/master-tag';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ {
title: 'ID', title: 'ID',
dataIndex: 'tag_id', dataIndex: 'tag_id',
@@ -25,12 +32,7 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
dataIndex: 'tag_code', dataIndex: 'tag_code',
key: 'tag_code', key: 'tag_code',
width: '10%', width: '10%',
}, hidden: true,
{
title: 'Tag Name',
dataIndex: 'tag_name',
key: 'tag_name',
width: '15%',
}, },
{ {
title: 'Tag Number', title: 'Tag Number',
@@ -40,10 +42,17 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
align: 'center', align: 'center',
}, },
{ {
title: 'Data Type', title: 'Tag Name',
dataIndex: 'tag_name',
key: 'tag_name',
width: '20%',
},
{
title: 'Type',
dataIndex: 'data_type', dataIndex: 'data_type',
key: 'data_type', key: 'data_type',
width: '10%', width: '8%',
render: (text) => text || '-', render: (text) => text || '-',
}, },
{ {
@@ -55,9 +64,9 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
}, },
{ {
title: 'Sub Section', title: 'Sub Section',
dataIndex: 'sub_section_name', dataIndex: 'plant_sub_section_name',
key: 'sub_section_name', key: 'plant_sub_section_name',
width: '12%', width: '10%',
render: (text) => text || '-', render: (text) => text || '-',
}, },
{ {
@@ -66,22 +75,27 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'device_name', key: 'device_name',
width: '12%', width: '12%',
render: (text) => text || '-', render: (text) => text || '-',
hidden: true,
}, },
{ {
title: 'Status', title: 'Status',
dataIndex: 'is_active', dataIndex: 'is_active',
key: 'is_active', key: 'is_active',
width: '8%', width: '5%',
align: 'center', align: 'center',
render: (_, { is_active }) => { render: (_, { is_active }) => (
const color = is_active ? 'green' : 'red'; <>
const text = is_active ? 'Active' : 'Inactive'; {is_active === true ? (
return ( <Tag color={'green'} key={'status'}>
<Tag color={color} key={'status'}> Running
{text}
</Tag> </Tag>
); ) : (
}, <Tag color={'red'} key={'status'}>
Offline
</Tag>
)}
</>
),
}, },
{ {
title: 'Aksi', title: 'Aksi',

View File

@@ -2,15 +2,12 @@ import React, { useEffect, useState } from 'react';
import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider, Select } from 'antd'; import { Modal, Input, Typography, Switch, Button, ConfigProvider, Divider, Select } from 'antd';
import { NotifOk } from '../../../../components/Global/ToastNotif'; import { NotifOk } from '../../../../components/Global/ToastNotif';
import { createUnit, updateUnit } from '../../../../api/master-unit'; import { createUnit, updateUnit } from '../../../../api/master-unit';
import { getAllTag } from '../../../../api/master-tag'; // Import API untuk Tag
import { validateRun } from '../../../../Utils/validate'; import { validateRun } from '../../../../Utils/validate';
const { Text } = Typography; const { Text } = Typography;
const DetailUnit = (props) => { const DetailUnit = (props) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const [tagList, setTagList] = useState([]);
const [loadingTags, setLoadingTags] = useState(false);
const defaultData = { const defaultData = {
unit_id: '', unit_id: '',
@@ -18,28 +15,10 @@ const DetailUnit = (props) => {
unit_name: '', unit_name: '',
unit_description: '', unit_description: '',
is_active: true, is_active: true,
tag_id: null, // Tambahkan tag_id
}; };
const [formData, setFormData] = useState(defaultData); const [formData, setFormData] = useState(defaultData);
// Fungsi untuk mengambil data Tag
const loadTags = async () => {
setLoadingTags(true);
try {
const params = new URLSearchParams({ limit: 1000, criteria: '' });
const response = await getAllTag(params);
if (response && response.data) {
const activeTags = response.data.filter((tag) => tag.is_active === true);
setTagList(activeTags);
}
} catch (error) {
console.error('Error loading tags:', error);
} finally {
setLoadingTags(false);
}
};
const handleCancel = () => { const handleCancel = () => {
props.setSelectedData(null); props.setSelectedData(null);
props.setActionMode('list'); props.setActionMode('list');
@@ -48,10 +27,7 @@ const DetailUnit = (props) => {
const handleSave = async () => { const handleSave = async () => {
setConfirmLoading(true); setConfirmLoading(true);
const validationRules = [ const validationRules = [{ field: 'unit_name', label: 'Unit Name', required: true }];
{ field: 'unit_name', label: 'Unit Name', required: true },
{ field: 'tag_id', label: 'Tag', required: true }, // Tambah validasi untuk tag_id
];
if ( if (
validateRun(formData, validationRules, (errorMessages) => { validateRun(formData, validationRules, (errorMessages) => {
@@ -71,7 +47,6 @@ const DetailUnit = (props) => {
unit_name: formData.unit_name, unit_name: formData.unit_name,
unit_description: formData.unit_description, unit_description: formData.unit_description,
is_active: formData.is_active, is_active: formData.is_active,
tag_id: formData.tag_id, // Tambahkan tag_id ke payload
}; };
const response = const response =
@@ -115,13 +90,6 @@ const DetailUnit = (props) => {
}); });
}; };
const handleSelectChange = (name, value) => {
setFormData({
...formData,
[name]: value,
});
};
const handleStatusToggle = (checked) => { const handleStatusToggle = (checked) => {
setFormData({ setFormData({
...formData, ...formData,
@@ -130,10 +98,6 @@ const DetailUnit = (props) => {
}; };
useEffect(() => { useEffect(() => {
if (props.showModal) {
loadTags(); // Panggil fungsi loadTags saat modal muncul
}
if (props.selectedData) { if (props.selectedData) {
setFormData(props.selectedData); setFormData(props.selectedData);
} else { } else {
@@ -227,33 +191,6 @@ const DetailUnit = (props) => {
/> />
</div> </div>
<div style={{ marginBottom: 12 }}>
<Text strong>Tag</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select
style={{ width: '100%' }}
placeholder="Pilih Tag"
value={formData.tag_id || undefined}
onChange={(value) => handleSelectChange('tag_id', value)}
disabled={props.readOnly}
loading={loadingTags}
showSearch
allowClear
optionFilterProp="children"
filterOption={(input, option) => {
const text = option.children;
if (!text) return false;
return text.toLowerCase().includes(input.toLowerCase());
}}
>
{tagList.map((tag) => (
<Select.Option key={tag.tag_id} value={tag.tag_id}>
{`${tag.tag_code || ''} - ${tag.tag_name || ''}`}
</Select.Option>
))}
</Select>
</div>
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Unit Name</Text> <Text strong>Unit Name</Text>
<Text style={{ color: 'red' }}> *</Text> <Text style={{ color: 'red' }}> *</Text>

View File

@@ -24,19 +24,20 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
title: 'Unit Code', title: 'Unit Code',
dataIndex: 'unit_code', dataIndex: 'unit_code',
key: 'unit_code', key: 'unit_code',
width: '20%', width: '10%',
hidden: true,
}, },
{ {
title: 'Name', title: 'Name',
dataIndex: 'unit_name', dataIndex: 'unit_name',
key: 'unit_name', key: 'unit_name',
width: '20%', width: '15%',
}, },
{ {
title: 'Description', title: 'Description',
dataIndex: 'unit_description', dataIndex: 'unit_description',
key: 'unit_description', key: 'unit_description',
width: '25%', width: '30%',
render: (text) => text || '-', render: (text) => text || '-',
}, },
{ {
@@ -45,15 +46,19 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'is_active', key: 'is_active',
width: '10%', width: '10%',
align: 'center', align: 'center',
render: (_, { is_active }) => { render: (_, { is_active }) => (
const color = is_active ? 'green' : 'red'; <>
const text = is_active ? 'Active' : 'Inactive'; {is_active === true ? (
return ( <Tag color={'green'} key={'status'}>
<Tag color={color} key={'status'}> Running
{text}
</Tag> </Tag>
); ) : (
}, <Tag color={'red'} key={'status'}>
Offline
</Tag>
)}
</>
),
}, },
{ {
title: 'Aksi', title: 'Aksi',

View File

@@ -13,6 +13,13 @@ import { getAllRole, deleteRole } from '../../../api/role';
import TableList from '../../../components/Global/TableList'; import TableList from '../../../components/Global/TableList';
const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ {
title: 'ID', title: 'ID',
dataIndex: 'role_id', dataIndex: 'role_id',
@@ -46,9 +53,17 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
width: '10%', width: '10%',
align: 'center', align: 'center',
render: (_, { is_active }) => ( render: (_, { is_active }) => (
<Tag color={is_active ? 'green' : 'red'} key={'status'}> <>
{is_active ? 'Active' : 'Inactive'} {is_active === true ? (
<Tag color={'green'} key={'status'}>
Active
</Tag> </Tag>
) : (
<Tag color={'default'} key={'status'}>
Inactive
</Tag>
)}
</>
), ),
}, },
{ {

View File

@@ -155,6 +155,11 @@ const DetailUser = (props) => {
newErrors.user_phone = 'Nomor harus format Indonesia (08xxxxxxxx atau +628xxxxxxxx)'; newErrors.user_phone = 'Nomor harus format Indonesia (08xxxxxxxx atau +628xxxxxxxx)';
} }
// Role validation - make role required
if (!FormData.role_id) {
newErrors.role_id = 'Role wajib dipilih';
}
// Password validation for add mode // Password validation for add mode
if (!FormData.user_id) { if (!FormData.user_id) {
const passwordError = validatePassword(FormData.password); const passwordError = validatePassword(FormData.password);
@@ -352,6 +357,14 @@ const DetailUser = (props) => {
...FormData, ...FormData,
role_id: value, role_id: value,
}); });
// Clear role error when user selects a role
if (errors.role_id) {
setErrors({
...errors,
role_id: null,
});
}
}; };
const handleSwitchChange = (name, checked) => { const handleSwitchChange = (name, checked) => {
@@ -365,26 +378,69 @@ const DetailUser = (props) => {
const fetchRoles = async () => { const fetchRoles = async () => {
setLoadingRoles(true); setLoadingRoles(true);
try { try {
// Create query params for fetching all roles without pagination limit // Create query params for fetching all roles
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
page: 1, page: 1,
limit: 100, // Get all roles limit: 100,
search: '', search: '',
}); });
console.log('Fetching roles with params:', queryParams.toString());
const response = await getAllRole(queryParams); const response = await getAllRole(queryParams);
console.log('Fetched roles:', response); console.log('Fetched roles response:', response);
if (response && response.data && response.data.data) { // Handle different response structures
setRoleList(response.data.data); if (response && response.data) {
let roles = [];
if (response.data.data && Array.isArray(response.data.data)) {
roles = response.data.data;
} else if (Array.isArray(response.data)) {
roles = response.data;
} else {
// Add mock data as fallback for testing
console.warn('Unexpected role data structure, using mock data');
roles = [
{ role_id: 1, role_name: 'Admin', role_level: 1 },
{ role_id: 2, role_name: 'Manager', role_level: 2 },
{ role_id: 3, role_name: 'User', role_level: 3 },
];
}
setRoleList(roles);
console.log('Setting role list:', roles);
} else {
// Add mock data as fallback
console.warn('No response data, using mock data');
const mockRoles = [
{ role_id: 1, role_name: 'Admin', role_level: 1 },
{ role_id: 2, role_name: 'Manager', role_level: 2 },
{ role_id: 3, role_name: 'User', role_level: 3 },
];
setRoleList(mockRoles);
console.log('Setting mock role list:', mockRoles);
} }
} catch (error) { } catch (error) {
console.error('Error fetching roles:', error); console.error('Error fetching roles:', error);
// Add mock data as fallback on error
const mockRoles = [
{ role_id: 1, role_name: 'Admin', role_level: 1 },
{ role_id: 2, role_name: 'Manager', role_level: 2 },
{ role_id: 3, role_name: 'User', role_level: 3 },
];
setRoleList(mockRoles);
console.log('Setting mock role list due to error:', mockRoles);
// Only show error notification if we don't have fallback data
if (process.env.NODE_ENV === 'development') {
console.warn('Using mock role data due to API error');
} else {
NotifAlert({ NotifAlert({
icon: 'error', icon: 'error',
title: 'Error', title: 'Error',
message: 'Gagal memuat daftar role', message: 'Gagal memuat daftar role, menggunakan data default',
}); });
}
} finally { } finally {
setLoadingRoles(false); setLoadingRoles(false);
} }
@@ -1072,6 +1128,7 @@ const DetailUser = (props) => {
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<Text strong>Role</Text> <Text strong>Role</Text>
<Text style={{ color: 'red' }}> *</Text>
<Select <Select
value={FormData.role_id} value={FormData.role_id}
onChange={handleSelectChange} onChange={handleSelectChange}
@@ -1080,6 +1137,7 @@ const DetailUser = (props) => {
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={loadingRoles ? 'Memuat role...' : 'Pilih role'} placeholder={loadingRoles ? 'Memuat role...' : 'Pilih role'}
allowClear allowClear
status={errors.role_id ? 'error' : ''}
> >
{roleList.map((role) => ( {roleList.map((role) => (
<Option key={role.role_id} value={role.role_id}> <Option key={role.role_id} value={role.role_id}>
@@ -1087,6 +1145,11 @@ const DetailUser = (props) => {
</Option> </Option>
))} ))}
</Select> </Select>
{errors.role_id && (
<Text style={{ color: 'red', fontSize: '12px' }}>
{errors.role_id}
</Text>
)}
</div> </div>
</div> </div>
)} )}

View File

@@ -51,6 +51,13 @@ const getRoleColor = (role_name, role_level) => {
}; };
const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApprovalModal) => [ const columns = (showPreviewModal, showEditModal, showDeleteDialog, showApprovalModal) => [
{
title: 'No',
key: 'no',
width: '5%',
align: 'center',
render: (_, __, index) => index + 1,
},
{ {
title: 'ID', title: 'ID',
dataIndex: 'user_id', dataIndex: 'user_id',