feat: update data structure for tag history and adjust related components for consistency

This commit is contained in:
2025-10-17 14:57:15 +07:00
parent 15b3339dcb
commit e42d1fa3ce
5 changed files with 208 additions and 232 deletions

View File

@@ -8,54 +8,33 @@ import dayjs from 'dayjs';
const { Text } = Typography; const { Text } = Typography;
// Line Chart Data (same as IndexTrending) converted to table format // New data structure for tag history
const lineChartData = [ const tagHistoryData = [
{ {
id: 'Compressor Section 1', tag: 'TEMP_SENSOR_1',
color: '#FF6B4A', color: '#FF6B4A',
data: [ history: [
{ x: 'Jan', y: 15 }, { timestamp: '2025-10-09 08:00', value: 75 },
{ x: 'Feb', y: 18 }, { timestamp: '2025-10-09 08:05', value: 76 },
{ x: 'Mar', y: 17 }, { timestamp: '2025-10-09 08:10', value: 75 },
{ x: 'Apr', y: 24 },
{ x: 'Mei', y: 21 },
{ x: 'Jun', y: 19 },
{ x: 'Jul', y: 28 },
{ x: 'Agu', y: 26 },
{ x: 'Sep', y: 22 },
{ x: 'Okt', y: 25 },
], ],
}, },
{ {
id: 'Compressor Section 2', tag: 'GAS_LEAK_SENSOR_1',
color: '#4ECDC4', color: '#4ECDC4',
data: [ history: [
{ x: 'Jan', y: 12 }, { timestamp: '2025-10-09 08:00', value: 10 },
{ x: 'Feb', y: 14 }, { timestamp: '2025-10-09 08:05', value: 150 },
{ x: 'Mar', y: 19 }, { timestamp: '2025-10-09 08:10', value: 12 },
{ x: 'Apr', y: 21 },
{ x: 'Mei', y: 22 },
{ x: 'Jun', y: 20 },
{ x: 'Jul', y: 19 },
{ x: 'Agu', y: 21 },
{ x: 'Sep', y: 18 },
{ x: 'Okt', y: 20 },
], ],
}, },
{ {
id: 'Compressor Section 3', tag: 'PRESSURE_SENSOR_1',
color: '#FFE66D', color: '#FFE66D',
data: [ history: [
{ x: 'Jan', y: 8 }, { timestamp: '2025-10-09 08:00', value: 1.2 },
{ x: 'Feb', y: 10 }, { timestamp: '2025-10-09 08:05', value: 1.3 },
{ x: 'Mar', y: 12 }, { timestamp: '2025-10-09 08:10', value: 1.2 },
{ x: 'Apr', y: 15 },
{ x: 'Mei', y: 18 },
{ x: 'Jun', y: 20 },
{ x: 'Jul', y: 22 },
{ x: 'Agu', y: 21 },
{ x: 'Sep', y: 19 },
{ x: 'Okt', y: 26 },
], ],
}, },
]; ];
@@ -67,7 +46,7 @@ const IndexReport = memo(function IndexReport() {
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30')); const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09')); const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('Bulanan'); const [periode, setPeriode] = useState('30 Menit');
const [userRole, setUserRole] = useState(null); const [userRole, setUserRole] = useState(null);
const [roleLevel, setRoleLevel] = useState(null); const [roleLevel, setRoleLevel] = useState(null);
@@ -120,63 +99,53 @@ const IndexReport = memo(function IndexReport() {
setPlantSubSection('Semua Plant'); setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30')); setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09')); setEndDate(dayjs('2025-10-09'));
setPeriode('Bulanan'); setPeriode('30 Menit');
}; };
// Check if user has permission to view data (all except guest) // Check if user has permission to view data (all except guest)
const canViewData = userRole && userRole !== 'guest'; const canViewData = userRole && userRole !== 'guest';
// Convert chart data to table format // Convert tag history data to table format
const convertToTableData = () => { const convertToTableData = () => {
return lineChartData.map((section, index) => { const timestamps = {}; // Use an object to collect data per timestamp
const rowData = {
key: index,
section: section.id,
color: section.color,
};
// Add each month's data as a column tagHistoryData.forEach((tagData) => {
section.data.forEach((point) => { tagData.history.forEach((point) => {
rowData[point.x] = point.y; if (!timestamps[point.timestamp]) {
timestamps[point.timestamp] = {
key: point.timestamp,
'Date and Time': point.timestamp,
};
}
timestamps[point.timestamp][tagData.tag] = point.value;
}); });
return rowData;
}); });
// Convert the object to an array
return Object.values(timestamps);
}; };
const tableData = convertToTableData(); const tableData = convertToTableData();
// Create dynamic columns based on months // Create dynamic columns based on tags
const months = lineChartData[0]?.data.map((point) => point.x) || []; const tags = tagHistoryData.map((tagData) => tagData.tag);
const columns = [ const columns = [
{ {
title: 'Plant Sub Section', title: 'Date and Time',
dataIndex: 'section', dataIndex: 'Date and Time',
key: 'section', key: 'Date and Time',
fixed: 'left', fixed: 'left',
width: 200, width: 180,
render: (text, record) => ( render: (text) => <Text strong>{text}</Text>,
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: record.color,
}}
/>
<Text strong>{text}</Text>
</div>
),
}, },
...months.map((month) => ({ ...tags.map((tag) => ({
title: month, title: tag,
dataIndex: month, dataIndex: tag,
key: month, key: tag,
align: 'center', align: 'center',
width: 80, width: 150,
render: (value) => <Text>{value}</Text>, render: (value) => <Text>{value !== undefined ? value : '-'}</Text>,
})), })),
]; ];
@@ -242,9 +211,10 @@ const IndexReport = memo(function IndexReport() {
onChange={setPeriode} onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ options={[
{ value: 'Harian', label: 'Harian' }, { value: '5 Menit', label: '5 Menit' },
{ value: 'Mingguan', label: 'Mingguan' }, { value: '10 Menit', label: '10 Menit' },
{ value: 'Bulanan', label: 'Bulanan' }, { value: '30 Menit', label: '30 Menit' },
{ value: '1 Jam', label: '1 Jam' },
]} ]}
/> />
</div> </div>
@@ -272,9 +242,8 @@ const IndexReport = memo(function IndexReport() {
</Col> </Col>
</Row> </Row>
</Card> </Card>
{/* Table Section */} {/* Table Section */}
{!canViewData ? ( {/* {!canViewData ? (
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}> <Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
<Text style={{ fontSize: '16px', color: '#999' }}> <Text style={{ fontSize: '16px', color: '#999' }}>
Anda tidak memiliki akses untuk melihat data report. Anda tidak memiliki akses untuk melihat data report.
@@ -282,22 +251,22 @@ const IndexReport = memo(function IndexReport() {
Silakan hubungi administrator untuk mendapatkan akses. Silakan hubungi administrator untuk mendapatkan akses.
</Text> </Text>
</Card> </Card>
) : ( ) : ( */}
<Card style={{ marginTop: '24px' }}> <Card style={{ marginTop: '24px' }}>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '16px' }}> <Text strong style={{ fontSize: '16px' }}>
Trending Error per Plant Sub Section History Report
</Text> </Text>
</div> </div>
<Table <Table
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
pagination={false} pagination={false}
scroll={{ x: 1000 }} scroll={{ x: 1000 }}
size="middle" size="middle"
/> />
</Card> </Card>
)} {/* )} */}
</div> </div>
</React.Fragment> </React.Fragment>
); );

