progress history report

This commit is contained in:
2025-10-27 10:28:42 +07:00
parent 5a8e2dee2f
commit 39d8be10cc
6 changed files with 344 additions and 169 deletions

View File

@@ -18,4 +18,37 @@ const getAllHistoryEvent = async (queryParams) => {
return response.data; return response.data;
}; };
export { getAllHistoryAlarm, getAllHistoryEvent }; const getAllHistoryValueReport = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-report?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryValueReportPivot = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-report-pivot?${queryParams.toString()}`,
});
return response.data;
};
const getAllHistoryValueTrendingPivot = async (queryParams) => {
const response = await SendRequest({
method: 'get',
prefix: `history/value-trending?${queryParams.toString()}`,
});
return response.data;
};
export {
getAllHistoryAlarm,
getAllHistoryEvent,
getAllHistoryValueReport,
getAllHistoryValueReportPivot,
getAllHistoryValueTrendingPivot,
};

View File

@@ -57,22 +57,28 @@ const CardList = ({
} }
style={getCardStyle(fieldColor ? item[fieldColor] : cardColor)} style={getCardStyle(fieldColor ? item[fieldColor] : cardColor)}
actions={[ actions={[
<EyeOutlined showPreviewModal && (
<EyeOutlined
style={{ color: '#1890ff' }} style={{ color: '#1890ff' }}
key="preview" key="preview"
onClick={() => showPreviewModal(item)} onClick={() => showPreviewModal(item)}
/>, />
<EditOutlined ),
showEditModal && (
<EditOutlined
style={{ color: '#faad14' }} style={{ color: '#faad14' }}
key="edit" key="edit"
onClick={() => showEditModal(item)} onClick={() => showEditModal(item)}
/>, />
<DeleteOutlined ),
showDeleteDialog && (
<DeleteOutlined
style={{ color: '#ff1818' }} style={{ color: '#ff1818' }}
key="delete" key="delete"
onClick={() => showDeleteDialog(item)} onClick={() => showDeleteDialog(item)}
/>, />
]} ),
].filter(Boolean)} // <== Hapus elemen yang undefined
> >
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
{column.map((itemCard, index) => ( {column.map((itemCard, index) => (

View File

@@ -1,6 +1,6 @@
import React, { memo, useState, useEffect, useRef } from 'react'; import React, { memo, useState, useEffect, useRef } from 'react';
import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd'; import { Table, Pagination, Row, Col, Card, Grid, Button, Typography, Tag, Segmented } from 'antd';
import { AppstoreOutlined, TableOutlined } from '@ant-design/icons'; import { MacCommandOutlined, TableOutlined } from '@ant-design/icons';
import CardList from './CardList'; import CardList from './CardList';
const { Text } = Typography; const { Text } = Typography;
@@ -18,6 +18,7 @@ const TableList = memo(function TableList({
showDeleteDialog, showDeleteDialog,
cardColor, cardColor,
fieldColor, fieldColor,
firstLoad = true,
}) { }) {
const [gridLoading, setGridLoading] = useState(false); const [gridLoading, setGridLoading] = useState(false);
@@ -30,12 +31,19 @@ const TableList = memo(function TableList({
total_page: 1, total_page: 1,
}); });
const [viewMode, setViewMode] = useState('list'); const [viewMode, setViewMode] = useState('table');
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
const [renderCount, setRenderCount] = useState(firstLoad ? 1 : 0);
useEffect(() => { useEffect(() => {
filter(1, pagination.current_limit); if (renderCount < 1) {
setRenderCount(renderCount + 1);
return;
} else {
filter(1, pagination.current_limit);
}
}, [triger]); }, [triger]);
const filter = async (currentPage, pageSize) => { const filter = async (currentPage, pageSize) => {
@@ -90,8 +98,8 @@ const TableList = memo(function TableList({
<div> <div>
<Segmented <Segmented
options={[ options={[
{ value: 'card', icon: <AppstoreOutlined /> },
{ value: 'table', icon: <TableOutlined /> }, { value: 'table', icon: <TableOutlined /> },
{ value: 'card', icon: <MacCommandOutlined /> },
]} ]}
value={viewMode} value={viewMode}
onChange={setViewMode} onChange={setViewMode}
@@ -116,6 +124,7 @@ const TableList = memo(function TableList({
pagination={false} pagination={false}
loading={gridLoading} loading={gridLoading}
scroll={{ y: 520 }} scroll={{ y: 520 }}
size="small"
/> />
</Row> </Row>
)} )}

View File

@@ -3,6 +3,8 @@ import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'a
import TableList from '../../../../components/Global/TableList'; import TableList from '../../../../components/Global/TableList';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { FileTextOutlined } from '@ant-design/icons'; import { FileTextOutlined } from '@ant-design/icons';
import { getAllHistoryValueReport } from '../../../../api/history-value';
import { getAllPlantSection } from '../../../../api/master-plant-section';
const { Text } = Typography; const { Text } = Typography;
@@ -19,7 +21,7 @@ const ListReport = memo(function ListReport(props) {
title: 'Datetime', title: 'Datetime',
dataIndex: 'datetime', dataIndex: 'datetime',
key: 'datetime', key: 'datetime',
width: '10%', width: '15%',
}, },
{ {
title: 'Tag Name', title: 'Tag Name',
@@ -32,38 +34,73 @@ const ListReport = memo(function ListReport(props) {
dataIndex: 'val', dataIndex: 'val',
key: 'val', key: 'val',
width: '10%', width: '10%',
render: (_, record) => Number(record.val).toFixed(4),
}, },
{ {
title: 'Stat', title: 'Stat',
dataIndex: 'stat', dataIndex: 'status',
key: 'stat', key: 'status',
width: '10%', width: '10%',
}, },
]; ];
const dateNow = dayjs();
const dateNowFormated = dateNow.format('YYYY-MM-DD');
const [trigerFilter, setTrigerFilter] = useState(false); const [trigerFilter, setTrigerFilter] = useState(false);
const defaultFilter = { search: '' }; const defaultFilter = {
criteria: '',
plant_sub_section_id: 0,
from: dateNowFormated,
to: dateNowFormated,
interval: 10,
};
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const [plantSubSection, setPlantSubSection] = useState(0);
const [startDate, setStartDate] = useState(dayjs('2025-09-30')); const [plantSubSectionList, setPlantSubSectionList] = useState([]);
const [endDate, setEndDate] = useState(dayjs('2025-10-09')); const [startDate, setStartDate] = useState(dateNow);
const [periode, setPeriode] = useState('10 Menit'); const [endDate, setEndDate] = useState(dateNow);
const [periode, setPeriode] = useState(5);
const getAllReport = async (params) => { const handleSearch = () => {
return { const formattedDateStart = startDate.format('YYYY-MM-DD');
data: [], const formattedDateEnd = endDate.format('YYYY-MM-DD');
};
setFormDataFilter({
criteria: '',
plant_sub_section_id: plantSubSection,
from: formattedDateStart,
to: formattedDateEnd,
interval: periode,
});
setTrigerFilter((prev) => !prev);
}; };
const handleReset = () => { const handleReset = () => {
setPlantSubSection('Semua Plant'); setPlantSubSection(0);
setStartDate(dayjs('2025-09-30')); setStartDate(dateNow);
setEndDate(dayjs('2025-10-09')); setEndDate(dateNow);
setPeriode('10 Menit'); setPeriode(5);
}; };
const getPlantSubSection = async () => {
const params = new URLSearchParams({ page: 1 });
const response = await getAllPlantSection(params);
if (response && response.data) {
const activePlantSubSections = response.data.filter(
(section) => section.is_active === true
);
setPlantSubSectionList(activePlantSubSections);
}
};
useEffect(() => {
getPlantSubSection();
}, []);
return ( return (
<React.Fragment> <React.Fragment>
<Card> <Card>
@@ -77,14 +114,21 @@ const ListReport = memo(function ListReport(props) {
</Text> </Text>
<Select <Select
value={plantSubSection} value={plantSubSection}
onChange={setPlantSubSection} onChange={(value) => setPlantSubSection(value)}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ >
{ value: 'Semua Plant', label: 'Semua Plant' }, <Select.Option key={0} value={0}>
{ value: 'Plant 1', label: 'Plant 1' }, Pilih Plant Sub Section
{ value: 'Plant 2', label: 'Plant 2' }, </Select.Option>
]} {plantSubSectionList.map((item) => (
/> <Select.Option
key={item.plant_sub_section_id}
value={item.plant_sub_section_id}
>
{item.plant_sub_section_name}
</Select.Option>
))}
</Select>
</div> </div>
</Col> </Col>
<Col xs={24} sm={12} md={6}> <Col xs={24} sm={12} md={6}>
@@ -95,7 +139,7 @@ const ListReport = memo(function ListReport(props) {
<DatePicker <DatePicker
value={startDate} value={startDate}
onChange={setStartDate} onChange={setStartDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -108,7 +152,7 @@ const ListReport = memo(function ListReport(props) {
<DatePicker <DatePicker
value={endDate} value={endDate}
onChange={setEndDate} onChange={setEndDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -121,18 +165,24 @@ const ListReport = memo(function ListReport(props) {
onChange={setPeriode} onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ options={[
{ value: '5 Menit', label: '5 Menit' }, { value: 5, label: '5 Minute' },
{ value: '10 Menit', label: '10 Menit' }, { value: 10, label: '10 Minute' },
{ value: '30 Menit', label: '30 Menit' }, { value: 30, label: '30 Minute' },
{ value: '1 Jam', label: '1 Jam' }, { value: 60, label: '1 Hour' },
{ value: 120, label: '2 Hour' },
]} ]}
/> ></Select>
</div> </div>
</Col> </Col>
</Row> </Row>
<Row gutter={8} style={{ marginTop: '16px' }}> <Row gutter={8} style={{ marginTop: '16px' }}>
<Col> <Col>
<Button type="primary" danger icon={<FileTextOutlined />}> <Button
type="primary"
danger
icon={<FileTextOutlined />}
onClick={handleSearch}
>
Tampilkan Tampilkan
</Button> </Button>
</Col> </Col>
@@ -148,7 +198,8 @@ const ListReport = memo(function ListReport(props) {
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}> <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<TableList <TableList
getData={getAllReport} firstLoad={false}
getData={getAllHistoryValueReport}
queryParams={formDataFilter} queryParams={formDataFilter}
columns={columns} columns={columns}
triger={trigerFilter} triger={trigerFilter}

View File

@@ -9,7 +9,6 @@ const { Text } = Typography;
const IndexTrending = memo(function IndexTrending() { const IndexTrending = memo(function IndexTrending() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setBreadcrumbItems } = useBreadcrumb(); const { setBreadcrumbItems } = useBreadcrumb();
const [selectedData, setSelectedData] = useState(null);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');

View File

@@ -4,75 +4,94 @@ import dayjs from 'dayjs';
import { FileTextOutlined } from '@ant-design/icons'; import { FileTextOutlined } from '@ant-design/icons';
import { ResponsiveLine } from '@nivo/line'; import { ResponsiveLine } from '@nivo/line';
import './trending.css'; import './trending.css';
import { getAllPlantSection } from '../../../api/master-plant-section';
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
const { Text } = Typography; const { Text } = Typography;
const tagTrendingData = [
{
id: 'TEMP_SENSOR_1',
color: '#FF6B4A',
data: [
{ y: '08:00', x: 75 },
{ y: '08:05', x: 76 },
{ y: '08:10', x: 75 },
{ y: '08:15', x: 77 },
{ y: '08:20', x: 76 },
{ y: '08:25', x: 78 },
{ y: '08:30', x: 79 },
],
},
{
id: 'GAS_LEAK_SENSOR_1',
color: '#4ECDC4',
data: [
{ y: '08:00', x: 10 },
{ y: '08:05', x: 150 },
{ y: '08:10', x: 40 },
{ y: '08:15', x: 20 },
{ y: '08:20', x: 15 },
{ y: '08:25', x: 18 },
{ y: '08:30', x: 25 },
],
},
{
id: 'PRESSURE_SENSOR_1',
color: '#FFE66D',
data: [
{ y: '08:00', x: 1.2 },
{ y: '08:05', x: 1.3 },
{ y: '08:10', x: 1.2 },
{ y: '08:15', x: 1.4 },
{ y: '08:20', x: 1.5 },
{ y: '08:25', x: 1.3 },
{ y: '08:30', x: 1.2 },
],
},
];
const ReportTrending = memo(function ReportTrending(props) { const ReportTrending = memo(function ReportTrending(props) {
const [trigerFilter, setTrigerFilter] = useState(false); const dateNow = dayjs();
const dateNowFormated = dateNow.format('YYYY-MM-DD');
const defaultFilter = { search: '' }; const [plantSubSection, setPlantSubSection] = useState(0);
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
const [startDate, setStartDate] = useState(dateNow);
const [endDate, setEndDate] = useState(dateNow);
const [periode, setPeriode] = useState(60);
const defaultFilter = {
criteria: '',
plant_sub_section_id: plantSubSection,
from: dateNowFormated,
to: dateNowFormated,
interval: periode,
};
const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
const [plantSubSection, setPlantSubSection] = useState('Semua Plant'); const [trendingValue, setTrendingValue] = useState([]);
const [startDate, setStartDate] = useState(dayjs('2025-09-30'));
const [endDate, setEndDate] = useState(dayjs('2025-10-09'));
const [periode, setPeriode] = useState('10 Menit');
const getAllReport = async (params) => { const handleSearch = async () => {
return { const formattedDateStart = startDate.format('YYYY-MM-DD');
data: [], const formattedDateEnd = endDate.format('YYYY-MM-DD');
const newFilter = {
criteria: '',
plant_sub_section_id: plantSubSection,
from: formattedDateStart,
to: formattedDateEnd,
interval: periode,
}; };
setFormDataFilter(newFilter);
const param = new URLSearchParams(newFilter);
const response = await getAllHistoryValueTrendingPivot(param);
if (response?.data?.length > 0) {
// 🔹 Bersihkan dan format data agar aman untuk Nivo
const cleanedData = response.data.map((serie) => ({
id: serie.id ?? 'Unknown',
data: Array.isArray(serie.data)
? serie.data.map((d) => ({
x: d?.x ?? null,
y:
d?.y !== null && d?.y !== undefined
? Number(d.y).toFixed(4) // format 4 angka di belakang koma
: null,
}))
: [],
}));
// setTrendingValue(cleanedData);
} else {
// 🔹 Jika tidak ada data dari API
// setTrendingValue([]);
}
}; };
const handleReset = () => { const handleReset = () => {
setPlantSubSection('Semua Plant'); setPlantSubSection(0);
setStartDate(dayjs('2025-09-30')); setStartDate(dateNow);
setEndDate(dayjs('2025-10-09')); setEndDate(dateNow);
setPeriode('10 Menit'); setPeriode(5);
}; };
const getPlantSubSection = async () => {
const params = new URLSearchParams({ page: 1 });
const response = await getAllPlantSection(params);
if (response && response.data) {
const activePlantSubSections = response.data.filter(
(section) => section.is_active === true
);
setPlantSubSectionList(activePlantSubSections);
}
};
useEffect(() => {
getPlantSubSection();
}, []);
return ( return (
<React.Fragment> <React.Fragment>
<Card> <Card>
@@ -86,14 +105,21 @@ const ReportTrending = memo(function ReportTrending(props) {
</Text> </Text>
<Select <Select
value={plantSubSection} value={plantSubSection}
onChange={setPlantSubSection} onChange={(value) => setPlantSubSection(value)}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ >
{ value: 'Semua Plant', label: 'Semua Plant' }, <Select.Option key={0} value={0}>
{ value: 'Plant 1', label: 'Plant 1' }, Pilih Plant Sub Section
{ value: 'Plant 2', label: 'Plant 2' }, </Select.Option>
]} {plantSubSectionList.map((item) => (
/> <Select.Option
key={item.plant_sub_section_id}
value={item.plant_sub_section_id}
>
{item.plant_sub_section_name}
</Select.Option>
))}
</Select>
</div> </div>
</Col> </Col>
<Col xs={24} sm={12} md={6}> <Col xs={24} sm={12} md={6}>
@@ -104,7 +130,7 @@ const ReportTrending = memo(function ReportTrending(props) {
<DatePicker <DatePicker
value={startDate} value={startDate}
onChange={setStartDate} onChange={setStartDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -117,7 +143,7 @@ const ReportTrending = memo(function ReportTrending(props) {
<DatePicker <DatePicker
value={endDate} value={endDate}
onChange={setEndDate} onChange={setEndDate}
format="DD/MM/YYYY" format="DD-MM-YYYY"
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
/> />
</div> </div>
@@ -130,18 +156,24 @@ const ReportTrending = memo(function ReportTrending(props) {
onChange={setPeriode} onChange={setPeriode}
style={{ width: '100%', marginTop: '4px' }} style={{ width: '100%', marginTop: '4px' }}
options={[ options={[
{ value: '5 Menit', label: '5 Menit' }, { value: 5, label: '5 Minute' },
{ value: '10 Menit', label: '10 Menit' }, { value: 10, label: '10 Minute' },
{ value: '30 Menit', label: '30 Menit' }, { value: 30, label: '30 Minute' },
{ value: '1 Jam', label: '1 Jam' }, { value: 60, label: '1 Hour' },
{ value: 120, label: '2 Hour' },
]} ]}
/> ></Select>
</div> </div>
</Col> </Col>
</Row> </Row>
<Row gutter={8} style={{ marginTop: '16px' }}> <Row gutter={8} style={{ marginTop: '16px' }}>
<Col> <Col>
<Button type="primary" danger icon={<FileTextOutlined />}> <Button
type="primary"
danger
icon={<FileTextOutlined />}
onClick={handleSearch}
>
Tampilkan Tampilkan
</Button> </Button>
</Col> </Col>
@@ -157,60 +189,105 @@ const ReportTrending = memo(function ReportTrending(props) {
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}> <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
<div style={{ height: '500px', marginTop: '16px' }}> <div style={{ height: '500px', marginTop: '16px' }}>
<ResponsiveLine {trendingValue && trendingValue.length > 0 ? (
data={tagTrendingData} <ResponsiveLine
margin={{ top: 20, right: 20, bottom: 50, left: 60 }} data={trendingValue} // [{ id, data: [{x, y}] }]
xScale={{ // data={
type: 'linear', // trendingValue && trendingValue.length
min: 'auto', // ? trendingValue
max: 'auto', // : [{ id, data: [{ x, y }] }]
stacked: false, // }
reverse: false, margin={{ top: 40, right: 100, bottom: 70, left: 70 }}
}} xScale={{
yScale={{ type: 'time',
type: 'point', format: '%Y-%m-%d %H:%M',
}} useUTC: false,
curve="natural" precision: 'minute',
axisBottom={{ }}
tickSize: 5, xFormat="time:%Y-%m-%d %H:%M"
tickPadding: 5, yScale={{
tickRotation: 0, type: 'linear',
legend: 'Value', min: 'auto',
legendOffset: 40, max: 'auto',
legendPosition: 'middle', stacked: false,
}} reverse: false,
axisLeft={{ }}
tickSize: 5, yFormat={(value) => Number(value).toFixed(4)} // ✅ format 4 angka di belakang koma
tickPadding: 5, axisBottom={{
tickRotation: 0, format: '%Y-%m-%d %H:%M', // ✅ tampilkan tanggal + jam
legend: 'Time', tickValues: 'every 2 hours', // tampilkan setiap 2 jam (bisa ubah ke every 30 minutes)
legendOffset: -45, tickSize: 5,
legendPosition: 'middle', tickPadding: 5,
}} tickRotation: -45,
colors={{ datum: 'color' }} legend: 'Tanggal & Waktu',
pointSize={6} legendOffset: 60,
pointColor={{ theme: 'background' }} legendPosition: 'middle',
pointBorderWidth={2} }}
pointBorderColor={{ from: 'serieColor' }} axisLeft={{
pointLabelYOffset={-12} tickSize: 5,
useMesh={true} tickPadding: 5,
legends={[ tickRotation: 0,
{ legend: 'Nilai (Avg)',
anchor: 'bottom-right', legendOffset: -60,
direction: 'column', legendPosition: 'middle',
justify: false, format: (value) => Number(value).toFixed(4), // ✅ tampilkan 4 angka di sumbu Y
translateX: 100, }}
translateY: 0, curve="monotoneX"
itemsSpacing: 2, colors={{ scheme: 'category10' }}
itemDirection: 'left-to-right', pointSize={6}
itemWidth: 80, pointColor={{ theme: 'background' }}
itemHeight: 20, pointBorderWidth={2}
itemOpacity: 0.75, pointBorderColor={{ from: 'serieColor' }}
symbolSize: 12, enablePointLabel={false}
symbolShape: 'circle', enableGridX={true}
}, enableGridY={true}
]} useMesh={true}
/> tooltip={({ point }) => (
<div
style={{
background: 'white',
padding: '6px 9px',
border: '1px solid #ccc',
borderRadius: '6px',
}}
>
<strong>{point.serieId}</strong>
<br />
{point.data.xFormatted}
<br />
<span style={{ color: point.serieColor }}>
{Number(point.data.y).toFixed(4)}
</span>
</div>
)}
legends={[
{
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 100,
translateY: 0,
itemsSpacing: 2,
itemDirection: 'left-to-right',
itemWidth: 120,
itemHeight: 20,
itemOpacity: 0.85,
symbolSize: 12,
symbolShape: 'circle',
},
]}
/>
) : (
<div
style={{
textAlign: 'center',
marginTop: '40px',
color: '#999',
}}
>
Tidak ada data untuk ditampilkan
</div>
)}
</div> </div>
</Col> </Col>
</Row> </Row>