Build out and push automatic report generation, remove emailing of every report indiidially

This commit is contained in:
Fred Boniface
2025-07-10 12:41:41 +01:00
parent bc642ce0a8
commit 997c758403
19 changed files with 507 additions and 338 deletions

View File

@@ -1,4 +1,5 @@
import mongoose from "mongoose";
import { Report } from "./models/report.js";
let isConnected = false;
@@ -23,6 +24,8 @@ export async function initDb(): Promise<void> {
await mongoose.connect(MONGO_URI);
isConnected = true;
console.log('✅ MongoDB connected');
await Report.syncIndexes();
console.log('✅ Indexes synced');
} catch (err) {
console.error('❌ MongoDB connection error:', err)
process.exit(1);

View File

@@ -41,7 +41,7 @@ export async function handleFormData(data: any) {
}
}
sendMail(report);
// sendMail(report); Disable sendMail in favour of daily,weekly,monthly reports
sendToDatabase(report);
}

View File

@@ -1,9 +1,10 @@
import express, { Request, Response } from "express";
import { handleFormData, fetchReports } from "./formHandler.js";
import { initDb } from "./database.js";
import { GenerateReportByDate } from "./reports/reports.js";
const app = express();
const port = process.env.port || 3000;
const port = process.env.PORT || 3000;
initDb();
@@ -20,7 +21,7 @@ app.post('/submit', (req, res) => {
res.status(200).json({ status: 'ok', message: 'Form received' });
});
app.get('/fetch', async (req, res) => {
app.get('/fetchall', async (req, res) => {
try {
const data = await fetchReports();
res.status(200).json(data);
@@ -29,6 +30,38 @@ app.get('/fetch', async (req, res) => {
}
});
interface ReportQuery {
start?: string;
end?: string;
password?: string;
};
const reportHandler = async (req: Request, res: Response) => {
const { start, end, password } = req.query;
if (password !== process.env.PASSWORD) {
return res.status(401).send('Unauthorized');
}
if (!start || !end) {
return res.status(400).send('Missing Parameters');
}
try {
const startDate = new Date(start as string);
const endDate = new Date(end as string);
endDate.setHours(23, 59, 59, 999); // Set to end of day
const html = await GenerateReportByDate(startDate, endDate);
res.setHeader('Content-Type', 'text/html');
res.send(html);
} catch (err) {
console.error('Report generation failed:', err);
res.status(500).send('Error generating report');
}
};
app.get('/report', reportHandler as express.RequestHandler);
app.listen(port, () => {
console.log("Server running on port:", port);
});

View File

@@ -1,5 +1,7 @@
import { Schema, model, Document } from "mongoose";
import type { Report as ReportType, Fault as FaultType } from "../formHandler";
import type { BaseReport } from "../reports/reports";
interface ReportDocument extends ReportType, Document {}
@@ -16,4 +18,16 @@ const ReportSchema = new Schema<ReportDocument>({
faults: { type: [FaultSchema], required: true },
});
export const Report = model<ReportDocument>('Report', ReportSchema);
ReportSchema.index({ utcTimestamp: 1 });
ReportSchema.index({ unitNumber: 1 });
export const Report = model<ReportDocument>('Report', ReportSchema);
export async function fetchReportsInRange(start: Date, end: Date): Promise<BaseReport[]> {
return Report.find({
utcTimestamp: {
$gte: start,
$lte: end,
}
}).exec();
}

View File

@@ -1,132 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>TrACreport - Report Output</title>
<style>
body {
font-family: sans-serif;
}
.preface {
text-align: center;
font-size: 0.9em;
}
.train-container {
width: 75%;
margin: 2em auto;
padding: 1em;
border-radius: 8px;
}
.unit-number {
text-align: center;
margin-bottom: 1em;
font-size: 1.5em;
}
.vehicle-container {
display: flex;
justify-content: space-between;
gap: 0.5em;
}
.coach {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.vehicle-number {
margin-bottom: 0.5em;
font-weight: bold;
}
.vehicle-box {
width: 100%;
height: 50px;
background-color: #e0e0e0;
border: 2px solid black;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 6px;
font-size: 1.2em;
box-sizing: border-box;
}
.cab-marker {
width: 1.5em;
text-align: center;
font-weight: bold;
background-color: #d0d0f0;
border-radius: 4px;
padding: 2px;
}
.report-count {
flex: 1;
text-align: center;
}
.comments-box {
margin-top: 1.5em;
padding: 1em;
background-color: #f9f9f9;
border: 1px solid #aaa;
border-radius: 6px;
}
.comments-box h2 {
margin-top: 0;
font-size: 1.2em;
border-bottom: 1px solid #ccc;
padding-bottom: 0.5em;
}
.comments-box p {
margin: 0.5em 0;
}
</style>
</head>
<body>
<div class="preface">
<h1>TrACreport</h1>
<h2>Report period: 09/06/2025-09/07/2025</h2>
<p>Total reports received for each vehicle in the West fleet</p>
<p>158 cabs are not recorded as cab cooling is disabled for Guards</p>
</div>
<div class="train-container">
<h1 class="unit-number">158747</h1>
<div class="vehicle-container">
<div class="coach">
<div class="vehicle-number">57747</div>
<div class="vehicle-box">
<div class="cab-marker">0</div>
<div class="report-count">2</div>
</div>
</div>
<div class="coach">
<div class="vehicle-number">51747</div>
<div class="vehicle-box">
<div class="report-count">4</div>
</div>
</div>
<div class="coach">
<div class="vehicle-number">52747</div>
<div class="vehicle-box">
<div class="report-count">5</div>
<div class="cab-marker">0</div>
</div>
</div>
</div>
<div class="comments-box">
<h2>Comments</h2>
<p>Why bother reporting?</p>
<p>Blue button in cab not working</p>
</div>
</div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
import { GroupedUnitReport, VehicleReport } from "./reports";
import { GroupedUnitReport, VehicleReport, Comments } from "./reports";
export function generateHTMLReport(units: GroupedUnitReport[], startDate: Date, endDate: Date, totalReports: number): string {
const unitsWithReports = units.filter(unit =>
@@ -44,17 +44,28 @@ function renderCoach(vehicle: VehicleReport, index: number, total: number): stri
`;
}
function renderComments(comments: string[]): string {
const renderedComments = comments.length
? comments.map(c => `<p>${c}</p>`).join('\n')
: '<p>No comments submitted.</p>';
function renderComments(comments: Comments[]): string {
const formatDate = (date: Date): string =>
new Intl.DateTimeFormat('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
const renderedComments = comments.length
? comments.map(c =>
`<p>Reported: <strong>${c.reportedBy}</strong> on: <strong>${formatDate(c.timestamp)}</strong><br>${c.comment}</p>`
).join('\n')
: '<p>No comment</p>';
return `
<div class="comments-box">
<h2>Comments</h2>
${renderedComments}
<h2>Comments</h2>
${renderedComments}
</div>
`;
`;
}
function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports: number): string {
@@ -167,6 +178,7 @@ function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports:
<div class="preface">
<h1>TrACreport</h1>
<h2>Report period: ${formatDate(startDate)} - ${formatDate(endDate)}</h2>
<p>All times are in Universal Time Coordinated</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>

View File

@@ -1,7 +1,8 @@
import { generateHTMLReport } from "./reportHtml.js";
import { writeFile } from "fs/promises";
import { fetchReportsInRange } from "../models/report.js";
interface BaseReport {
export interface BaseReport {
unitNumber: string;
reported: string;
comments?: string;
@@ -17,7 +18,13 @@ interface Fault {
export interface GroupedUnitReport {
unitNumber: string;
vehicles: VehicleReport[];
comments: string[];
comments: Comments[];
}
export interface Comments {
timestamp: Date;
reportedBy: string;
comment: string;
}
export interface VehicleReport {
@@ -82,7 +89,11 @@ function populateFaultReports(
const unit = unitMap.get(report.unitNumber);
if (!unit) continue;
if (report.comments) unit.comments.push(report.comments);
unit.comments.push({
comment: report.comments ?? "",
timestamp: report.utcTimestamp,
reportedBy: report.reported,
});
for (const fault of report.faults) {
const vehicle = unit.vehicles.find(v => v.vehicleNumber === fault.coach);
@@ -97,7 +108,17 @@ function populateFaultReports(
return grouped;
}
export async function GenerateReportByDate(start: Date, end: Date): Promise<string> {
const unitLayout = await fetchUnitLayout();
const reports = await fetchReportsInRange(start, end);
const totalReports = reports.length;
const grouped = buildInitialReportStructure(unitLayout);
const finalReport = populateFaultReports(grouped, reports);
return generateHTMLReport(finalReport, start, end, totalReports);
}
/*
(async () => {
console.log('Fetching unit layout...');
const unitLayout = await fetchUnitLayout();