Release initial version supporting report-generation. Currently only through CLI.

This commit is contained in:
Fred Boniface 2025-07-10 00:17:34 +01:00
parent 84530f7a1f
commit bc642ce0a8
6 changed files with 68 additions and 92 deletions

View File

@ -11,7 +11,7 @@ export interface Report {
unitNumber: string; unitNumber: string;
reported: string; reported: string;
comments: string; comments: string;
utcTimestamp: string; utcTimestamp: Date;
faults: Fault[]; faults: Fault[];
} }
@ -27,7 +27,7 @@ export async function handleFormData(data: any) {
unitNumber: data.unitNumber, unitNumber: data.unitNumber,
reported: data.reported, reported: data.reported,
comments: data.comments || '', comments: data.comments || '',
utcTimestamp: data.utcTimestamp, utcTimestamp: new Date(data.utcTimestamp),
faults: [] faults: []
}; };

View File

@ -12,7 +12,7 @@ const ReportSchema = new Schema<ReportDocument>({
unitNumber: { type: String, required: true }, unitNumber: { type: String, required: true },
reported: { type: String, required: true }, reported: { type: String, required: true },
comments: { type: String, required: false }, comments: { type: String, required: false },
utcTimestamp: { type: String, required: true }, utcTimestamp: { type: Date, required: true },
faults: { type: [FaultSchema], required: true }, faults: { type: [FaultSchema], required: true },
}); });

View File

@ -1,84 +0,0 @@
interface ReportSummary {
totalReports: number;
firstTimestamp: Date;
lastTimestamp: Date;
reportsPerUnit: Record<string, number>;
}
interface vehicleDetails {
vehicleNumber: string;
reportCount: number;
cabFront: boolean;
cabRear: boolean;
}
interface ReportsByUnit {
unitNumber: string;
vehicles: vehicleDetails[];
comments: string[];
}
interface Report {
unitNumber: string;
reported: string;
comments: string;
utcTimestamp: string;
faults: string[];
}
export async function organiseReports(): Promise<ReportSummary> {
try {
const res = await fetch('https://rep.fjla.uk/fetch');
if (!res.ok) throw new Error(`Fetch failed: ${res.statusText}`);
const reports: Report[] = await res.json();
if (reports.length === 0) {
return {
totalReports: 0,
firstTimestamp: new Date(0), // Epoch
lastTimestamp: new Date(0),
reportsPerUnit: {},
};
}
// Sort by string utcTimestamp (lexical sort works fine for ISO strings)
reports.sort((a, b) => a.utcTimestamp.localeCompare(b.utcTimestamp));
const firstTimestamp = reports[0].utcTimestamp;
const lastTimestamp = reports[reports.length - 1].utcTimestamp;
const totalReports = reports.length;
const reportsPerUnit: Record<string, number> = {};
for (const report of reports) {
const unit = report.unitNumber;
reportsPerUnit[unit] = (reportsPerUnit[unit] || 0) + 1;
}
return {
totalReports,
firstTimestamp: new Date(firstTimestamp),
lastTimestamp: new Date(lastTimestamp),
reportsPerUnit,
};
} catch (err) {
console.error('Error organising reports:', err);
throw err;
}
}
// Fetch all unit numbers.
// Group reports by unit number.
// Build HTML for each report.
(async () => {
try {
const summary = await organiseReports();
console.log("Total reports:", summary.totalReports);
console.log("First report:", summary.firstTimestamp.toISOString());
console.log("Last report:", summary.lastTimestamp.toISOString());
console.log("Reports per unit:", summary.reportsPerUnit);
} catch (err) {
console.error("Failed to organise reports:", err);
}
})();

View File

