Perbaikan Menu Report dan Trending
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import React, { memo, useState, useEffect } from 'react';
|
||||
import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd';
|
||||
import { Button, Row, Col, Card, DatePicker, Select, Typography, Modal, Spin } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { FileTextOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer
|
||||
} from 'recharts';
|
||||
import './trending.css';
|
||||
import { getAllPlantSection } from '../../../api/master-plant-section';
|
||||
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
|
||||
@@ -18,6 +27,7 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
const [startDate, setStartDate] = useState(dateNow);
|
||||
const [endDate, setEndDate] = useState(dateNow);
|
||||
const [periode, setPeriode] = useState(60);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const defaultFilter = {
|
||||
criteria: '',
|
||||
@@ -29,51 +39,83 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||
|
||||
const [trendingValue, setTrendingValue] = useState([]);
|
||||
const [chartData, setChartData] = useState([]);
|
||||
const [metrics, setMetrics] = useState([]);
|
||||
|
||||
// Palet warna
|
||||
const colorPalette = [
|
||||
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
||||
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16'
|
||||
];
|
||||
|
||||
const handleSearch = async () => {
|
||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||
|
||||
const newFilter = {
|
||||
criteria: '',
|
||||
plant_sub_section_id: plantSubSection,
|
||||
from: formattedDateStart,
|
||||
to: formattedDateEnd,
|
||||
interval: periode,
|
||||
};
|
||||
const newFilter = {
|
||||
criteria: '',
|
||||
plant_sub_section_id: plantSubSection,
|
||||
from: formattedDateStart,
|
||||
to: formattedDateEnd,
|
||||
interval: periode,
|
||||
};
|
||||
|
||||
setFormDataFilter(newFilter);
|
||||
setFormDataFilter(newFilter);
|
||||
|
||||
const param = new URLSearchParams(newFilter);
|
||||
const response = await getAllHistoryValueTrendingPivot(param);
|
||||
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([]);
|
||||
if (response?.data?.length > 0) {
|
||||
transformDataForRecharts(response.data);
|
||||
} else {
|
||||
setTrendingValue([]);
|
||||
setChartData([]);
|
||||
setMetrics([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching trending data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const transformDataForRecharts = (nivoData) => {
|
||||
setTrendingValue(nivoData);
|
||||
|
||||
const metricNames = nivoData.map(serie => serie.id);
|
||||
setMetrics(metricNames);
|
||||
|
||||
const timeMap = new Map();
|
||||
|
||||
nivoData.forEach(serie => {
|
||||
serie.data.forEach(point => {
|
||||
if (!timeMap.has(point.x)) {
|
||||
timeMap.set(point.x, { time: point.x });
|
||||
}
|
||||
const entry = timeMap.get(point.x);
|
||||
entry[serie.id] = point.y !== null && point.y !== undefined
|
||||
? parseFloat(point.y)
|
||||
: null;
|
||||
});
|
||||
});
|
||||
|
||||
const transformedData = Array.from(timeMap.values()).sort((a, b) =>
|
||||
new Date(a.time) - new Date(b.time)
|
||||
);
|
||||
|
||||
setChartData(transformedData);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setPlantSubSection(0);
|
||||
setStartDate(dateNow);
|
||||
setEndDate(dateNow);
|
||||
setPeriode(5);
|
||||
setPeriode(60);
|
||||
setChartData([]);
|
||||
setMetrics([]);
|
||||
};
|
||||
|
||||
const getPlantSubSection = async () => {
|
||||
@@ -88,12 +130,154 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const formatXAxis = (tickItem) => {
|
||||
const date = new Date(tickItem);
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
||||
padding: '12px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
|
||||
}}>
|
||||
<p style={{ margin: 0, fontWeight: 'bold', marginBottom: '8px' }}>
|
||||
{new Date(label).toLocaleString('id-ID')}
|
||||
</p>
|
||||
{payload.map((entry, index) => (
|
||||
<p key={index} style={{
|
||||
margin: '4px 0',
|
||||
color: entry.color,
|
||||
fontSize: '13px'
|
||||
}}>
|
||||
<strong>{entry.name}:</strong> {Number(entry.value).toFixed(4)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderChart = () => {
|
||||
if (!chartData || chartData.length === 0) {
|
||||
return (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '100px',
|
||||
color: '#999',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
Tidak ada data untuk ditampilkan
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={500}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{ top: 20, right: 200, left: 80, bottom: 40 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={100}
|
||||
tick={{ fontSize: 11 }}
|
||||
tickFormatter={formatXAxis}
|
||||
label={{
|
||||
value: 'Waktu',
|
||||
position: 'bottom',
|
||||
offset: -50,
|
||||
style: { fontSize: 14, fontWeight: 'bold' }
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11 }}
|
||||
label={{
|
||||
value: 'Nilai',
|
||||
angle: -90,
|
||||
position: 'right',
|
||||
offset: -70,
|
||||
dy: 0,
|
||||
style: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
fill: '#059669',
|
||||
textAnchor: 'middle'
|
||||
}
|
||||
}}
|
||||
tickFormatter={(value) => Number(value).toFixed(2)}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Legend
|
||||
layout="vertical"
|
||||
align="right"
|
||||
verticalAlign="middle"
|
||||
wrapperStyle={{
|
||||
position: 'absolute',
|
||||
right: 150,
|
||||
top: '35%',
|
||||
transform: 'translateY(-50%)'
|
||||
}}
|
||||
/>
|
||||
{metrics.map((metric, index) => {
|
||||
const color = colorPalette[index % colorPalette.length];
|
||||
return (
|
||||
<Line
|
||||
key={metric}
|
||||
type="monotone"
|
||||
dataKey={metric}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
dot={chartData.length < 50}
|
||||
name={metric}
|
||||
connectNulls={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPlantSubSection();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Loading Modal */}
|
||||
<Modal
|
||||
open={isLoading}
|
||||
footer={null}
|
||||
closable={false}
|
||||
centered
|
||||
width={400}
|
||||
bodyStyle={{
|
||||
textAlign: 'center',
|
||||
padding: '40px 20px'
|
||||
}}
|
||||
>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 48, color: '#1890ff' }} spin />}
|
||||
/>
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: '8px' }}>
|
||||
Please Wait
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
System is generating trending data...
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={24}>
|
||||
@@ -162,10 +346,11 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
{ value: 60, label: '1 Hour' },
|
||||
{ value: 120, label: '2 Hour' },
|
||||
]}
|
||||
></Select>
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={8} style={{ marginTop: '16px' }}>
|
||||
<Col>
|
||||
<Button
|
||||
@@ -187,108 +372,9 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
||||
<div style={{ height: '500px', marginTop: '16px' }}>
|
||||
{trendingValue && trendingValue.length > 0 ? (
|
||||
<ResponsiveLine
|
||||
data={trendingValue} // [{ id, data: [{x, y}] }]
|
||||
// data={
|
||||
// trendingValue && trendingValue.length
|
||||
// ? trendingValue
|
||||
// : [{ id, data: [{ x, y }] }]
|
||||
// }
|
||||
margin={{ top: 40, right: 100, bottom: 70, left: 70 }}
|
||||
xScale={{
|
||||
type: 'time',
|
||||
format: '%Y-%m-%d %H:%M',
|
||||
useUTC: false,
|
||||
precision: 'minute',
|
||||
}}
|
||||
xFormat="time:%Y-%m-%d %H:%M"
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
min: 'auto',
|
||||
max: 'auto',
|
||||
stacked: false,
|
||||
reverse: false,
|
||||
}}
|
||||
yFormat={(value) => Number(value).toFixed(4)} // ✅ format 4 angka di belakang koma
|
||||
axisBottom={{
|
||||
format: '%Y-%m-%d %H:%M', // ✅ tampilkan tanggal + jam
|
||||
tickValues: 'every 2 hours', // tampilkan setiap 2 jam (bisa ubah ke every 30 minutes)
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: -45,
|
||||
legend: 'Tanggal & Waktu',
|
||||
legendOffset: 60,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
axisLeft={{
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'Nilai (Avg)',
|
||||
legendOffset: -60,
|
||||
legendPosition: 'middle',
|
||||
format: (value) => Number(value).toFixed(4), // ✅ tampilkan 4 angka di sumbu Y
|
||||
}}
|
||||
curve="monotoneX"
|
||||
colors={{ scheme: 'category10' }}
|
||||
pointSize={6}
|
||||
pointColor={{ theme: 'background' }}
|
||||
pointBorderWidth={2}
|
||||
pointBorderColor={{ from: 'serieColor' }}
|
||||
enablePointLabel={false}
|
||||
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>
|
||||
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '24px' }}>
|
||||
{renderChart()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
@@ -296,4 +382,4 @@ const ReportTrending = memo(function ReportTrending(props) {
|
||||
);
|
||||
});
|
||||
|
||||
export default ReportTrending;
|
||||
export default ReportTrending;
|
||||
Reference in New Issue
Block a user