From 450873895882c9af7f6ac1b67e8cd558c50020dd Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 22 Dec 2025 18:47:42 +0700 Subject: [PATCH 1/6] feat: implement log history fetching and display in ListNotification component --- .../component/ListNotification.jsx | 717 ++++++++++-------- 1 file changed, 387 insertions(+), 330 deletions(-) diff --git a/src/pages/notification/component/ListNotification.jsx b/src/pages/notification/component/ListNotification.jsx index 3fc11c8..96cba0f 100644 --- a/src/pages/notification/component/ListNotification.jsx +++ b/src/pages/notification/component/ListNotification.jsx @@ -38,7 +38,7 @@ import { SearchOutlined, } from '@ant-design/icons'; import { useNavigate, Link as RouterLink } from 'react-router-dom'; -import { getAllNotification } from '../../../api/notification'; +import { getAllNotification, getNotificationLogByNotificationId } from '../../../api/notification'; const { Text, Paragraph, Link: AntdLink } = Typography; @@ -47,17 +47,18 @@ const transformNotificationData = (apiData) => { return apiData.map((item, index) => ({ id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical', - title: item.error_code?.error_code_name || item.device_name || 'Unknown Error', + title: item.device_name || 'Unknown Device', issue: item.error_code || item.error_code_name || 'Unknown Error', description: `${item.error_code} - ${item.error_code_name || ''}`, - timestamp: - item.created_at ? new Date(item.created_at).toLocaleString('id-ID', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) + ' WIB' : 'N/A', + timestamp: item.created_at + ? new Date(item.created_at).toLocaleString('id-ID', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + ' WIB' + : 'N/A', location: item.plant_sub_section_name || item.device_location || 'Location not specified', details: item.message_error_issue || 'No details available', link: `/verification-sparepart/${item.notification_error_id}`, // Dummy URL untuk verifikasi spare part @@ -68,7 +69,10 @@ const transformNotificationData = (apiData) => { errorCode: item.error_code, solutionName: item.error_code?.solution?.[0]?.solution_name || 'N/A', typeSolution: item.error_code?.solution?.[0]?.type_solution || 'N/A', - pathSolution: item.error_code?.solution?.[0]?.path_document || item.error_code?.solution?.[0]?.path_solution || 'N/A', + pathSolution: + item.error_code?.solution?.[0]?.path_document || + item.error_code?.solution?.[0]?.path_solution || + 'N/A', error_code: item.error_code, })); }; @@ -98,37 +102,6 @@ const userHistoryData = [ }, ]; -// Dummy data untuk log history -const logHistoryData = [ - { - id: 1, - timestamp: '04-11-2025 11:55 WIB', - addedBy: { - name: 'Budi Santoso', - phone: '081122334455', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, - { - id: 2, - timestamp: '04-11-2025 11:45 WIB', - addedBy: { - name: 'John Doe', - phone: '081234567890', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, - { - id: 3, - timestamp: '04-11-2025 11:40 WIB', - addedBy: { - name: 'Jane Smith', - phone: '087654321098', - }, - description: 'Suhu sudah coba diturunkan, namun masih belum mencapai treshold aman.', - }, -]; - const ListNotification = memo(function ListNotification(props) { const [notifications, setNotifications] = useState([]); const [activeTab, setActiveTab] = useState('all'); @@ -138,6 +111,8 @@ const ListNotification = memo(function ListNotification(props) { const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null const [isAddingLog, setIsAddingLog] = useState(false); const [selectedNotification, setSelectedNotification] = useState(null); + const [logHistoryData, setLogHistoryData] = useState([]); + const [logLoading, setLogLoading] = useState(false); const [pagination, setPagination] = useState({ current_page: 1, current_limit: 10, @@ -281,6 +256,40 @@ const ListNotification = memo(function ListNotification(props) { }); }; + // Fetch log history from API + const fetchLogHistory = async (notificationId) => { + try { + setLogLoading(true); + const response = await getNotificationLogByNotificationId(notificationId); + if (response && response.data) { + // Transform API data to component format + const transformedLogs = response.data.map((log) => ({ + id: log.notification_error_log_id, + timestamp: log.created_at + ? new Date(log.created_at).toLocaleString('id-ID', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + ' WIB' + : 'N/A', + addedBy: { + name: log.contact_name || 'Unknown', + phone: log.contact_phone || 'N/A', + }, + description: log.notification_error_log_description || '', + })); + setLogHistoryData(transformedLogs); + } + } catch (err) { + console.error('Error fetching log history:', err); + setLogHistoryData([]); // Set empty array on error + } finally { + setLogLoading(false); + } + }; + const tabButtonStyle = (isActive) => ({ padding: '12px 16px', border: 'none', @@ -500,6 +509,15 @@ const ListNotification = memo(function ListNotification(props) { }} onClick={(e) => { e.stopPropagation(); + + // Set the selected notification for the log history + const notificationId = + notification.id.split('-')[1]; + setSelectedNotification(notification); + + // Fetch log history for the selected notification + fetchLogHistory(notificationId); + setModalContent('log'); }} /> @@ -554,291 +572,314 @@ const ListNotification = memo(function ListNotification(props) { const renderLogHistory = () => ( <> -
- {/* Garis vertikal yang menyambung */} -
+ {logLoading ? ( +
+ +
+ ) : logHistoryData.length === 0 ? ( +
+ Tidak ada log history +
+ ) : ( +
+ {/* Garis vertikal yang menyambung */} +
- {logHistoryData.map((log, index) => ( - - {/* Kolom Kiri: Branch/Timeline */} - ( + -
- - - {/* Kolom Kanan: Card */} - - - - - - - - - Added at {log.timestamp} - - -
- Added by: {log.addedBy.name} - - {log.addedBy.phone} - -
-
- - - - {log.description} - - -
-
- -
- ))} -
- - ); - - const renderDetailsNotification = () => { - if (!selectedNotification) return null; - - const { IconComponent, color } = getIconAndColor(selectedNotification.type); - - return ( - - - {/* Kolom Kiri: Data Kompresor */} - - - - - -
+ + + {/* Kolom Kanan: Card */} + + + + + + + + + Added at {log.timestamp} + + +
+ Added by: {log.addedBy.name} + + {log.addedBy.phone} + +
+
+ + + - -
- - - {selectedNotification.title} -
- - {selectedNotification.issue} - -
+ {log.description} + -
- Plant Subsection -
{selectedNotification.subsection}
- - Date & Time - -
{selectedNotification.timestamp}
-
- -
- - - - Value - -
- N/A -
- - - - Treshold - -
N/A
- -
-
- - - - - {/* Kolom Kanan: Informasi Teknis */} - - - -
- PLC -
{selectedNotification.plc}
-
-
- Status -
- {selectedNotification.status} -
-
-
- Tag -
- {selectedNotification.tag} -
-
-
-
- - -
- - - - - - - Handling Guideline - - - - - - - - - - Spare Part - - - - - - setModalContent('log')} - > - - - - Log Activity - - - - - - - - PDF - - } > + ))} +
+ )} + + ); + + const renderDetailsNotification = () => { + if (!selectedNotification) return null; + + const { IconComponent, color } = getIconAndColor(selectedNotification.type); + + return ( + + + {/* Kolom Kiri: Data Kompresor */} + + + + + +
+ +
+ + + {selectedNotification.title} +
+ + {selectedNotification.issue} + +
+ +
+
+ Plant Subsection +
{selectedNotification.subsection}
+ + Date & Time + +
{selectedNotification.timestamp}
+
+ +
+ + + + Value + +
+ N/A +
+ + + + Treshold + +
N/A
+ +
+
+
+
+ + + {/* Kolom Kanan: Informasi Teknis */} + + + +
+ PLC +
{selectedNotification.plc}
+
+
+ Status +
+ {selectedNotification.status} +
+
+
+ Tag +
+ {selectedNotification.tag} +
+
+
+
+ +
+
+ + + + + + + Handling Guideline + + + + + + + + + + Spare Part + + + + + + { + // Set the selected notification for the log history if not already set + if (selectedNotification) { + const notificationId = + selectedNotification.id.split('-')[1]; + // Fetch log history for the selected notification + fetchLogHistory(notificationId); + } + setModalContent('log'); + }} + > + + + + Log Activity + + + + + + + + + + + PDF + + } + >
- {logHistoryData.map((log) => ( - + +
+ ) : logHistoryData.length === 0 ? ( +
- + ) : ( + logHistoryData.map((log) => ( + - {log.addedBy.name}:{' '} - {log.description} - - - {log.timestamp} - - - ))} + + {log.addedBy.name}:{' '} + {log.description} + + + {log.timestamp} + + + )) + )} From 978e020305befccb7902fd7893f126c303341b0b Mon Sep 17 00:00:00 2001 From: Rafiafrzl Date: Mon, 22 Dec 2025 20:49:48 +0700 Subject: [PATCH 2/6] feat: update notification data transformation and enhance user history display --- .../component/ListNotification.jsx | 3 +- .../IndexNotificationDetail.jsx | 111 +++++++++++++++--- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/pages/notification/component/ListNotification.jsx b/src/pages/notification/component/ListNotification.jsx index 96cba0f..a959cd9 100644 --- a/src/pages/notification/component/ListNotification.jsx +++ b/src/pages/notification/component/ListNotification.jsx @@ -47,7 +47,7 @@ const transformNotificationData = (apiData) => { return apiData.map((item, index) => ({ id: `notification-${item.notification_error_id}-${index}`, // Unique key prefix with array index type: item.is_read ? 'resolved' : item.is_delivered ? 'warning' : 'critical', - title: item.device_name || 'Unknown Device', + title: item.error_code_name || 'Unknown Error', issue: item.error_code || item.error_code_name || 'Unknown Error', description: `${item.error_code} - ${item.error_code_name || ''}`, timestamp: item.created_at @@ -324,7 +324,6 @@ const ListNotification = memo(function ListNotification(props) { borderColor: notification.isRead ? '#f0f0f0' : '#d6e4ff', cursor: 'pointer', }} - onClick={() => handleMarkAsRead(notification.id)} >
{ }; }; +// Dummy data baru untuk user history +const getDummyUsers = (notification) => { + if (!notification) return []; + return [ + { + id: '1', + name: 'John Doe', + phone: '081234567890', + status: 'delivered', + }, + { + id: '2', + name: 'Jane Smith', + phone: '082345678901', + status: 'sent', + }, + { + id: '3', + name: 'Bob Johnson', + phone: '083456789012', + status: 'failed', + }, + { + id: '4', + name: 'Alice Brown', + phone: '084567890123', + status: 'delivered', + }, + ]; +}; + +const getStatusTag = (status) => { + switch (status) { + case 'delivered': + return } color="success">Delivered; + case 'sent': + return } color="processing">Sent; + case 'failed': + return Failed; + default: + return {status}; + } +}; + const getIconAndColor = (type) => { switch (type) { case 'critical': @@ -262,14 +309,6 @@ const NotificationDetailTab = () => { Back to notification list - - -
{ - {/* Kolom Kanan: Log History */} + {/* Kolom Kanan: User History */} - + +
+ + {getDummyUsers(notification).map((user) => ( + + + + + } /> +
+ {user.name} +
+ + {user.phone} +
+
+
+ + + + {getStatusTag(user.status)} + + + +
+
+ ))} +
+
+
@@ -432,6 +510,7 @@ const NotificationDetailTab = () => { setModalContent('user')} > Date: Tue, 23 Dec 2025 02:20:16 +0700 Subject: [PATCH 3/6] update Menu Report --- .../report/report/component/ListReport.jsx | 222 +++++++++++++++++- src/pages/report/trending/ReportTrending.jsx | 19 +- 2 files changed, 230 insertions(+), 11 deletions(-) diff --git a/src/pages/report/report/component/ListReport.jsx b/src/pages/report/report/component/ListReport.jsx index 693a18e..f48f51e 100644 --- a/src/pages/report/report/component/ListReport.jsx +++ b/src/pages/report/report/component/ListReport.jsx @@ -9,6 +9,8 @@ import { import { getAllPlantSection } from '../../../../api/master-plant-section'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; +import ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; const { Text } = Typography; @@ -54,9 +56,9 @@ const ListReport = memo(function ListReport(props) { }; const fetchData = async (page = 1, pageSize = 10, showModal = false) => { - if (!plantSubSection) { - return; - } + // if (!plantSubSection) { + // return; + // } if (showModal) { setIsLoadingModal(true); @@ -195,8 +197,34 @@ const ListReport = memo(function ListReport(props) { fetchData(pagination.current, pagination.pageSize, false); }; - const handleSearch = () => { - fetchData(1, pagination.pageSize, true); + const handleSearch = async () => { + setIsLoadingModal(true); + + 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); + + // Jika response sukses, proses data + if (pivotResponse && pivotResponse.data) { + await fetchData(1, pagination.pageSize, false); + } + } catch (error) { + console.error('Error fetching data:', error); + // Error akan ditangkap oleh api-request.js dan muncul Swal otomatis + } finally { + setIsLoadingModal(false); + } }; const handleReset = () => { @@ -247,6 +275,168 @@ const ListReport = memo(function ListReport(props) { { value: 120, label: '2 Hour', disabled: false }, ]; + const exportToExcel = 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'; + + // Buat struktur pivot yang sama seperti di tabel + const timeMap = new Map(); + const tagSet = new Set(); + + pivotData.forEach((row) => { + const tagName = row.id; + tagSet.add(tagName); + + 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 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('Excel Pivot data:', pivotTableData.slice(0, 5)); + console.log('Total rows for Excel:', pivotTableData.length); + + const workbook = new ExcelJS.Workbook(); + const ws = workbook.addWorksheet('Pivot Report'); + + // Buat header info (3 baris pertama) + ws.addRow(['PT. PUPUK INDONESIA UTILITAS']); + ws.addRow(['GRESIK GAS COGENERATION PLANT']); + ws.addRow([`${sectionName}`]); + ws.addRow([]); // Baris kosong sebagai pemisah + + // Buat header kolom dengan tag number + const headerRow = [ + 'Datetime', + ...sortedTags.map(tag => tagMapping[tag] || tag) + ]; + ws.addRow(headerRow); + + // Buat data rows - PERBAIKAN: Simpan sebagai number murni + pivotTableData.forEach((rowData) => { + const row = [dayjs(rowData.datetime).format('DD-MM-YYYY HH:mm')]; + sortedTags.forEach((tagName) => { + const value = rowData[tagName]; + // Simpan sebagai number, bukan string + if (value !== undefined && value !== null) { + row.push(Number(value)); + } else { + row.push('-'); + } + }); + ws.addRow(row); + }); + + // Set column widths + ws.getColumn(1).width = 18; // Datetime column + for (let i = 2; i <= sortedTags.length + 1; i++) { + ws.getColumn(i).width = 12; // Tag columns + } + + // Merge cells untuk header info + const totalCols = sortedTags.length + 1; + ws.mergeCells(1, 1, 1, totalCols); // Baris 1 + ws.mergeCells(2, 1, 2, totalCols); // Baris 2 + ws.mergeCells(3, 1, 3, totalCols); // Baris 3 + + // Style untuk header info (3 baris pertama - bold dan center) + for (let i = 1; i <= 3; i++) { + const cell = ws.getCell(i, 1); + cell.font = { bold: true, size: 12 }; + cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; + } + + // Style untuk header kolom (bold, background color, center, border) + const headerRowIndex = 5; // Baris header + for (let col = 1; col <= totalCols; col++) { + const cell = ws.getCell(headerRowIndex, col); + cell.font = { bold: true, size: 11 }; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFDCDCDC' } + }; + cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; + cell.border = { + top: { style: 'thin', color: { argb: 'FF000000' } }, + bottom: { style: 'thin', color: { argb: 'FF000000' } }, + left: { style: 'thin', color: { argb: 'FF000000' } }, + right: { style: 'thin', color: { argb: 'FF000000' } } + }; + } + + // Style untuk data cells (border dan alignment) - PERBAIKAN: Format number dengan 2 desimal + for (let row = headerRowIndex + 1; row <= ws.rowCount; row++) { + for (let col = 1; col <= totalCols; col++) { + const cell = ws.getCell(row, col); + + cell.alignment = { + horizontal: 'center', + vertical: 'middle', + wrapText: true + }; + cell.border = { + top: { style: 'thin', color: { argb: 'FF000000' } }, + bottom: { style: 'thin', color: { argb: 'FF000000' } }, + left: { style: 'thin', color: { argb: 'FF000000' } }, + right: { style: 'thin', color: { argb: 'FF000000' } } + }; + + // Format number dengan 2 desimal untuk kolom value (kolom 2 dst) + if (col > 1) { + const cellValue = cell.value; + // Hanya set format number jika cell berisi angka + if (typeof cellValue === 'number') { + cell.numFmt = '0.00'; + } + } + } + } + + // Generate file name + const fileName = `Report_Pivot_${startDate.format('DD-MM-YYYY')}_to_${endDate.format('DD-MM-YYYY')}.xlsx`; + + // Save file + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + saveAs(blob, fileName); + }; + const exportToPDF = async () => { if (pivotData.length === 0) { alert('No data to export'); @@ -393,7 +583,7 @@ const ListReport = memo(function ListReport(props) { doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); - doc.text(`Plant Section : ${sectionName}`, marginLeft + col1Width + col2Width / 2, 41, { align: 'center' }); + doc.text(`${sectionName}`, marginLeft + col1Width + col2Width / 2, 38, { align: 'center' }); }; // Hitung total kolom tag chunks @@ -534,7 +724,7 @@ const ListReport = memo(function ListReport(props) { autoTable(doc, { head: [headerRow], body: pdfRows, - startY: isFirstPage ? 50 : 15, + startY: isFirstPage ? 43 : 15, theme: 'grid', rowPageBreak: 'avoid', styles: { @@ -542,7 +732,7 @@ const ListReport = memo(function ListReport(props) { cellPadding: 1.5, minCellHeight: 8, lineColor: [0, 0, 0], - lineWidth: 0.1, + lineWidth: 0.5, halign: 'center', valign: 'middle', overflow: 'linebreak', @@ -554,7 +744,7 @@ const ListReport = memo(function ListReport(props) { halign: 'center', valign: 'middle', lineColor: [0, 0, 0], - lineWidth: 0.3, + lineWidth: 0.5, }, columnStyles: { 0: { @@ -694,11 +884,23 @@ const ListReport = memo(function ListReport(props) { type="primary" icon={} onClick={exportToPDF} - disabled={false} + disabled={pivotData.length === 0} + style={{ backgroundColor: '#1890ff', borderColor: '#1890ff' }} > Export PDF + + + - - + {!props.id && ( + + + + + + )} +
{ {/* Kolom Kanan: User History */} - -
- + +
+ {getDummyUsers(notification).map((user) => ( - + - } /> + } + />
{user.name} -
- - {user.phone} +
+ + + {user.phone} +
@@ -457,7 +512,9 @@ const NotificationDetailTab = () => { size="small" onClick={(e) => { e.stopPropagation(); - console.log(`Resend to ${user.name}`); + console.log( + `Resend to ${user.name}` + ); }} > Resend @@ -505,9 +562,7 @@ const NotificationDetailTab = () => { - + { style={{ width: '100%' }} > {notification.error_code?.solution && - notification.error_code.solution.length > 0 ? ( + notification.error_code.solution.length > 0 ? ( <> {notification.error_code.solution .filter((sol) => sol.is_active) // Hanya tampilkan solusi yang aktif @@ -607,7 +662,7 @@ const NotificationDetailTab = () => { ) : null} {sol.type_solution === 'text' && - sol.text_solution ? ( + sol.text_solution ? ( { style={{ width: '100%' }} > {notification.spareparts && - notification.spareparts.length > 0 ? ( + notification.spareparts.length > 0 ? ( notification.spareparts.map((sparepart, index) => ( { color: sparepart.sparepart_stok === 'Available' || - sparepart.sparepart_stok === + sparepart.sparepart_stok === 'available' ? '#52c41a' : '#ff4d4f', @@ -797,7 +852,9 @@ const NotificationDetailTab = () => { rows={2} placeholder="Tuliskan update penanganan di sini..." value={newLogDescription} - onChange={(e) => setNewLogDescription(e.target.value)} + onChange={(e) => + setNewLogDescription(e.target.value) + } disabled={submitLoading} /> @@ -806,8 +863,18 @@ const NotificationDetailTab = () => { type={isAddingLog ? 'primary' : 'dashed'} size="small" block - icon={submitLoading ? : (!isAddingLog && )} - onClick={isAddingLog ? handleSubmitLog : () => setIsAddingLog(true)} + icon={ + submitLoading ? ( + + ) : ( + !isAddingLog && + ) + } + onClick={ + isAddingLog + ? handleSubmitLog + : () => setIsAddingLog(true) + } loading={submitLoading} disabled={submitLoading} > @@ -855,7 +922,6 @@ const NotificationDetailTab = () => { - ); };