diff --git a/src/pages/report/report/component/ListReport.jsx b/src/pages/report/report/component/ListReport.jsx index b9cf769..693a18e 100644 --- a/src/pages/report/report/component/ListReport.jsx +++ b/src/pages/report/report/component/ListReport.jsx @@ -16,9 +16,10 @@ const ListReport = memo(function ListReport(props) { const dateNow = dayjs(); const dateNowFormated = dateNow.format('YYYY-MM-DD'); - const [isLoadingModal, setIsLoadingModal] = useState(false); // Modal loading - const [isLoadingTable, setIsLoadingTable] = useState(false); // Table loading + const [isLoadingModal, setIsLoadingModal] = useState(false); + const [isLoadingTable, setIsLoadingTable] = useState(false); const [tableData, setTableData] = useState([]); + const [columns, setColumns] = useState([]); const [pivotData, setPivotData] = useState([]); const [valueReportData, setValueReportData] = useState([]); const [pagination, setPagination] = useState({ @@ -33,46 +34,6 @@ const ListReport = memo(function ListReport(props) { const [endDate, setEndDate] = useState(dateNow); const [periode, setPeriode] = useState(30); - 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'); @@ -84,7 +45,6 @@ const ListReport = memo(function ListReport(props) { 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; } @@ -99,9 +59,9 @@ const ListReport = memo(function ListReport(props) { } if (showModal) { - setIsLoadingModal(true); // Modal untuk fetch pertama + setIsLoadingModal(true); } else { - setIsLoadingTable(true); // Spin untuk pagination + setIsLoadingTable(true); } try { const formattedDateStart = startDate.format('YYYY-MM-DD'); @@ -121,8 +81,6 @@ const ListReport = memo(function ListReport(props) { 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) { @@ -130,42 +88,90 @@ const ListReport = memo(function ListReport(props) { setValueReportData(valueReportResponse.data); } - const unpivotedData = []; + // Buat struktur pivot: waktu sebagai baris, tag sebagai kolom + const timeMap = new Map(); + const tagSet = new Set(); + // Kumpulkan semua waktu unik dan tag unik pivotResponse.data.forEach((row) => { const tagName = row.id; - const dataPoints = row.data || []; + tagSet.add(tagName); + 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, - }); + const datetime = item.x; + if (!timeMap.has(datetime)) { + timeMap.set(datetime, {}); + } + timeMap.get(datetime)[tagName] = item.y; } }); }); - console.log('Unpivoted data sample:', unpivotedData.slice(0, 10)); - console.log('Total unpivoted rows:', unpivotedData.length); + // Konversi ke array dan sort berdasarkan waktu + const sortedTimes = Array.from(timeMap.keys()).sort(); + const sortedTags = Array.from(tagSet).sort(); - unpivotedData.sort((a, b) => { - if (a.tagName !== b.tagName) { - return a.tagName.localeCompare(b.tagName); - } - return new Date(a.datetime) - new Date(b.datetime); + // Buat data untuk table + const pivotTableData = sortedTimes.map((datetime, index) => { + const rowData = { + key: index, + datetime: datetime, + }; + + sortedTags.forEach((tagName) => { + rowData[tagName] = timeMap.get(datetime)[tagName]; + }); + + return rowData; }); - const transformedData = unpivotedData.map((item, index) => ({ - key: index, - ...item, - })); + console.log('Pivot table data sample:', pivotTableData.slice(0, 5)); + console.log('Total pivot rows:', pivotTableData.length); - const total = transformedData.length; + // Buat kolom dinamis + const dynamicColumns = [ + { + title: 'No', + key: 'no', + width: 60, + align: 'center', + fixed: 'left', + render: (_, __, index) => { + return (page - 1) * pageSize + index + 1; + }, + }, + { + title: 'Datetime', + dataIndex: 'datetime', + key: 'datetime', + width: 180, + fixed: 'left', + sorter: (a, b) => new Date(a.datetime) - new Date(b.datetime), + }, + ...sortedTags.map((tagName) => ({ + title: tagName, + dataIndex: tagName, + key: tagName, + width: 120, + align: 'center', + render: (value) => { + if (value === null || value === undefined) { + return '-'; + } + return Number(value).toFixed(2); + }, + })), + ]; + + setColumns(dynamicColumns); + + // Pagination + const total = pivotTableData.length; const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; - const paginatedData = transformedData.slice(startIndex, endIndex); + const paginatedData = pivotTableData.slice(startIndex, endIndex); setTableData(paginatedData); setPagination({ @@ -199,6 +205,7 @@ const ListReport = memo(function ListReport(props) { setEndDate(dateNow); setPeriode(30); setTableData([]); + setColumns([]); setPivotData([]); setValueReportData([]); setPagination({ @@ -256,47 +263,43 @@ const ListReport = memo(function ListReport(props) { 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 = {}; + // Buat struktur pivot yang sama seperti di tabel + const timeMap = new Map(); + const tagSet = new Set(); - // 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); - }); - }); + pivotData.forEach((row) => { + const tagName = row.id; + tagSet.add(tagName); - // 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 dataPoints = row.data || []; + dataPoints.forEach((item) => { + if (item && typeof item === 'object' && 'x' in item && 'y' in item) { + const datetime = item.x; + if (!timeMap.has(datetime)) { + timeMap.set(datetime, {}); } - }); + timeMap.get(datetime)[tagName] = item.y; + } }); }); - const sortedDates = Object.keys(dataByDate).sort(); + const sortedTimes = Array.from(timeMap.keys()).sort(); + const sortedTags = Array.from(tagSet).sort(); + + const pivotTableData = sortedTimes.map((datetime) => { + const rowData = { + datetime: datetime, + }; + + sortedTags.forEach((tagName) => { + rowData[tagName] = timeMap.get(datetime)[tagName]; + }); + + return rowData; + }); + + console.log('PDF Pivot data:', pivotTableData.slice(0, 5)); + console.log('Total rows for PDF:', pivotTableData.length); const loadImage = (src) => { return new Promise((resolve, reject) => { @@ -322,21 +325,17 @@ const ListReport = memo(function ListReport(props) { 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; + const DATETIME_COLUMN_WIDTH = 25; + const HEADER_LEFT_COLUMN_WIDTH = 40; + const MAX_TAG_COLUMNS_PER_PAGE = 15; - let globalPageNumber = 1; - - // FUNGSI HEADER LENGKAP - Untuk Halaman Pertama - const drawFullHeader = (doc, date) => { + const drawFullHeader = (doc) => { 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 col1Width = HEADER_LEFT_COLUMN_WIDTH; const col3Width = tableWidth * 0.20; const col2Width = tableWidth - col1Width - col3Width; @@ -391,194 +390,199 @@ const ListReport = memo(function ListReport(props) { 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'); + // Hitung total kolom tag chunks + const totalTagColumns = sortedTags.length; + const totalTagChunks = Math.ceil(totalTagColumns / MAX_TAG_COLUMNS_PER_PAGE); - // 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; + // PERBAIKAN: Variabel untuk tracking total halaman yang sebenarnya + let actualTotalPages = 0; + const pageInfoArray = []; // Array untuk menyimpan info setiap page - // 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); + // Loop pertama: hitung dulu total halaman yang akan dibuat + for (let pageChunk = 0; pageChunk < totalTagChunks; pageChunk++) { + const startTagIndex = pageChunk * MAX_TAG_COLUMNS_PER_PAGE; + const endTagIndex = Math.min(startTagIndex + MAX_TAG_COLUMNS_PER_PAGE, totalTagColumns); + const pageTagColumns = sortedTags.slice(startTagIndex, endTagIndex); + const isFirstPage = (pageChunk === 0); - // Garis vertikal pemisah kolom - doc.line(marginLeft + tableWidth - col3Width, 10, marginLeft + tableWidth - col3Width, 30); + // Simulasi autoTable untuk menghitung jumlah halaman + const tempDoc = new jsPDF({ orientation: 'landscape' }); + const headerRow = ['Datetime', ...pageTagColumns.map(tag => tagMapping[tag] || tag)]; - // 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; + const pdfRows = pivotTableData.map((rowData) => { + const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')]; + pageTagColumns.forEach((tagName) => { + const value = rowData[tagName]; + row.push(value !== undefined && value !== null ? Number(value).toFixed(2) : '-'); + }); + return row; }); - for (let pageChunk = 0; pageChunk < totalPages; pageChunk++) { - if (dateIndex > 0 || pageChunk > 0) { - doc.addPage(); - } + const availableWidthForTags = tableWidth - DATETIME_COLUMN_WIDTH; + const TAG_COLUMN_WIDTH = availableWidthForTags / pageTagColumns.length; - 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 tagColumnStyles = {}; + for (let i = 0; i < pageTagColumns.length; i++) { + tagColumnStyles[i + 1] = { + cellWidth: TAG_COLUMN_WIDTH, + halign: 'center' + }; + } - const isFirstPage = (dateIndex === 0 && pageChunk === 0); + let pagesForThisChunk = 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], + autoTable(tempDoc, { + head: [headerRow], + body: pdfRows, + startY: isFirstPage ? 50 : 15, + theme: 'grid', + rowPageBreak: 'avoid', + styles: { + fontSize: 7, + cellPadding: 1.5, + minCellHeight: 8, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center', + valign: 'middle', + overflow: 'linebreak', + }, + headStyles: { + fillColor: [220, 220, 220], + textColor: [0, 0, 0], + fontStyle: 'bold', + halign: 'center', + valign: 'middle', + lineColor: [0, 0, 0], + lineWidth: 0.3, + }, + columnStyles: { + 0: { + cellWidth: DATETIME_COLUMN_WIDTH, fontStyle: 'bold', halign: 'center', - valign: 'middle', - lineColor: [0, 0, 0], - lineWidth: 0.3, + valign: 'middle' }, - 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++; - }, - }); - } - }); + ...tagColumnStyles + }, + margin: { left: marginLeft, right: marginRight, top: 15 }, + tableWidth: tableWidth, + pageBreak: 'auto', + didDrawPage: () => { + pagesForThisChunk++; + } + }); - doc.save(`Report_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}_ByDate.pdf`); + pageInfoArray.push({ + chunkIndex: pageChunk, + pagesCount: pagesForThisChunk, + startPage: actualTotalPages + 1 + }); + + actualTotalPages += pagesForThisChunk; + } + + console.log('Total pages akan dibuat:', actualTotalPages); + + // Loop kedua: buat PDF yang sebenarnya dengan nomor halaman yang benar + let globalPageNumber = 1; + + for (let pageChunk = 0; pageChunk < totalTagChunks; pageChunk++) { + if (pageChunk > 0) { + doc.addPage(); + } + + const startTagIndex = pageChunk * MAX_TAG_COLUMNS_PER_PAGE; + const endTagIndex = Math.min(startTagIndex + MAX_TAG_COLUMNS_PER_PAGE, totalTagColumns); + const pageTagColumns = sortedTags.slice(startTagIndex, endTagIndex); + const isFirstPage = (pageChunk === 0); + + if (isFirstPage) { + drawFullHeader(doc); + } + + const headerRow = ['Datetime', ...pageTagColumns.map(tag => tagMapping[tag] || tag)]; + + const pdfRows = pivotTableData.map((rowData) => { + const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')]; + + pageTagColumns.forEach((tagName) => { + const value = rowData[tagName]; + row.push(value !== undefined && value !== null ? Number(value).toFixed(2) : '-'); + }); + + return row; + }); + + const availableWidthForTags = tableWidth - DATETIME_COLUMN_WIDTH; + const TAG_COLUMN_WIDTH = availableWidthForTags / pageTagColumns.length; + + const tagColumnStyles = {}; + for (let i = 0; i < pageTagColumns.length; i++) { + tagColumnStyles[i + 1] = { + cellWidth: TAG_COLUMN_WIDTH, + halign: 'center' + }; + } + + autoTable(doc, { + head: [headerRow], + body: pdfRows, + startY: isFirstPage ? 50 : 15, + theme: 'grid', + rowPageBreak: 'avoid', + styles: { + fontSize: 7, + cellPadding: 1.5, + minCellHeight: 8, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center', + valign: 'middle', + overflow: 'linebreak', + }, + headStyles: { + fillColor: [220, 220, 220], + textColor: [0, 0, 0], + fontStyle: 'bold', + halign: 'center', + valign: 'middle', + lineColor: [0, 0, 0], + lineWidth: 0.3, + }, + columnStyles: { + 0: { + cellWidth: DATETIME_COLUMN_WIDTH, + fontStyle: 'bold', + halign: 'center', + valign: 'middle' + }, + ...tagColumnStyles + }, + margin: { left: marginLeft, right: marginRight, top: 15 }, + tableWidth: tableWidth, + pageBreak: 'auto', + didDrawPage: (data) => { + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.text( + `Page ${globalPageNumber} of ${actualTotalPages}`, + doc.internal.pageSize.width / 2, + doc.internal.pageSize.height - 10, + { align: 'center' } + ); + globalPageNumber++; + }, + }); + } + + doc.save(`Report_Pivot_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}.pdf`); }; return ( @@ -707,20 +711,23 @@ const ListReport = memo(function ListReport(props) { - `Total ${total} data`, - pageSizeOptions: ['10', '20', '50', '100'], - }} - onChange={handleTableChange} - scroll={{ x: 'max-content', y: 500 }} - bordered - size="small" - /> +
+
`Total ${total} data`, + pageSizeOptions: ['10', '20', '50', '100'], + }} + onChange={handleTableChange} + scroll={{ x: 'max-content', y: 500 }} + bordered + size="small" + sticky + /> +