import React from 'react'; import { useTranslation } from 'react-i18next'; import { FaExternalLinkAlt } from 'react-icons/fa'; import { DEFAULT_REPORT_HOSPITAL, DEFAULT_CLUSTER_NAME, DEFAULT_HOSPITAL_LOGO, DEFAULT_CLUSTER_LOGO } from '../config/defaults'; // ── Props ───────────────────────────────────────────────────────────────────── interface PpmKpiReportCardProps { ppmKpiChartData: any; dashFromDate: string; /** Pre-rendered chart node from ModernDashboard (which owns GroupedBarChart) */ chartElement: React.ReactNode; onOpenDetail: (exportExcelFn: () => void) => void; } // ── Component ───────────────────────────────────────────────────────────────── const PpmKpiReportCard: React.FC = ({ ppmKpiChartData, dashFromDate, chartElement, onOpenDetail, }) => { const { t } = useTranslation(); const handleExportExcel = async () => { if (!ppmKpiChartData?.labels?.length) return; const MONTH_NAMES = [ 'JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE', 'JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER', ]; const now = dashFromDate ? new Date(dashFromDate + 'T00:00:00') : new Date(); const monthLabel = `${MONTH_NAMES[now.getMonth()]}-${now.getFullYear()}`; const labels: string[] = ppmKpiChartData.labels; const datasets: any[] = ppmKpiChartData.datasets || []; const getVal = (fieldName: string, idx: number): number => { const ds = datasets.find((d: any) => d.name === fieldName); return Number(ds?.values?.[idx]) || 0; }; // ── Draw chart ──────────────────────────────────────────────────────────── const chartBase64 = await new Promise((resolve) => { const canvas = document.createElement('canvas'); canvas.width = 900; canvas.height = 420; const ctx = canvas.getContext('2d')!; ctx.fillStyle = '#FCE8D8'; ctx.fillRect(0, 0, canvas.width, canvas.height); const DEPT_COLORS = [ '#4472C4','#ED7D31','#A9D18E','#7030A0', '#FF0000','#00B0F0','#92D050','#FFC000', ]; const xGroups = ['Total WOs', 'Pending', 'Completed']; const deptData = labels.map((lbl: string, i: number) => ({ label: (lbl || '').toUpperCase(), color: DEPT_COLORS[i % DEPT_COLORS.length], values: xGroups.map(g => getVal(g, i)), })); const paddingLeft = 60; const paddingRight = 20; const paddingTop = 50; const paddingBottom = 90; const chartW = canvas.width - paddingLeft - paddingRight; const chartH = canvas.height - paddingTop - paddingBottom; const allVals = deptData.flatMap(d => d.values); const maxVal = Math.max(...allVals, 1); const numGroups = xGroups.length; const groupWidth = chartW / numGroups; const numDepts = deptData.length; const barWidth = Math.min((groupWidth * 0.8) / numDepts, 30); const groupPad = (groupWidth - barWidth * numDepts) / 2; ctx.strokeStyle = '#D0D0D0'; ctx.lineWidth = 1; for (let i = 0; i <= 5; i++) { const y = paddingTop + chartH - (i / 5) * chartH; ctx.beginPath(); ctx.moveTo(paddingLeft, y); ctx.lineTo(paddingLeft + chartW, y); ctx.stroke(); ctx.fillStyle = '#555555'; ctx.font = '11px Arial'; ctx.textAlign = 'right'; ctx.fillText(Math.round((maxVal * i) / 5).toString(), paddingLeft - 6, y + 4); } xGroups.forEach((group, grpIdx) => { deptData.forEach((dept, deptIdx) => { const val = dept.values[grpIdx]; const barH = maxVal > 0 ? (val / maxVal) * chartH : 0; const x = paddingLeft + grpIdx * groupWidth + groupPad + deptIdx * barWidth; const y = paddingTop + chartH - barH; ctx.fillStyle = dept.color; ctx.fillRect(x, y, barWidth - 1, barH); if (val > 0) { ctx.fillStyle = '#222222'; ctx.font = '8px Arial'; ctx.textAlign = 'center'; ctx.fillText(val.toString(), x + (barWidth - 1) / 2, y - 3); } }); }); ctx.fillStyle = '#333333'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; xGroups.forEach((group, i) => { const x = paddingLeft + i * groupWidth + groupWidth / 2; const y = paddingTop + chartH + 16; ctx.fillText(group, x, y); }); ctx.strokeStyle = '#999999'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(paddingLeft, paddingTop + chartH); ctx.lineTo(paddingLeft + chartW, paddingTop + chartH); ctx.stroke(); const legendY = paddingTop + chartH + 36; const itemW = 110; const perRow = Math.floor(chartW / itemW); deptData.forEach((dept, i) => { const row = Math.floor(i / perRow); const col = i % perRow; const lx = paddingLeft + col * itemW; const ly = legendY + row * 16; ctx.fillStyle = dept.color; ctx.fillRect(lx, ly - 9, 12, 12); ctx.fillStyle = '#333333'; ctx.font = '10px Arial'; ctx.textAlign = 'left'; const short = dept.label.length > 10 ? dept.label.substring(0, 9) + '…' : dept.label; ctx.fillText(short, lx + 16, ly); }); ctx.fillStyle = '#7030A0'; ctx.font = 'bold 14px Arial'; ctx.textAlign = 'center'; ctx.fillText(`PPM FOR ${monthLabel}`, canvas.width / 2, 28); resolve(canvas.toDataURL('image/png').split(',')[1]); }); // ── Load logos ──────────────────────────────────────────────────────────── const loadLogo = async (path: string): Promise => { try { const baseUrl = import.meta.env.BASE_URL || '/'; const url = `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`; const res = await fetch(url); if (!res.ok) return null; const buf = await res.arrayBuffer(); return btoa(String.fromCharCode(...new Uint8Array(buf))); } catch { return null; } }; const [logoLeft, logoRight] = await Promise.all([ loadLogo(DEFAULT_HOSPITAL_LOGO), loadLogo(DEFAULT_CLUSTER_LOGO), ]); // ── Build workbook ──────────────────────────────────────────────────────── const ExcelJS = (await import('exceljs')).default; const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('PPM KPI Report'); ws.columns = [ { width: 20 }, { width: 12 }, { width: 12 }, { width: 12 }, { width: 14 }, ]; const mkFill = (hex: string): ExcelJS.Fill => ({ type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF' + hex.replace('#', '') }, }); const mkFont = (bold = false, size = 10, color = '000000'): Partial => ({ name: 'Arial', bold, size, color: { argb: 'FF' + color }, }); const mkBorder = (style: ExcelJS.BorderStyle = 'thin'): Partial => ({ top: { style, color: { argb: 'FF000000' } }, bottom: { style, color: { argb: 'FF000000' } }, left: { style, color: { argb: 'FF000000' } }, right: { style, color: { argb: 'FF000000' } }, }); const mkAlign = ( h: ExcelJS.Alignment['horizontal'] = 'center', wrap = true ): Partial => ({ horizontal: h, vertical: 'middle', wrapText: wrap }); const styleCell = ( cell: ExcelJS.Cell, opts: { bold?: boolean; size?: number; color?: string; bg?: string; h?: ExcelJS.Alignment['horizontal']; wrap?: boolean; brd?: boolean; brdStyle?: ExcelJS.BorderStyle; } ) => { cell.font = mkFont(opts.bold, opts.size ?? 10, opts.color ?? '000000'); cell.alignment = mkAlign(opts.h ?? 'center', opts.wrap ?? true); if (opts.bg) cell.fill = mkFill(opts.bg); if (opts.brd !== false) cell.border = mkBorder(opts.brdStyle ?? 'thin'); }; const TITLE_BG = '70AD47'; const HDR_BG = 'F2F2F2'; const ROW_YELLOW = 'FFFFC7'; const RATE_BG = 'D9B3FF'; const TOTAL_BG = 'D9B3FF'; ws.getRow(1).height = 30; ws.getRow(2).height = 20; ws.getRow(3).height = 16; ws.mergeCells('A1:B3'); ws.mergeCells('C1:C3'); ws.mergeCells('D1:E3'); if (logoLeft) { const id = wb.addImage({ base64: logoLeft, extension: 'png' }); ws.addImage(id, { tl: { col: 0, row: 0 }, br: { col: 2, row: 3 }, editAs: 'oneCell' }); } else { const c = ws.getCell('A1'); c.value = DEFAULT_REPORT_HOSPITAL; styleCell(c, { bold: true, size: 10, h: 'center', brd: false }); } if (logoRight) { const id = wb.addImage({ base64: logoRight, extension: 'png' }); ws.addImage(id, { tl: { col: 3, row: 0 }, br: { col: 5, row: 3 }, editAs: 'oneCell' }); } else { const c = ws.getCell('D1'); c.value = DEFAULT_CLUSTER_NAME; styleCell(c, { bold: true, size: 10, h: 'center', brd: false }); } ws.getRow(4).height = 6; ws.mergeCells('A5:E5'); const titleCell = ws.getCell('A5'); titleCell.value = `PPM JOB ORDERS KPI REPORT FOR THE MONTH OF ${monthLabel}`; styleCell(titleCell, { bold: true, size: 11, color: 'FFFFFF', bg: TITLE_BG, h: 'center' }); ws.getRow(5).height = 24; ws.getRow(6).height = 6; const HDR_LABELS = ['DEPARTMENT', 'Totl Wos', 'PENDING', 'COMPLETED', 'Completion\nRate']; HDR_LABELS.forEach((lbl, i) => { const cell = ws.getRow(7).getCell(i + 1); cell.value = lbl; if (i === 4) { cell.fill = mkFill(RATE_BG); cell.font = mkFont(true, 10, '000000'); cell.alignment = mkAlign('center', true); cell.border = mkBorder(); } else { styleCell(cell, { bold: true, size: 10, bg: HDR_BG, h: 'center' }); } }); ws.getRow(7).height = 28; const DATA_START = 8; labels.forEach((label: string, idx: number) => { const rowNum = DATA_START + idx; const altRow = idx % 2 === 0; const bg = altRow ? ROW_YELLOW : 'FFFFFF'; const row = ws.getRow(rowNum); row.height = 18; const totalWOs_ = getVal('Total WOs', idx); const completed_ = getVal('Completed', idx); const pending_ = getVal('Pending', idx); const rate_ = totalWOs_ > 0 ? parseFloat(((completed_ / totalWOs_) * 100).toFixed(0)) : 0; const rowData = [ (label || '').toUpperCase(), totalWOs_, pending_, completed_, rate_ > 0 ? `${rate_}%` : '0%', ]; rowData.forEach((val, colIdx) => { const cell = row.getCell(colIdx + 1); cell.value = val; const cellBg = colIdx === 4 ? RATE_BG : bg; styleCell(cell, { bold: colIdx === 4, size: 10, bg: cellBg, h: 'center' }); }); }); const totalRowNum = DATA_START + labels.length; const totalRow = ws.getRow(totalRowNum); totalRow.height = 20; const grandTotal = labels.reduce((s: number, _: string, i: number) => s + getVal('Total WOs', i), 0); const grandComp = labels.reduce((s: number, _: string, i: number) => s + getVal('Completed', i), 0); const grandPend = labels.reduce((s: number, _: string, i: number) => s + getVal('Pending', i), 0); const grandRate = grandTotal > 0 ? `${((grandComp / grandTotal) * 100).toFixed(0)}%` : '0%'; ['TOTAL', grandTotal, grandPend, grandComp, grandRate].forEach((val, colIdx) => { const cell = totalRow.getCell(colIdx + 1); cell.value = val; styleCell(cell, { bold: true, size: 10, bg: TOTAL_BG, h: 'center', brdStyle: 'medium' }); }); const sepRow = totalRowNum + 1; ws.getRow(sepRow).height = 8; const chartTitleRow = sepRow + 1; ws.mergeCells(`A${chartTitleRow}:E${chartTitleRow}`); const ctCell = ws.getCell(`A${chartTitleRow}`); ctCell.value = `PPM FOR ${monthLabel}`; styleCell(ctCell, { bold: true, size: 12, color: '7030A0', bg: 'F8E4FF', h: 'center', brd: false }); ws.getRow(chartTitleRow).height = 22; const imageId = wb.addImage({ base64: chartBase64, extension: 'png' }); ws.addImage(imageId, { tl: { col: 0, row: chartTitleRow }, br: { col: 5, row: chartTitleRow + 22 }, editAs: 'oneCell', }); ws.views = [{ state: 'frozen', xSplit: 1, ySplit: 7 }]; ws.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1 }; const buffer = await wb.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `PPM_KPI_Report_${monthLabel}.xlsx`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; return (

PPM KPI Report

{!ppmKpiChartData || !ppmKpiChartData.datasets?.length ? (
No chart data available
) : ( chartElement )}
); }; export default PpmKpiReportCard; export { PpmKpiReportCard }; export type { PpmKpiReportCardProps };