diff --git a/package.json b/package.json index 21bd8b0..e942371 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "exceljs": "^4.4.0", "file-saver": "^2.0.5", "html2canvas": "^1.4.1", - "jspdf": "^3.0.1", + "jspdf": "^3.0.4", + "jspdf-autotable": "^5.0.2", "mqtt": "^5.14.0", "qrcode": "^1.5.4", "react": "^18.2.0", @@ -30,6 +31,7 @@ "react-icons": "^4.11.0", "react-router-dom": "^6.22.3", "react-svg": "^16.3.0", + "recharts": "^3.6.0", "sweetalert2": "^11.17.2" }, "devDependencies": { diff --git a/public/assets/pupuk-indonesia-1.png b/public/assets/pupuk-indonesia-1.png new file mode 100644 index 0000000..689669a Binary files /dev/null and b/public/assets/pupuk-indonesia-1.png differ diff --git a/public/assets/pupuk-indonesia-2.jpg b/public/assets/pupuk-indonesia-2.jpg new file mode 100644 index 0000000..d1a5d2b Binary files /dev/null and b/public/assets/pupuk-indonesia-2.jpg differ diff --git a/src/pages/report/report/component/ListReport.jsx b/src/pages/report/report/component/ListReport.jsx index 16544cf..b9cf769 100644 --- a/src/pages/report/report/component/ListReport.jsx +++ b/src/pages/report/report/component/ListReport.jsx @@ -1,91 +1,211 @@ import React, { memo, useState, useEffect } from 'react'; -import { Button, Row, Col, Card, Input, DatePicker, Select, Typography } from 'antd'; -import TableList from '../../../../components/Global/TableList'; +import { Button, Row, Col, Card, DatePicker, Select, Typography, Table, Spin, Modal } from 'antd'; import dayjs from 'dayjs'; -import { FileTextOutlined } from '@ant-design/icons'; +import { FileTextOutlined, DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { - getAllHistoryValueReport, getAllHistoryValueReportPivot, + getAllHistoryValueReport, } from '../../../../api/history-value'; import { getAllPlantSection } from '../../../../api/master-plant-section'; +import jsPDF from 'jspdf'; +import autoTable from 'jspdf-autotable'; const { Text } = Typography; 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 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 [plantSubSectionList, setPlantSubSectionList] = useState([]); const [startDate, setStartDate] = useState(dateNow); const [endDate, setEndDate] = useState(dateNow); - const [periode, setPeriode] = useState(10); + const [periode, setPeriode] = useState(30); - const defaultFilter = { - criteria: '', - plant_sub_section_id: 0, - from: dateNowFormated, - to: dateNowFormated, - interval: periode, + const columns = [ + { + title: 'No', + key: 'no', + width: 60, + 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 formattedDateStart = startDate.format('YYYY-MM-DD'); - const formattedDateEnd = endDate.format('YYYY-MM-DD'); - - setFormDataFilter({ - criteria: '', - plant_sub_section_id: plantSubSection, - from: formattedDateStart, - to: formattedDateEnd, - interval: periode, - }); - setTrigerFilter((prev) => !prev); + fetchData(1, pagination.pageSize, true); }; const handleReset = () => { setPlantSubSection(0); setStartDate(dateNow); setEndDate(dateNow); - setPeriode(5); + setPeriode(30); + setTableData([]); + setPivotData([]); + setValueReportData([]); + setPagination({ + current: 1, + pageSize: 10, + total: 0, + }); }; const getPlantSubSection = async () => { @@ -104,8 +224,389 @@ const ListReport = memo(function ListReport(props) { 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 ( + + } + /> +
+ + Please Wait + + + System is generating report data... + +
+
+ @@ -167,14 +668,8 @@ const ListReport = memo(function ListReport(props) { value={periode} onChange={setPeriode} style={{ width: '100%', marginTop: '4px' }} - options={[ - { 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' }, - ]} - > + options={periodeOptions} + /> @@ -185,10 +680,21 @@ const ListReport = memo(function ListReport(props) { danger icon={} onClick={handleSearch} + disabled={false} > Show + + +