220 lines
6.7 KiB
TypeScript
220 lines
6.7 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(requestedMonth: string) {
|
|
let month = requestedMonth;
|
|
let monthDir = join(REPORTS_DIR, month);
|
|
|
|
if (!existsSync(REPORTS_DIR)) {
|
|
console.error(`Reports directory not found: ${REPORTS_DIR}`);
|
|
return;
|
|
}
|
|
|
|
if (!existsSync(monthDir)) {
|
|
const availableMonths = readdirSync(REPORTS_DIR)
|
|
.filter(f => fs.statSync(join(REPORTS_DIR, f)).isDirectory())
|
|
.filter(f => /^\d{4}$/.test(f))
|
|
.sort((a, b) => b.localeCompare(a));
|
|
|
|
if (availableMonths.length === 0) {
|
|
console.error(`No month directories found in ${REPORTS_DIR}`);
|
|
return;
|
|
}
|
|
|
|
month = availableMonths[0];
|
|
monthDir = join(REPORTS_DIR, month);
|
|
console.log(`⚠️ Requested month ${requestedMonth} not found. Falling back to latest: ${month}`);
|
|
}
|
|
|
|
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);
|