View File

@@ -17,7 +17,7 @@ const IndexTrending = memo(function IndexTrending() {
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const [plantSubSection, setPlantSubSection] = useState('Semua Plant');
const [startDate, setStartDate] = useState(dayjs('2025-09-30')); const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09')); const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('Bulanan'); const [periode, setPeriode] = useState('10 Menit');
const [userRole, setUserRole] = useState(null); const [userRole, setUserRole] = useState(null);
const [roleLevel, setRoleLevel] = useState(null); const [roleLevel, setRoleLevel] = useState(null);
@@ -66,79 +66,53 @@ const IndexTrending = memo(function IndexTrending() {
} }
}, []); }, []);
const lineChartData = [ const tagTrendingData = [
{ {
id: 'Compressor Section 1', id: 'TEMP_SENSOR_1',
color: '#FF6B4A', color: '#FF6B4A',
data: [ data: [
{ x: 'Jan', y: 15 }, { y: '08:00', x: 75 },
{ x: 'Feb', y: 18 }, { y: '08:05', x: 76 },
{ x: 'Mar', y: 17 }, { y: '08:10', x: 75 },
{ x: 'Apr', y: 24 }, { y: '08:15', x: 77 },
{ x: 'Mei', y: 21 }, { y: '08:20', x: 76 },
{ x: 'Jun', y: 19 }, { y: '08:25', x: 78 },
{ x: 'Jul', y: 28 }, { y: '08:30', x: 79 },
{ x: 'Agu', y: 26 },
{ x: 'Sep', y: 22 },
{ x: 'Okt', y: 25 },
], ],
}, },
{ {
id: 'Compressor Section 2', id: 'GAS_LEAK_SENSOR_1',
color: '#4ECDC4', color: '#4ECDC4',
data: [ data: [
{ x: 'Jan', y: 12 }, { y: '08:00', x: 10 },
{ x: 'Feb', y: 14 }, { y: '08:05', x: 150 },
{ x: 'Mar', y: 19 }, { y: '08:10', x: 40 },
{ x: 'Apr', y: 21 }, { y: '08:15', x: 20 },
{ x: 'Mei', y: 22 }, { y: '08:20', x: 15 },
{ x: 'Jun', y: 20 }, { y: '08:25', x: 18 },
{ x: 'Jul', y: 19 }, { y: '08:30', x: 25 },
{ x: 'Agu', y: 21 },
{ x: 'Sep', y: 18 },
{ x: 'Okt', y: 20 },
], ],
}, },
{ {
id: 'Compressor Section 3', id: 'PRESSURE_SENSOR_1',
color: '#FFE66D', color: '#FFE66D',
data: [ data: [
{ x: 'Jan', y: 8 }, { y: '08:00', x: 1.2 },
{ x: 'Feb', y: 10 }, { y: '08:05', x: 1.3 },
{ x: 'Mar', y: 12 }, { y: '08:10', x: 1.2 },
{ x: 'Apr', y: 15 }, { y: '08:15', x: 1.4 },
{ x: 'Mei', y: 18 }, { y: '08:20', x: 1.5 },
{ x: 'Jun', y: 20 }, { y: '08:25', x: 1.3 },
{ x: 'Jul', y: 22 }, { y: '08:30', x: 1.2 },
{ x: 'Agu', y: 21 },
{ x: 'Sep', y: 19 },
{ x: 'Okt', y: 26 },
], ],
}, },
]; ];
const donutChartData = [
{ id: 'Section 1', label: 'Section 1', value: 35, color: '#FF6B4A' },
{ id: 'Section 2', label: 'Section 2', value: 25, color: '#4ECDC4' },
{ id: 'Section 3', label: 'Section 3', value: 15, color: '#FFE66D' },
{ id: 'Section 4', label: 'Section 4', value: 12, color: '#95E1D3' },
{ id: 'Section 5', label: 'Section 5', value: 8, color: '#F38BA0' },
{ id: 'Section 6', label: 'Section 6', value: 5, color: '#A78BFA' },
];
const barChartData = [
{ label: 'Overheat', value: 65 },
{ label: 'Low Pressure', value: 48 },
{ label: 'High Vibration', value: 42 },
{ label: 'Oil Leak', value: 35 },
{ label: 'Sensor Error', value: 28 },
];
const handleReset = () => { const handleReset = () => {
setPlantSubSection('Semua Plant'); setPlantSubSection('Semua Plant');
setStartDate(dayjs('2025-09-30')); setStartDate(dayjs('2025-09-30'));
setEndDate(dayjs('2025-10-09')); setEndDate(dayjs('2025-10-09'));
setPeriode('Bulanan'); setPeriode('10 Menit');
}; };
// Check if user has permission to view data (all except guest) // Check if user has permission to view data (all except guest)
@@ -204,9 +178,10 @@ const IndexTrending = memo(function IndexTrending() {
onChange={setPeriode} onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ options={[
{ value: 'Harian', label: 'Harian' }, { value: '5 Menit', label: '5 Menit' },
{ value: 'Mingguan', label: 'Mingguan' }, { value: '10 Menit', label: '10 Menit' },
{ value: 'Bulanan', label: 'Bulanan' }, { value: '30 Menit', label: '30 Menit' },
{ value: '1 Jam', label: '1 Jam' },
]} ]}
/> />
</div> </div>
@@ -234,9 +209,8 @@ const IndexTrending = memo(function IndexTrending() {
</Col> </Col>
</Row> </Row>
</Card> </Card>
{/* Charts Section */} {/* Charts Section */}
{!canViewData ? ( {/* {!canViewData ? (
<Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}> <Card style={{ marginTop: '24px', textAlign: 'center', padding: '40px' }}>
<Text style={{ fontSize: '16px', color: '#999' }}> <Text style={{ fontSize: '16px', color: '#999' }}>
Anda tidak memiliki akses untuk melihat data trending. Anda tidak memiliki akses untuk melihat data trending.
@@ -244,64 +218,78 @@ const IndexTrending = memo(function IndexTrending() {
Silakan hubungi administrator untuk mendapatkan akses. Silakan hubungi administrator untuk mendapatkan akses.
</Text> </Text>
</Card> </Card>
) : ( ) : ( */}
<> <>
<Row gutter={16} style={{ marginTop: '24px' }}> <Row gutter={16} style={{ marginTop: '24px' }}>
{/* Line Chart */} {/* Line Chart */}
<Col xs={24}> <Col xs={24}>
<Card className="chart-card"> <Card className="chart-card">
<div className="chart-header"> <div className="chart-header">
<Text strong style={{ fontSize: '14px' }}> <Text strong style={{ fontSize: '14px' }}>
Trending Error per Plant Sub Section Tag Value Trending
</Text> </Text>
</div> </div>
<div style={{ height: '500px', marginTop: '16px' }}> <div style={{ height: '500px', marginTop: '16px' }}>
<ResponsiveLine <ResponsiveLine
data={lineChartData} data={tagTrendingData}
margin={{ top: 20, right: 20, bottom: 50, left: 50 }} margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
xScale={{ type: 'point' }} xScale={{
yScale={{ type: 'linear', min: 0, max: 35 }} type: 'linear',
curve="natural" min: 'auto',
axisBottom={{ max: 'auto',
tickSize: 5, stacked: false,
tickPadding: 5, reverse: false,
tickRotation: 0, }}
}} yScale={{
axisLeft={{ type: 'point',
tickSize: 5, }}
tickPadding: 5, curve="natural"
tickRotation: 0, axisBottom={{
}} tickSize: 5,
colors={{ datum: 'color' }} tickPadding: 5,
pointSize={6} tickRotation: 0,
pointColor={{ theme: 'background' }} legend: 'Value',
pointBorderWidth={2} legendOffset: 40,
pointBorderColor={{ from: 'serieColor' }} legendPosition: 'middle',
pointLabelYOffset={-12} }}
useMesh={true} axisLeft={{
legends={[ tickSize: 5,
{ tickPadding: 5,
anchor: 'bottom', tickRotation: 0,
direction: 'row', legend: 'Time',
justify: false, legendOffset: -45,
translateX: 0, legendPosition: 'middle',
translateY: 50, }}
itemsSpacing: 10, colors={{ datum: 'color' }}
itemDirection: 'left-to-right', pointSize={6}
itemWidth: 140, pointColor={{ theme: 'background' }}
itemHeight: 20, pointBorderWidth={2}
itemOpacity: 0.75, pointBorderColor={{ from: 'serieColor' }}
symbolSize: 12, pointLabelYOffset={-12}
symbolShape: 'circle', useMesh={true}
}, legends={[
]} {
/> anchor: 'bottom-right',
</div> direction: 'column',
</Card> justify: false,
</Col> translateX: 100,
</Row> translateY: 0,
</> itemsSpacing: 2,
)} itemDirection: 'left-to-right',
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
},
]}
/>
</div>
</Card>
</Col>
</Row>
</>
{/* )} */}
</React.Fragment> </React.Fragment>
); );
}); });

