import { join } from "path"; import { readdirSync, readFileSync, existsSync } from "fs"; import { spawnSync } from "child_process"; import dayjs from "dayjs"; import ExcelJS from "exceljs"; import fs from "fs-extra"; const REPORTS_DIR = join(import.meta.dir, "reports"); const TEMPLATE_FILE = join(import.meta.dir, "templates", "workstation-summary.typ"); interface Assertion { passed: boolean; score: number; minScore: number; context: any; } interface Report { username: string; os: string; arch: string; assertions: Record; stats: { passed: number; failed: number; }; timestamps: { start: string; end: string; }; } interface EmployeeSummary { name: string; latestReport: Report; timestamp: string; } async function generateMonthlySummary(month: string) { const monthDir = join(REPORTS_DIR, month); if (!existsSync(monthDir)) { console.error(`Month directory not found: ${monthDir}`); return; } console.log(`Processing reports for month: ${month}...`); const employees = readdirSync(monthDir).filter(f => fs.statSync(join(monthDir, f)).isDirectory()); const summaryData: EmployeeSummary[] = []; for (const employeeName of employees) { const employeeDir = join(monthDir, employeeName); const files = readdirSync(employeeDir).filter(f => f.endsWith(".report.json")); if (files.length === 0) continue; // Find latest report by timestamp in filename YYMMDD-HHMMSS const sortedFiles = files.sort((a, b) => b.localeCompare(a)); const latestFile = sortedFiles[0]; const reportPath = join(employeeDir, latestFile); try { const report: Report = JSON.parse(readFileSync(reportPath, "utf-8")); summaryData.push({ name: employeeName, latestReport: report, timestamp: latestFile.split(".")[0] }); } catch (err) { console.error(`Failed to parse ${reportPath}:`, err); } } if (summaryData.length === 0) { console.log("No valid reports found."); return; } // 1. Generate Excel const excelPath = join(monthDir, `summary_${month}.xlsx`); await createExcelReport(summaryData, excelPath); // 2. Generate PDF const pdfPath = join(monthDir, `summary_${month}.pdf`); await createPdfReport(summaryData, month, pdfPath); } async function createExcelReport(data: EmployeeSummary[], path: string) { const workbook = new ExcelJS.Workbook(); const summarySheet = workbook.addWorksheet("Summary"); const matrixSheet = workbook.addWorksheet("Assertion Matrix"); // Summary Sheet summarySheet.columns = [ { header: "Employee", key: "name", width: 25 }, { header: "Last Audit", key: "timestamp", width: 20 }, { header: "OS", key: "os", width: 10 }, { header: "Passed", key: "passed", width: 10 }, { header: "Failed", key: "failed", width: 10 }, { header: "Score %", key: "scorePct", width: 12 }, ]; data.forEach(s => { const total = s.latestReport.stats.passed + s.latestReport.stats.failed; summarySheet.addRow({ name: s.name, timestamp: s.timestamp, os: s.latestReport.os, passed: s.latestReport.stats.passed, failed: s.latestReport.stats.failed, scorePct: ((s.latestReport.stats.passed / total) * 100).toFixed(1) + "%" }); }); // Matrix Sheet const allAssertionKeys = Array.from(new Set(data.flatMap(s => Object.keys(s.latestReport.assertions)))).sort(); matrixSheet.getRow(1).values = ["Employee", ...allAssertionKeys]; data.forEach(s => { const rowValues = [s.name]; allAssertionKeys.forEach(key => { const assertion = s.latestReport.assertions[key]; rowValues.push(assertion ? (assertion.passed ? "PASS" : "FAIL") : "N/A"); }); const row = matrixSheet.addRow(rowValues); // Color coding PASS/FAIL row.eachCell((cell, colNumber) => { if (colNumber === 1) return; // Skip Employee name if (cell.value === "PASS") { cell.font = { color: { argb: "FF006100" }, bold: true }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC6EFCE' } }; } else if (cell.value === "FAIL") { cell.font = { color: { argb: "FF9C0006" }, bold: true }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } }; } }); }); // Basic styling [summarySheet, matrixSheet].forEach(sheet => { sheet.getRow(1).font = { bold: true }; sheet.getRow(1).fill = { type: 'pattern', pattern:'solid', fgColor:{argb:'FFE0E0E0'} }; sheet.getRow(1).alignment = { horizontal: 'center' }; }); await workbook.xlsx.writeFile(path); console.log(`✅ Excel generated: ${path}`); } async function createPdfReport(data: EmployeeSummary[], month: string, path: string) { const allAssertionKeys = Array.from(new Set(data.flatMap(s => Object.keys(s.latestReport.assertions)))).sort(); const metadata = { title: "Workstation Compliance Summary", month: month, date: dayjs().format("YYYY-MM-DD"), timestamp: dayjs().format("YYYY-MM-DD HH:mm:ss"), employeeCount: data.length }; const processedData = { assertions: allAssertionKeys, employees: data.map(s => ({ name: s.name, passed: s.latestReport.stats.passed, failed: s.latestReport.stats.failed, results: allAssertionKeys.map(key => ({ key, passed: s.latestReport.assertions[key]?.passed ?? null })) })) }; const jsonTmpPath = join(REPORTS_DIR, month, ".summary.json"); fs.writeJsonSync(jsonTmpPath, processedData); const relativeJsonPath = "/" + jsonTmpPath.replace(import.meta.dir + "/", ""); const result = spawnSync("typst", [ "compile", TEMPLATE_FILE, path, "--input", `dataPath=${relativeJsonPath}`, "--input", `metadata=${JSON.stringify(metadata)}`, "--root", import.meta.dir ]); if (result.status === 0) { console.log(`✅ PDF generated: ${path}`); fs.removeSync(jsonTmpPath); } else { console.error(`❌ Failed to generate PDF`); console.error(result.stderr.toString()); } } // CLI entry point const monthArg = process.argv[2] || dayjs().format("YYMM"); generateMonthlySummary(monthArg).catch(console.error);