employee-workstation-compli.../generateMonthlySummary.ts

202 lines
6.1 KiB
TypeScript

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<string, Assertion>;
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);