View File

@@ -26,7 +26,7 @@ const DetailDevice = (props) => {
device_id: '', device_id: '',
device_code: '', device_code: '',
device_name: '', device_name: '',
device_status: true, is_active: true,
device_location: 'Building A', device_location: 'Building A',
device_description: '', device_description: '',
ip_address: '', ip_address: '',
@@ -139,7 +139,7 @@ const DetailDevice = (props) => {
// Backend validation schema doesn't include device_code // Backend validation schema doesn't include device_code
const payload = { const payload = {
device_name: FormData.device_name, device_name: FormData.device_name,
device_status: FormData.device_status, is_active: FormData.is_active,
device_location: FormData.device_location, device_location: FormData.device_location,
ip_address: FormData.ip_address, ip_address: FormData.ip_address,
}; };
@@ -211,11 +211,10 @@ const DetailDevice = (props) => {
const isChecked = event; const isChecked = event;
setFormData({ setFormData({
...FormData, ...FormData,
device_status: isChecked ? true : false, is_active: isChecked ? true : false,
}); });
}; };
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
@@ -312,16 +311,14 @@ const DetailDevice = (props) => {
disabled={props.readOnly} disabled={props.readOnly}
style={{ style={{
backgroundColor: backgroundColor:
FormData.device_status === true ? '#23A55A' : '#bfbfbf', FormData.is_active === true ? '#23A55A' : '#bfbfbf',
}} }}
checked={FormData.device_status === true} checked={FormData.is_active === true}
onChange={handleStatusToggle} onChange={handleStatusToggle}
/> />
</div> </div>
<div> <div>
<Text> <Text>{FormData.is_active === true ? 'Running' : 'Offline'}</Text>
{FormData.device_status === true ? 'Running' : 'Offline'}
</Text>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@ const DetailTag = (props) => {
const defaultData = { const defaultData = {
tag_id: '', tag_id: '',
tag_code: '',
tag_name: '', tag_name: '',
tag_number: '', tag_number: '',
data_type: '', data_type: '',
@@ -24,8 +24,7 @@ const DetailTag = (props) => {
is_active: true, is_active: true,
is_alarm: false, is_alarm: false,
device_id: null, device_id: null,
device_code: '',
device_name: '',
sub_section_id: null, sub_section_id: null,
}; };
@@ -118,6 +117,18 @@ const DetailTag = (props) => {
return; return;
} }
// Validasi data type harus Diskrit atau Analog
const validDataTypes = ['Diskrit', '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.`,
});
setConfirmLoading(false);
return;
}
if (!FormData.unit || FormData.unit.trim() === '') { if (!FormData.unit || FormData.unit.trim() === '') {
NotifOk({ NotifOk({
icon: 'warning', icon: 'warning',
@@ -141,17 +152,22 @@ const DetailTag = (props) => {
// Prepare payload berdasarkan backend validation schema // Prepare payload berdasarkan backend validation schema
const payload = { const payload = {
tag_name: FormData.tag_name, tag_name: FormData.tag_name.trim(),
tag_number: parseInt(FormData.tag_number), tag_number: parseInt(FormData.tag_number),
data_type: FormData.data_type, data_type: FormData.data_type,
unit: FormData.unit, unit: FormData.unit.trim(),
is_active: FormData.is_active, is_active: FormData.is_active,
is_alarm: FormData.is_alarm, is_alarm: FormData.is_alarm,
device_id: parseInt(FormData.device_id), device_id: parseInt(FormData.device_id),
device_code: FormData.device_code,
device_name: FormData.device_name,
}; };
// Add sub_section_id only if it's selected
if (FormData.sub_section_id) {
payload.sub_section_id = parseInt(FormData.sub_section_id);
}
// Debug logging
try { try {
let response; let response;
@@ -185,6 +201,7 @@ const DetailTag = (props) => {
} }
} catch (error) { } catch (error) {
console.error('Save Tag Error:', error); console.error('Save Tag Error:', error);
console.error('Error details:', error);
NotifAlert({ NotifAlert({
icon: 'error', icon: 'error',
title: 'Error', title: 'Error',
@@ -215,8 +232,6 @@ const DetailTag = (props) => {
setFormData({ setFormData({
...FormData, ...FormData,
device_id: deviceId, device_id: deviceId,
device_code: selectedDevice?.device_code || '',
device_name: selectedDevice?.device_name || '',
}); });
}; };

View File

@@ -51,6 +51,13 @@ const columns = (showPreviewModal, showEditModal, showDeleteDialog) => [
key: 'unit', key: 'unit',
width: '8%', width: '8%',
}, },
{
title: 'Sub Section',
dataIndex: 'sub_section_name',
key: 'sub_section_name',
width: '12%',
render: (text) => text || '-',
},
{ {
title: 'Device', title: 'Device',
dataIndex: 'device_name', dataIndex: 'device_name',