Perbaikan Menu Report dan Trending
This commit is contained in:
@@ -22,7 +22,8 @@
|
|||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.4",
|
||||||
|
"jspdf-autotable": "^5.0.2",
|
||||||
"mqtt": "^5.14.0",
|
"mqtt": "^5.14.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-svg": "^16.3.0",
|
"react-svg": "^16.3.0",
|
||||||
|
"recharts": "^3.6.0",
|
||||||
"sweetalert2": "^11.17.2"
|
"sweetalert2": "^11.17.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
public/assets/pupuk-indonesia-1.png
Normal file
BIN
public/assets/pupuk-indonesia-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
BIN
public/assets/pupuk-indonesia-2.jpg
Normal file
BIN
public/assets/pupuk-indonesia-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -1,91 +1,211 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
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, Table, Spin, Modal } from 'antd';
|
||||||
import TableList from '../../../../components/Global/TableList';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
getAllHistoryValueReport,
|
|
||||||
getAllHistoryValueReportPivot,
|
getAllHistoryValueReportPivot,
|
||||||
|
getAllHistoryValueReport,
|
||||||
} from '../../../../api/history-value';
|
} from '../../../../api/history-value';
|
||||||
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
import { getAllPlantSection } from '../../../../api/master-plant-section';
|
||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import autoTable from 'jspdf-autotable';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const ListReport = memo(function ListReport(props) {
|
const ListReport = memo(function ListReport(props) {
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'No',
|
|
||||||
key: 'no',
|
|
||||||
width: '5%',
|
|
||||||
align: 'center',
|
|
||||||
render: (_, __, index) => index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Datetime',
|
|
||||||
dataIndex: 'datetime',
|
|
||||||
key: 'datetime',
|
|
||||||
width: '15%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tag Name',
|
|
||||||
dataIndex: 'tag_name',
|
|
||||||
key: 'tag_name',
|
|
||||||
width: '70%',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: 'Value',
|
|
||||||
// dataIndex: 'val',
|
|
||||||
// key: 'val',
|
|
||||||
// width: '10%',
|
|
||||||
// render: (_, record) => Number(record.val).toFixed(4),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: 'Stat',
|
|
||||||
// dataIndex: 'status',
|
|
||||||
// key: 'status',
|
|
||||||
// width: '10%',
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
|
|
||||||
const dateNow = dayjs();
|
const dateNow = dayjs();
|
||||||
const dateNowFormated = dateNow.format('YYYY-MM-DD');
|
const dateNowFormated = dateNow.format('YYYY-MM-DD');
|
||||||
|
|
||||||
const [trigerFilter, setTrigerFilter] = useState(false);
|
const [isLoadingModal, setIsLoadingModal] = useState(false); // Modal loading
|
||||||
|
const [isLoadingTable, setIsLoadingTable] = useState(false); // Table loading
|
||||||
|
const [tableData, setTableData] = useState([]);
|
||||||
|
const [pivotData, setPivotData] = useState([]);
|
||||||
|
const [valueReportData, setValueReportData] = useState([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const [plantSubSection, setPlantSubSection] = useState(0);
|
const [plantSubSection, setPlantSubSection] = useState(0);
|
||||||
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
const [plantSubSectionList, setPlantSubSectionList] = useState([]);
|
||||||
const [startDate, setStartDate] = useState(dateNow);
|
const [startDate, setStartDate] = useState(dateNow);
|
||||||
const [endDate, setEndDate] = useState(dateNow);
|
const [endDate, setEndDate] = useState(dateNow);
|
||||||
const [periode, setPeriode] = useState(10);
|
const [periode, setPeriode] = useState(30);
|
||||||
|
|
||||||
const defaultFilter = {
|
const columns = [
|
||||||
criteria: '',
|
{
|
||||||
plant_sub_section_id: 0,
|
title: 'No',
|
||||||
from: dateNowFormated,
|
key: 'no',
|
||||||
to: dateNowFormated,
|
width: 60,
|
||||||
interval: periode,
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
render: (_, __, index) => {
|
||||||
|
return (pagination.current - 1) * pagination.pageSize + index + 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datetime',
|
||||||
|
dataIndex: 'datetime',
|
||||||
|
key: 'datetime',
|
||||||
|
width: 180,
|
||||||
|
sorter: (a, b) => new Date(a.datetime) - new Date(b.datetime),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tag Name',
|
||||||
|
dataIndex: 'tagName',
|
||||||
|
key: 'tagName',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
width: 120,
|
||||||
|
align: 'left', // Aligned kanan (seperti angka pada umumnya)
|
||||||
|
render: (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return Number(value).toFixed(2); // Format 2 desimal
|
||||||
|
},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fungsi helper untuk generate semua waktu dalam sehari berdasarkan periode
|
||||||
|
const generateFullDayTimes = (dateString, intervalMinutes) => {
|
||||||
|
const times = [];
|
||||||
|
const startOfDay = dayjs(dateString).startOf('day');
|
||||||
|
const endOfDay = dayjs(dateString).endOf('day');
|
||||||
|
|
||||||
|
let currentTime = startOfDay;
|
||||||
|
|
||||||
|
while (currentTime.isBefore(endOfDay) || currentTime.isSame(endOfDay)) {
|
||||||
|
times.push(currentTime.format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
currentTime = currentTime.add(intervalMinutes, 'minute');
|
||||||
|
|
||||||
|
// Jika waktu berikutnya melebihi akhir hari, break
|
||||||
|
if (currentTime.isAfter(endOfDay)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return times;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async (page = 1, pageSize = 10, showModal = false) => {
|
||||||
|
if (!plantSubSection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showModal) {
|
||||||
|
setIsLoadingModal(true); // Modal untuk fetch pertama
|
||||||
|
} else {
|
||||||
|
setIsLoadingTable(true); // Spin untuk pagination
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
plant_sub_section_id: plantSubSection,
|
||||||
|
from: formattedDateStart,
|
||||||
|
to: formattedDateEnd,
|
||||||
|
interval: periode,
|
||||||
|
page: 1,
|
||||||
|
limit: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pivotResponse = await getAllHistoryValueReportPivot(params);
|
||||||
|
const valueReportResponse = await getAllHistoryValueReportPivot(params);
|
||||||
|
|
||||||
|
if (pivotResponse && pivotResponse.data) {
|
||||||
|
console.log('API Pivot Response:', pivotResponse);
|
||||||
|
console.log('First row pivot data:', pivotResponse.data[0]);
|
||||||
|
|
||||||
|
setPivotData(pivotResponse.data);
|
||||||
|
|
||||||
|
if (valueReportResponse && valueReportResponse.data) {
|
||||||
|
console.log('API Value Report Response:', valueReportResponse);
|
||||||
|
setValueReportData(valueReportResponse.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpivotedData = [];
|
||||||
|
|
||||||
|
pivotResponse.data.forEach((row) => {
|
||||||
|
const tagName = row.id;
|
||||||
|
const dataPoints = row.data || [];
|
||||||
|
|
||||||
|
dataPoints.forEach((item) => {
|
||||||
|
if (item && typeof item === 'object' && 'x' in item && 'y' in item) {
|
||||||
|
unpivotedData.push({
|
||||||
|
datetime: item.x,
|
||||||
|
tagName: tagName,
|
||||||
|
value: item.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Unpivoted data sample:', unpivotedData.slice(0, 10));
|
||||||
|
console.log('Total unpivoted rows:', unpivotedData.length);
|
||||||
|
|
||||||
|
unpivotedData.sort((a, b) => {
|
||||||
|
if (a.tagName !== b.tagName) {
|
||||||
|
return a.tagName.localeCompare(b.tagName);
|
||||||
|
}
|
||||||
|
return new Date(a.datetime) - new Date(b.datetime);
|
||||||
|
});
|
||||||
|
|
||||||
|
const transformedData = unpivotedData.map((item, index) => ({
|
||||||
|
key: index,
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const total = transformedData.length;
|
||||||
|
const startIndex = (page - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const paginatedData = transformedData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
setTableData(paginatedData);
|
||||||
|
setPagination({
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
} finally {
|
||||||
|
if (showModal) {
|
||||||
|
setIsLoadingModal(false);
|
||||||
|
} else {
|
||||||
|
setIsLoadingTable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
fetchData(pagination.current, pagination.pageSize, false);
|
||||||
};
|
};
|
||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
fetchData(1, pagination.pageSize, true);
|
||||||
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(0);
|
setPlantSubSection(0);
|
||||||
setStartDate(dateNow);
|
setStartDate(dateNow);
|
||||||
setEndDate(dateNow);
|
setEndDate(dateNow);
|
||||||
setPeriode(5);
|
setPeriode(30);
|
||||||
|
setTableData([]);
|
||||||
|
setPivotData([]);
|
||||||
|
setValueReportData([]);
|
||||||
|
setPagination({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
const getPlantSubSection = async () => {
|
||||||
@@ -104,8 +224,389 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
getPlantSubSection();
|
getPlantSubSection();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isWithinOneDay = startDate.isSame(endDate, 'day');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWithinOneDay && periode < 60) {
|
||||||
|
setPeriode(60);
|
||||||
|
}
|
||||||
|
}, [startDate, endDate, periode, isWithinOneDay]);
|
||||||
|
|
||||||
|
const periodeOptions = [
|
||||||
|
{ value: 5, label: '5 Minute', disabled: !isWithinOneDay },
|
||||||
|
{ value: 10, label: '10 Minute', disabled: !isWithinOneDay },
|
||||||
|
{ value: 30, label: '30 Minute', disabled: !isWithinOneDay },
|
||||||
|
{ value: 60, label: '1 Hour', disabled: false },
|
||||||
|
{ value: 120, label: '2 Hour', disabled: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const exportToPDF = async () => {
|
||||||
|
if (pivotData.length === 0) {
|
||||||
|
alert('No data to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagMapping = {};
|
||||||
|
valueReportData.forEach(item => {
|
||||||
|
if (item.tag_name && item.tag_number) {
|
||||||
|
tagMapping[item.tag_name] = item.tag_number;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSection = plantSubSectionList.find(item => item.plant_sub_section_id === plantSubSection);
|
||||||
|
const sectionName = selectedSection ? selectedSection.plant_sub_section_name : 'Unknown';
|
||||||
|
|
||||||
|
// Generate data lengkap untuk setiap tanggal dengan semua interval waktu
|
||||||
|
const dataByDate = {};
|
||||||
|
|
||||||
|
// Pertama, kumpulkan semua tanggal yang ada
|
||||||
|
const allDates = new Set();
|
||||||
|
pivotData.forEach(series => {
|
||||||
|
series.data.forEach(item => {
|
||||||
|
const date = dayjs(item.x).format('YYYY-MM-DD');
|
||||||
|
allDates.add(date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Untuk setiap tanggal, generate semua waktu dan isi dengan data yang ada
|
||||||
|
Array.from(allDates).forEach(date => {
|
||||||
|
const fullDayTimes = generateFullDayTimes(date, periode);
|
||||||
|
dataByDate[date] = {};
|
||||||
|
|
||||||
|
// Initialize dengan semua waktu
|
||||||
|
pivotData.forEach(series => {
|
||||||
|
dataByDate[date][series.id] = fullDayTimes.map(time => ({
|
||||||
|
x: time,
|
||||||
|
y: null // Default null, akan diisi jika ada data
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Isi dengan data aktual yang ada
|
||||||
|
pivotData.forEach(series => {
|
||||||
|
series.data.forEach(item => {
|
||||||
|
const itemDate = dayjs(item.x).format('YYYY-MM-DD');
|
||||||
|
if (itemDate === date) {
|
||||||
|
const itemTime = dayjs(item.x).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
const timeIndex = fullDayTimes.indexOf(itemTime);
|
||||||
|
if (timeIndex !== -1 && dataByDate[date][series.id][timeIndex]) {
|
||||||
|
dataByDate[date][series.id][timeIndex].y = item.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedDates = Object.keys(dataByDate).sort();
|
||||||
|
|
||||||
|
const loadImage = (src) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = src;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let logo1, logo2;
|
||||||
|
try {
|
||||||
|
logo1 = await loadImage('/assets/pupuk-indonesia-2.jpg');
|
||||||
|
logo2 = await loadImage('/assets/pupuk-indonesia-1.png');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading logos:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = new jsPDF({ orientation: 'landscape' });
|
||||||
|
const pageWidth = doc.internal.pageSize.width;
|
||||||
|
const pageHeight = doc.internal.pageSize.height;
|
||||||
|
const marginLeft = 10;
|
||||||
|
const marginRight = 10;
|
||||||
|
const tableWidth = pageWidth - marginLeft - marginRight;
|
||||||
|
|
||||||
|
// Konstanta untuk mengatur lebar kolom
|
||||||
|
const IO_TAG_COLUMN_WIDTH = 17; // Lebar kolom IO Soft Tag di TABEL (mm)
|
||||||
|
const HEADER_LEFT_COLUMN_WIDTH = 40; // Lebar kolom KIRI di HEADER (untuk logo1) (mm)
|
||||||
|
const MAX_TIME_COLUMNS_PER_PAGE = 24;
|
||||||
|
|
||||||
|
let globalPageNumber = 1;
|
||||||
|
|
||||||
|
// FUNGSI HEADER LENGKAP - Untuk Halaman Pertama
|
||||||
|
const drawFullHeader = (doc, date) => {
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.line(marginLeft, 10, marginLeft + tableWidth, 10);
|
||||||
|
doc.line(marginLeft, 10, marginLeft, 50);
|
||||||
|
doc.line(marginLeft + tableWidth, 10, marginLeft + tableWidth, 50);
|
||||||
|
|
||||||
|
const col1Width = HEADER_LEFT_COLUMN_WIDTH; // Gunakan konstanta HEADER
|
||||||
|
const col3Width = tableWidth * 0.20;
|
||||||
|
const col2Width = tableWidth - col1Width - col3Width;
|
||||||
|
|
||||||
|
doc.line(marginLeft + col1Width, 10, marginLeft + col1Width, 30);
|
||||||
|
doc.line(marginLeft + tableWidth - col3Width, 10, marginLeft + tableWidth - col3Width, 30);
|
||||||
|
doc.line(marginLeft, 30, marginLeft + tableWidth, 30);
|
||||||
|
|
||||||
|
if (logo1) {
|
||||||
|
const maxLogoHeight = 18;
|
||||||
|
const maxLogoWidth = col1Width - 4;
|
||||||
|
const logoAspectRatio = logo1.width / logo1.height;
|
||||||
|
let logoWidth, logoHeight;
|
||||||
|
|
||||||
|
if (logoAspectRatio > (maxLogoWidth / maxLogoHeight)) {
|
||||||
|
logoWidth = maxLogoWidth;
|
||||||
|
logoHeight = logoWidth / logoAspectRatio;
|
||||||
|
} else {
|
||||||
|
logoHeight = maxLogoHeight;
|
||||||
|
logoWidth = logoHeight * logoAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoX = marginLeft + (col1Width - logoWidth) / 2;
|
||||||
|
const logoY = 10 + (20 - logoHeight) / 2;
|
||||||
|
|
||||||
|
doc.addImage(logo1, 'JPEG', logoX, logoY, logoWidth, logoHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.setFontSize(12);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('PT. PUPUK INDONESIA UTILITAS', marginLeft + col1Width + col2Width / 2, 17, { align: 'center' });
|
||||||
|
doc.line(marginLeft + col1Width, 21, marginLeft + tableWidth - col3Width, 21);
|
||||||
|
doc.setFontSize(11);
|
||||||
|
doc.text('GRESIK GAS COGENERATION PLANT', marginLeft + col1Width + col2Width / 2, 27, { align: 'center' });
|
||||||
|
|
||||||
|
if (logo2) {
|
||||||
|
const maxLogoHeight = 18;
|
||||||
|
const maxLogoWidth = col3Width - 4;
|
||||||
|
const logoAspectRatio = logo2.width / logo2.height;
|
||||||
|
let logoWidth, logoHeight;
|
||||||
|
|
||||||
|
if (logoAspectRatio > (maxLogoWidth / maxLogoHeight)) {
|
||||||
|
logoWidth = maxLogoWidth;
|
||||||
|
logoHeight = logoWidth / logoAspectRatio;
|
||||||
|
} else {
|
||||||
|
logoHeight = maxLogoHeight;
|
||||||
|
logoWidth = logoHeight * logoAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoX = marginLeft + tableWidth - col3Width + (col3Width - logoWidth) / 2;
|
||||||
|
const logoY = 10 + (20 - logoHeight) / 2;
|
||||||
|
|
||||||
|
doc.addImage(logo2, 'PNG', logoX, logoY, logoWidth, logoHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.line(marginLeft + tableWidth - col3Width, 30, marginLeft + tableWidth - col3Width, 50);
|
||||||
|
|
||||||
|
const formattedDate = dayjs(date).format('DD-MM-YYYY');
|
||||||
|
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Laporan Periode :', marginLeft + tableWidth - col3Width / 2, 35, { align: 'center' });
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(formattedDate, marginLeft + tableWidth - col3Width / 2, 40, { align: 'center' });
|
||||||
|
|
||||||
|
// Hitung waktu akhir berdasarkan periode
|
||||||
|
const minutesInDay = 24 * 60;
|
||||||
|
const lastTimeMinutes = Math.floor((minutesInDay - periode) / periode) * periode;
|
||||||
|
const lastHour = Math.floor(lastTimeMinutes / 60);
|
||||||
|
const lastMinute = lastTimeMinutes % 60;
|
||||||
|
const endTime = `${String(lastHour).padStart(2, '0')}:${String(lastMinute).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.text(`00:00 - ${endTime}`, marginLeft + tableWidth - col3Width / 2, 45, { align: 'center' });
|
||||||
|
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.text(`Plant Section : ${sectionName}`, marginLeft + col1Width + col2Width / 2, 41, { align: 'center' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// FUNGSI HEADER SEDERHANA - Untuk Halaman Selanjutnya
|
||||||
|
const drawSimpleHeader = (doc, date) => {
|
||||||
|
const formattedDate = dayjs(date).format('DD-MM-YYYY');
|
||||||
|
|
||||||
|
// Gunakan struktur kolom yang SAMA seperti header lengkap
|
||||||
|
const col1Width = HEADER_LEFT_COLUMN_WIDTH; // Gunakan konstanta HEADER
|
||||||
|
const col3Width = tableWidth * 0.20;
|
||||||
|
const col2Width = tableWidth - col1Width - col3Width;
|
||||||
|
|
||||||
|
// Border luar
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.line(marginLeft, 10, marginLeft + tableWidth, 10);
|
||||||
|
doc.line(marginLeft, 10, marginLeft, 30);
|
||||||
|
doc.line(marginLeft + tableWidth, 10, marginLeft + tableWidth, 30);
|
||||||
|
doc.line(marginLeft, 30, marginLeft + tableWidth, 30);
|
||||||
|
|
||||||
|
// Garis vertikal pemisah kolom
|
||||||
|
doc.line(marginLeft + tableWidth - col3Width, 10, marginLeft + tableWidth - col3Width, 30);
|
||||||
|
|
||||||
|
// Kolom Kiri (KOSONG - tidak ada isi)
|
||||||
|
|
||||||
|
// Kolom Tengah - Plant Section
|
||||||
|
doc.setFontSize(10);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text(`Plant Section : ${sectionName}`, marginLeft + col1Width + col2Width / 2, 20, { align: 'center' });
|
||||||
|
|
||||||
|
// Kolom Kanan - Laporan Periode + Time Range
|
||||||
|
doc.setFontSize(9);
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
|
doc.text('Laporan Periode :', marginLeft + tableWidth - col3Width / 2, 14, { align: 'center' });
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(formattedDate, marginLeft + tableWidth - col3Width / 2, 19, { align: 'center' });
|
||||||
|
|
||||||
|
// Hitung waktu akhir berdasarkan periode
|
||||||
|
const minutesInDay = 24 * 60;
|
||||||
|
const lastTimeMinutes = Math.floor((minutesInDay - periode) / periode) * periode;
|
||||||
|
const lastHour = Math.floor(lastTimeMinutes / 60);
|
||||||
|
const lastMinute = lastTimeMinutes % 60;
|
||||||
|
const endTime = `${String(lastHour).padStart(2, '0')}:${String(lastMinute).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
doc.text(`00:00 - ${endTime}`, marginLeft + tableWidth - col3Width / 2, 24, { align: 'center' });
|
||||||
|
};
|
||||||
|
|
||||||
|
sortedDates.forEach((date, dateIndex) => {
|
||||||
|
const dateData = dataByDate[date];
|
||||||
|
|
||||||
|
// Ambil semua waktu yang sudah di-generate lengkap
|
||||||
|
const allTimes = dateData[Object.keys(dateData)[0]].map(item => item.x);
|
||||||
|
const formattedTimes = allTimes.map(time => dayjs(time).format('HH:mm'));
|
||||||
|
|
||||||
|
const sortedTags = Object.keys(dateData).sort();
|
||||||
|
|
||||||
|
const totalTimeColumns = formattedTimes.length;
|
||||||
|
const totalPages = Math.ceil(totalTimeColumns / MAX_TIME_COLUMNS_PER_PAGE);
|
||||||
|
|
||||||
|
let totalPagesCount = 0;
|
||||||
|
sortedDates.forEach((date) => {
|
||||||
|
const dateData = dataByDate[date];
|
||||||
|
const totalTimeColumns = dateData[Object.keys(dateData)[0]].length;
|
||||||
|
const pagesForThisDate = Math.ceil(totalTimeColumns / MAX_TIME_COLUMNS_PER_PAGE);
|
||||||
|
totalPagesCount += pagesForThisDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let pageChunk = 0; pageChunk < totalPages; pageChunk++) {
|
||||||
|
if (dateIndex > 0 || pageChunk > 0) {
|
||||||
|
doc.addPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startColIndex = pageChunk * MAX_TIME_COLUMNS_PER_PAGE;
|
||||||
|
const endColIndex = Math.min(startColIndex + MAX_TIME_COLUMNS_PER_PAGE, totalTimeColumns);
|
||||||
|
const pageTimeColumns = formattedTimes.slice(startColIndex, endColIndex);
|
||||||
|
const pageRawTimes = allTimes.slice(startColIndex, endColIndex);
|
||||||
|
|
||||||
|
const isFirstPage = (dateIndex === 0 && pageChunk === 0);
|
||||||
|
|
||||||
|
// Gunakan header yang sesuai
|
||||||
|
if (isFirstPage) {
|
||||||
|
drawFullHeader(doc, date);
|
||||||
|
} else {
|
||||||
|
drawSimpleHeader(doc, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableWidthForTime = tableWidth - IO_TAG_COLUMN_WIDTH;
|
||||||
|
const TIME_COLUMN_WIDTH = availableWidthForTime / pageTimeColumns.length;
|
||||||
|
|
||||||
|
const headerRow = ['IO Soft Tag', ...pageTimeColumns];
|
||||||
|
|
||||||
|
const pdfRows = sortedTags.map(tagName => {
|
||||||
|
const tagIdentifier = tagMapping[tagName] || tagName;
|
||||||
|
const row = [tagIdentifier];
|
||||||
|
|
||||||
|
const tagData = dateData[tagName];
|
||||||
|
|
||||||
|
for (let i = startColIndex; i < endColIndex; i++) {
|
||||||
|
const dataPoint = tagData[i];
|
||||||
|
const val = dataPoint && dataPoint.y;
|
||||||
|
row.push(val !== undefined && val !== null ? Number(val).toFixed(2) : '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeColumnStyles = {};
|
||||||
|
for (let i = 0; i < pageTimeColumns.length; i++) {
|
||||||
|
timeColumnStyles[i + 1] = {
|
||||||
|
cellWidth: TIME_COLUMN_WIDTH,
|
||||||
|
minCellWidht: 10,
|
||||||
|
halign: 'center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
autoTable(doc, {
|
||||||
|
head: [headerRow],
|
||||||
|
body: pdfRows,
|
||||||
|
startY: isFirstPage ? 50 : 30, // Sesuaikan startY untuk header sederhana (sama tinggi dengan header lengkap)
|
||||||
|
theme: 'grid',
|
||||||
|
styles: {
|
||||||
|
fontSize: 5,
|
||||||
|
cellPadding: 1,
|
||||||
|
minCellHeight: 10,
|
||||||
|
lineColor: [0, 0, 0],
|
||||||
|
lineWidth: 0.1,
|
||||||
|
halign: 'center',
|
||||||
|
valign: 'middle',
|
||||||
|
overflow: 'linebreak',
|
||||||
|
},
|
||||||
|
headStyles: {
|
||||||
|
fillColor: [255, 255, 255],
|
||||||
|
textColor: [0, 0, 0],
|
||||||
|
fontStyle: 'bold',
|
||||||
|
halign: 'center',
|
||||||
|
valign: 'middle',
|
||||||
|
lineColor: [0, 0, 0],
|
||||||
|
lineWidth: 0.3,
|
||||||
|
},
|
||||||
|
columnStyles: {
|
||||||
|
0: {
|
||||||
|
cellWidth: IO_TAG_COLUMN_WIDTH,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
halign: 'center',
|
||||||
|
valign: 'middle'
|
||||||
|
},
|
||||||
|
...timeColumnStyles
|
||||||
|
},
|
||||||
|
margin: { left: marginLeft, right: marginRight },
|
||||||
|
tableWidth: tableWidth,
|
||||||
|
pageBreak: 'auto',
|
||||||
|
didDrawPage: (data) => {
|
||||||
|
doc.setFontSize(8);
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.text(
|
||||||
|
`Page ${globalPageNumber} of ${totalPagesCount}`,
|
||||||
|
doc.internal.pageSize.width / 2,
|
||||||
|
doc.internal.pageSize.height - 10,
|
||||||
|
{ align: 'center' }
|
||||||
|
);
|
||||||
|
globalPageNumber++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.save(`Report_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}_ByDate.pdf`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
<Modal
|
||||||
|
open={isLoadingModal}
|
||||||
|
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 report data...
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
@@ -167,14 +668,8 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
value={periode}
|
value={periode}
|
||||||
onChange={setPeriode}
|
onChange={setPeriode}
|
||||||
style={{ width: '100%', marginTop: '4px' }}
|
style={{ width: '100%', marginTop: '4px' }}
|
||||||
options={[
|
options={periodeOptions}
|
||||||
{ value: 5, label: '5 Minute' },
|
/>
|
||||||
{ value: 10, label: '10 Minute' },
|
|
||||||
{ value: 30, label: '30 Minute' },
|
|
||||||
{ value: 60, label: '1 Hour' },
|
|
||||||
{ value: 120, label: '2 Hour' },
|
|
||||||
]}
|
|
||||||
></Select>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -185,10 +680,21 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
danger
|
danger
|
||||||
icon={<FileTextOutlined />}
|
icon={<FileTextOutlined />}
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
|
disabled={false}
|
||||||
>
|
>
|
||||||
Show
|
Show
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={exportToPDF}
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
Export PDF
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
@@ -199,18 +705,23 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
<Col xs={24} style={{ marginTop: '16px' }}>
|
||||||
<TableList
|
<Spin spinning={isLoadingTable}>
|
||||||
firstLoad={false}
|
<Table
|
||||||
mobile
|
columns={columns}
|
||||||
cardColor={'#d38943ff'}
|
dataSource={tableData}
|
||||||
header={'datetime'}
|
pagination={{
|
||||||
getData={getAllHistoryValueReportPivot}
|
...pagination,
|
||||||
queryParams={formDataFilter}
|
showSizeChanger: true,
|
||||||
columns={columns}
|
showTotal: (total) => `Total ${total} data`,
|
||||||
columnDynamic={'columns'}
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
triger={trigerFilter}
|
}}
|
||||||
/>
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 'max-content', y: 500 }}
|
||||||
|
bordered
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -218,4 +729,4 @@ const ListReport = memo(function ListReport(props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ListReport;
|
export default ListReport;
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import React, { memo, useState, useEffect } from 'react';
|
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 dayjs from 'dayjs';
|
||||||
import { FileTextOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { ResponsiveLine } from '@nivo/line';
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer
|
||||||
|
} from 'recharts';
|
||||||
import './trending.css';
|
import './trending.css';
|
||||||
import { getAllPlantSection } from '../../../api/master-plant-section';
|
import { getAllPlantSection } from '../../../api/master-plant-section';
|
||||||
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
|
import { getAllHistoryValueTrendingPivot } from '../../../api/history-value';
|
||||||
@@ -18,6 +27,7 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
const [startDate, setStartDate] = useState(dateNow);
|
const [startDate, setStartDate] = useState(dateNow);
|
||||||
const [endDate, setEndDate] = useState(dateNow);
|
const [endDate, setEndDate] = useState(dateNow);
|
||||||
const [periode, setPeriode] = useState(60);
|
const [periode, setPeriode] = useState(60);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const defaultFilter = {
|
const defaultFilter = {
|
||||||
criteria: '',
|
criteria: '',
|
||||||
@@ -29,51 +39,83 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
const [formDataFilter, setFormDataFilter] = useState(defaultFilter);
|
||||||
|
|
||||||
const [trendingValue, setTrendingValue] = useState([]);
|
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 handleSearch = async () => {
|
||||||
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
setIsLoading(true);
|
||||||
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
|
||||||
|
try {
|
||||||
|
const formattedDateStart = startDate.format('YYYY-MM-DD');
|
||||||
|
const formattedDateEnd = endDate.format('YYYY-MM-DD');
|
||||||
|
|
||||||
const newFilter = {
|
const newFilter = {
|
||||||
criteria: '',
|
criteria: '',
|
||||||
plant_sub_section_id: plantSubSection,
|
plant_sub_section_id: plantSubSection,
|
||||||
from: formattedDateStart,
|
from: formattedDateStart,
|
||||||
to: formattedDateEnd,
|
to: formattedDateEnd,
|
||||||
interval: periode,
|
interval: periode,
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormDataFilter(newFilter);
|
setFormDataFilter(newFilter);
|
||||||
|
|
||||||
const param = new URLSearchParams(newFilter);
|
const param = new URLSearchParams(newFilter);
|
||||||
const response = await getAllHistoryValueTrendingPivot(param);
|
const response = await getAllHistoryValueTrendingPivot(param);
|
||||||
|
|
||||||
if (response?.data?.length > 0) {
|
if (response?.data?.length > 0) {
|
||||||
// 🔹 Bersihkan dan format data agar aman untuk Nivo
|
transformDataForRecharts(response.data);
|
||||||
const cleanedData = response.data.map((serie) => ({
|
} else {
|
||||||
id: serie.id ?? 'Unknown',
|
setTrendingValue([]);
|
||||||
data: Array.isArray(serie.data)
|
setChartData([]);
|
||||||
? serie.data.map((d) => ({
|
setMetrics([]);
|
||||||
x: d?.x ?? null,
|
}
|
||||||
y:
|
} catch (error) {
|
||||||
d?.y !== null && d?.y !== undefined
|
console.error('Error fetching trending data:', error);
|
||||||
? Number(d.y).toFixed(4) // format 4 angka di belakang koma
|
} finally {
|
||||||
: null,
|
setIsLoading(false);
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTrendingValue(cleanedData);
|
|
||||||
} else {
|
|
||||||
// 🔹 Jika tidak ada data dari API
|
|
||||||
setTrendingValue([]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = () => {
|
const handleReset = () => {
|
||||||
setPlantSubSection(0);
|
setPlantSubSection(0);
|
||||||
setStartDate(dateNow);
|
setStartDate(dateNow);
|
||||||
setEndDate(dateNow);
|
setEndDate(dateNow);
|
||||||
setPeriode(5);
|
setPeriode(60);
|
||||||
|
setChartData([]);
|
||||||
|
setMetrics([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlantSubSection = async () => {
|
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(() => {
|
useEffect(() => {
|
||||||
getPlantSubSection();
|
getPlantSubSection();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<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>
|
<Card>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
@@ -162,10 +346,11 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
{ value: 60, label: '1 Hour' },
|
{ value: 60, label: '1 Hour' },
|
||||||
{ value: 120, label: '2 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
|
<Button
|
||||||
@@ -187,108 +372,9 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '16px' }}>
|
|
||||||
<div style={{ height: '500px', marginTop: '16px' }}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: '24px' }}>
|
||||||
{trendingValue && trendingValue.length > 0 ? (
|
{renderChart()}
|
||||||
<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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -296,4 +382,4 @@ const ReportTrending = memo(function ReportTrending(props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ReportTrending;
|
export default ReportTrending;
|
||||||
Reference in New Issue
Block a user