@ -1,6 +1,6 @@
import { GroupedUnitReport, VehicleReport } from "./reports"; import { GroupedUnitReport, VehicleReport } from "./reports";
export function generateHTMLReport(units: GroupedUnitReport[], startDate: Date, endDate: Date): string { export function generateHTMLReport(units: GroupedUnitReport[], startDate: Date, endDate: Date, totalReports: number): string {
const unitsWithReports = units.filter(unit => const unitsWithReports = units.filter(unit =>
unit.vehicles.some( unit.vehicles.some(
v => v.saloonReports > 0 || v.cabReports > 0 v => v.saloonReports > 0 || v.cabReports > 0
@ -8,14 +8,14 @@ export function generateHTMLReport(units: GroupedUnitReport[], startDate: Date,
); );
const body = unitsWithReports.map(renderUnitSection).join('\n'); const body = unitsWithReports.map(renderUnitSection).join('\n');
return wrapHtml(body, startDate, endDate); return wrapHtml(body, startDate, endDate, totalReports);
} }
function renderUnitSection(unit: GroupedUnitReport): string { function renderUnitSection(unit: GroupedUnitReport): string {
const total = unit.vehicles.length; const total = unit.vehicles.length;
return ` return `
<div class="train-container"> <div class="train-container avoid-break">
<h1 class="unit-number">${unit.unitNumber}</h1> <h1 class="unit-number">${unit.unitNumber}</h1>
<div class="vehicle-container"> <div class="vehicle-container">
${unit.vehicles.map((v, i) => renderCoach(v, i, total)).join('\n')} ${unit.vehicles.map((v, i) => renderCoach(v, i, total)).join('\n')}
@ -57,7 +57,7 @@ function renderComments(comments: string[]): string {
`; `;
} }
function wrapHtml(content: string, startDate: Date, endDate: Date): string { function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports: number): string {
const formatDate = (d: Date) => d.toISOString().split('T')[0]; const formatDate = (d: Date) => d.toISOString().split('T')[0];
return ` return `
@ -70,6 +70,11 @@ function wrapHtml(content: string, startDate: Date, endDate: Date): string {
font-family: sans-serif; font-family: sans-serif;
} }
.avoid-break {
page-break-inside: avoid;
break-inside: avoid;
}
.preface { .preface {
text-align: center; text-align: center;
font-size: 0.9em; font-size: 0.9em;
@ -152,13 +157,18 @@ function wrapHtml(content: string, startDate: Date, endDate: Date): string {
.comments-box p { .comments-box p {
margin: 0.5em 0; margin: 0.5em 0;
} }
.bold {
font-weight: bold;
}
</style> </style>
</head> </head>
<body> <body>
<div class="preface"> <div class="preface">
<h1>TrACreport</h1> <h1>TrACreport</h1>
<h2>Report period: ${formatDate(startDate)} - ${formatDate(endDate)}</h2> <h2>Report period: ${formatDate(startDate)} - ${formatDate(endDate)}</h2>
<p>Total reports received for each vehicle in the West fleet</p> <p class="bold">Total reports received: ${totalReports}</p>
<p>Reports are shown per saloon or cab</p>
<p>158 cabs are not recorded as cab cooling is disabled for Guards</p> <p>158 cabs are not recorded as cab cooling is disabled for Guards</p>
</div> </div>
${content} ${content}

View File

@ -1,3 +1,6 @@
import { generateHTMLReport } from "./reportHtml.js";
import { writeFile } from "fs/promises";
interface BaseReport { interface BaseReport {
unitNumber: string; unitNumber: string;
reported: string; reported: string;
@ -94,6 +97,25 @@ function populateFaultReports(
return grouped; return grouped;
} }
(async () => {
console.log('Fetching unit layout...');
const unitLayout = await fetchUnitLayout();
console.log('Unit layout:', unitLayout);
console.log('Fetching reports...')
const reports = await fetchReports();
console.log('Reports:', reports)
const totalReports = reports.length;
const grouped = buildInitialReportStructure(unitLayout)
const finalReport = populateFaultReports(grouped, reports)
const htmlReport = generateHTMLReport(finalReport, new Date("2025-06-27"), new Date("2025-07-09"), totalReports)
await writeFile('out.html', htmlReport, 'utf-8');
})().catch(err => {
console.error('Unhandled error:', err);
process.exit(1);
});
/* USAGE: /* USAGE:
const unitLayout = await fetchUnitLayout(); const unitLayout = await fetchUnitLayout();
const reports = await fetchReports(); const reports = await fetchReports();

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Generate Report</title>
</head>
<body>
<h1>Generate A/C Fault Report</h1>
<p>This page is under development. For now, reports will be sent out through other means.</p>
<!--
<form action="/report/generate" method="GET">
<div>
<label for="start">Start Date</label>
<input type="date" id="start" name="start" required>
</div>
<div>
<label for="end">End Date</label>
<input type="date" id="end" name="end" required>
</div>
<button type="submit">Generate Report</button>
</form>
-->
</body>
</html>