Build out and push automatic report generation, remove emailing of every report indiidially
This commit is contained in:
parent
bc642ce0a8
commit
997c758403
@ -2,4 +2,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dockercompose
|
dockercompose
|
||||||
dockerfile
|
dockerfile
|
||||||
|
auto-report-email
|
1
auto-report-mail/.dockerignore
Normal file
1
auto-report-mail/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
10
auto-report-mail/Dockerfile
Normal file
10
auto-report-mail/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
ENV TZ=Europe/London
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
COPY . .
|
||||||
|
CMD ["node", "index.js"]
|
81
auto-report-mail/index.js
Normal file
81
auto-report-mail/index.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: parseInt(process.env.SMTP_PORT || '587'),
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function sendReportEmail(start, end, subject = "TrACreport Summary") {
|
||||||
|
const url = new URL(process.env.WEBURL);
|
||||||
|
url.searchParams.set('start', start.toISOString().slice(0, 10));
|
||||||
|
url.searchParams.set('end', end.toISOString().slice(0, 10));
|
||||||
|
url.searchParams.set('password', process.env.PASSWORD);
|
||||||
|
|
||||||
|
const maxAttempts = 3;
|
||||||
|
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(`📡 Attempt ${attempt}: fetching ${url}`);
|
||||||
|
|
||||||
|
const res = await fetch(url.toString());
|
||||||
|
if (!res.ok) throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
|
||||||
|
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"TrACreport Bot" <${process.env.SMTP_USER}>`,
|
||||||
|
to: process.env.MAIL_RECIPIENTS,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Report emailed: ${subject} (${start.toISOString().slice(0, 10)})`);
|
||||||
|
return; // Success, exit function
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Attempt ${attempt} failed:`, err);
|
||||||
|
|
||||||
|
if (attempt < maxAttempts) {
|
||||||
|
console.log(`⏳ Retrying in 5s...`);
|
||||||
|
await delay(5000); // Wait before retry
|
||||||
|
} else {
|
||||||
|
console.error(`🚫 All ${maxAttempts} attempts failed. Giving up.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daily Report
|
||||||
|
cron.schedule('5 1 * * *', () => {
|
||||||
|
const today = new Date();
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setUTCDate(today.getUTCDate() - 1);
|
||||||
|
sendReportEmail(yesterday, yesterday, "TrACreport Daily Report");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weekly Report
|
||||||
|
cron.schedule('10 1 * * 1', () => {
|
||||||
|
const today = new Date();
|
||||||
|
const end = new Date(today);
|
||||||
|
end.setUTCDate(roday.getUTCDate() - 1);
|
||||||
|
const start = new Date(end);
|
||||||
|
start.setUTCDate(end.getUTCDate() - 6);
|
||||||
|
sendReportEmail(start, end, "TrACreport Weekly Report");
|
||||||
|
})
|
||||||
|
|
||||||
|
// Monthly Report
|
||||||
|
cron.schedule('15 1 1 * *', () => {
|
||||||
|
const today = new Date();
|
||||||
|
const end = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), 0)); // Last day of previous month
|
||||||
|
const start = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), 1)); // First day of previous month
|
||||||
|
sendReportEmail(start, end, "TrACreport Monthly Report");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📅 TrACreport schedulers are active');
|
35
auto-report-mail/package-lock.json
generated
Normal file
35
auto-report-mail/package-lock.json
generated
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "auto-report-mail",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "auto-report-mail",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"node-cron": "^4.2.0",
|
||||||
|
"nodemailer": "^7.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-nOdP7uH7u55w7ybQq9fusXtsResok+ErzvOBydJUPBBaQ9W+EfBaBWFPgJ8sOB7FWQednDvVBJtgP5xA0bME7Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
auto-report-mail/package.json
Normal file
16
auto-report-mail/package.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "auto-report-mail",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"node-cron": "^4.2.0",
|
||||||
|
"nodemailer": "^7.0.5"
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx tsx --watch src/index.ts",
|
"start": "npx tsx --watch src/index.ts",
|
||||||
|
"report": "npx tsc src/reports/reports.ts",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
import { Report } from "./models/report.js";
|
||||||
|
|
||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ export async function initDb(): Promise<void> {
|
|||||||
await mongoose.connect(MONGO_URI);
|
await mongoose.connect(MONGO_URI);
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
console.log('✅ MongoDB connected');
|
console.log('✅ MongoDB connected');
|
||||||
|
await Report.syncIndexes();
|
||||||
|
console.log('✅ Indexes synced');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ MongoDB connection error:', err)
|
console.error('❌ MongoDB connection error:', err)
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -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);
|
sendToDatabase(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
src/index.ts
37
src/index.ts
@ -1,9 +1,10 @@
|
|||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { handleFormData, fetchReports } from "./formHandler.js";
|
import { handleFormData, fetchReports } from "./formHandler.js";
|
||||||
import { initDb } from "./database.js";
|
import { initDb } from "./database.js";
|
||||||
|
import { GenerateReportByDate } from "./reports/reports.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.port || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
initDb();
|
initDb();
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ app.post('/submit', (req, res) => {
|
|||||||
res.status(200).json({ status: 'ok', message: 'Form received' });
|
res.status(200).json({ status: 'ok', message: 'Form received' });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/fetch', async (req, res) => {
|
app.get('/fetchall', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = await fetchReports();
|
const data = await fetchReports();
|
||||||
res.status(200).json(data);
|
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, () => {
|
app.listen(port, () => {
|
||||||
console.log("Server running on port:", port);
|
console.log("Server running on port:", port);
|
||||||
});
|
});
|
@ -1,5 +1,7 @@
|
|||||||
import { Schema, model, Document } from "mongoose";
|
import { Schema, model, Document } from "mongoose";
|
||||||
import type { Report as ReportType, Fault as FaultType } from "../formHandler";
|
import type { Report as ReportType, Fault as FaultType } from "../formHandler";
|
||||||
|
import type { BaseReport } from "../reports/reports";
|
||||||
|
|
||||||
|
|
||||||
interface ReportDocument extends ReportType, Document {}
|
interface ReportDocument extends ReportType, Document {}
|
||||||
|
|
||||||
@ -16,4 +18,16 @@ const ReportSchema = new Schema<ReportDocument>({
|
|||||||
faults: { type: [FaultSchema], required: true },
|
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();
|
||||||
|
}
|
@ -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>
|
|
@ -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 {
|
export function generateHTMLReport(units: GroupedUnitReport[], startDate: Date, endDate: Date, totalReports: number): string {
|
||||||
const unitsWithReports = units.filter(unit =>
|
const unitsWithReports = units.filter(unit =>
|
||||||
@ -44,17 +44,28 @@ function renderCoach(vehicle: VehicleReport, index: number, total: number): stri
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderComments(comments: string[]): string {
|
function renderComments(comments: Comments[]): string {
|
||||||
const renderedComments = comments.length
|
const formatDate = (date: Date): string =>
|
||||||
? comments.map(c => `<p>${c}</p>`).join('\n')
|
new Intl.DateTimeFormat('en-GB', {
|
||||||
: '<p>No comments submitted.</p>';
|
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 `
|
return `
|
||||||
<div class="comments-box">
|
<div class="comments-box">
|
||||||
<h2>Comments</h2>
|
<h2>Comments</h2>
|
||||||
${renderedComments}
|
${renderedComments}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports: number): string {
|
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">
|
<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>All times are in Universal Time Coordinated</p>
|
||||||
<p class="bold">Total reports received: ${totalReports}</p>
|
<p class="bold">Total reports received: ${totalReports}</p>
|
||||||
<p>Reports are shown per saloon or cab</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>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { generateHTMLReport } from "./reportHtml.js";
|
import { generateHTMLReport } from "./reportHtml.js";
|
||||||
import { writeFile } from "fs/promises";
|
import { writeFile } from "fs/promises";
|
||||||
|
import { fetchReportsInRange } from "../models/report.js";
|
||||||
|
|
||||||
interface BaseReport {
|
export interface BaseReport {
|
||||||
unitNumber: string;
|
unitNumber: string;
|
||||||
reported: string;
|
reported: string;
|
||||||
comments?: string;
|
comments?: string;
|
||||||
@ -17,7 +18,13 @@ interface Fault {
|
|||||||
export interface GroupedUnitReport {
|
export interface GroupedUnitReport {
|
||||||
unitNumber: string;
|
unitNumber: string;
|
||||||
vehicles: VehicleReport[];
|
vehicles: VehicleReport[];
|
||||||
comments: string[];
|
comments: Comments[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Comments {
|
||||||
|
timestamp: Date;
|
||||||
|
reportedBy: string;
|
||||||
|
comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VehicleReport {
|
export interface VehicleReport {
|
||||||
@ -82,7 +89,11 @@ function populateFaultReports(
|
|||||||
const unit = unitMap.get(report.unitNumber);
|
const unit = unitMap.get(report.unitNumber);
|
||||||
if (!unit) continue;
|
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) {
|
for (const fault of report.faults) {
|
||||||
const vehicle = unit.vehicles.find(v => v.vehicleNumber === fault.coach);
|
const vehicle = unit.vehicles.find(v => v.vehicleNumber === fault.coach);
|
||||||
@ -97,7 +108,17 @@ function populateFaultReports(
|
|||||||
return grouped;
|
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 () => {
|
(async () => {
|
||||||
console.log('Fetching unit layout...');
|
console.log('Fetching unit layout...');
|
||||||
const unitLayout = await fetchUnitLayout();
|
const unitLayout = await fetchUnitLayout();
|
||||||
|
@ -1,28 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Generate Report</title>
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
</head>
|
<title>TrACreport</title>
|
||||||
<body>
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
<h1>Generate A/C Fault Report</h1>
|
form {
|
||||||
<p>This page is under development. For now, reports will be sent out through other means.</p>
|
display: flex;
|
||||||
<!--
|
flex-direction: column;
|
||||||
<form action="/report/generate" method="GET">
|
align-items: center;
|
||||||
<div>
|
gap: 1rem;
|
||||||
<label for="start">Start Date</label>
|
}
|
||||||
<input type="date" id="start" name="start" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
label {
|
||||||
<label for="end">End Date</label>
|
font-weight: bold;
|
||||||
<input type="date" id="end" name="end" required>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Generate Report</button>
|
input[type="date"],
|
||||||
</form>
|
input[type="password"],
|
||||||
-->
|
button {
|
||||||
</body>
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>TrACreport</h1>
|
||||||
|
<h2>Generate Fault Report</h2>
|
||||||
|
|
||||||
|
<form id="reportForm" action="/report" method="GET" target="_blank">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password">Access Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Generate Report</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Default both fields to today's date
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('start').value = today;
|
||||||
|
document.getElementById('end').value = today;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -5,10 +5,203 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>TrACreport</title>
|
<title>TrACreport</title>
|
||||||
<link rel="stylesheet" href="/style.css">
|
<style>* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #f2f3f2;
|
||||||
|
background-color: #525252;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin:auto;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#report-link {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#report-link:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100vw;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unitNumber {
|
||||||
|
width: 40vw;
|
||||||
|
font-size: larger;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: larger;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: large;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#formExpansion {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#formExpansion h3 {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-top: 1.0rem;
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#formExpansion h3:first-of-type {
|
||||||
|
margin-top: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper input[type="checkbox"] {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
margin-top: 0rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#formHidden {
|
||||||
|
display: none;
|
||||||
|
width: 100vw;
|
||||||
|
margin: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio-group {
|
||||||
|
width: 90vw;
|
||||||
|
margin:auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio-group p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.25rem 0.75rem; /* top/bottom 0.25rem, left/right 0.75rem */
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
color:#525252;
|
||||||
|
}
|
||||||
|
|
||||||
|
#radio-group input[type="radio"] {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#commentsLabel {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
width: 90vw;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#formStatus {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 37.5vh;
|
||||||
|
left: 13vw;
|
||||||
|
width: 74vw;
|
||||||
|
height: 25vh;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 25px;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background-color: green;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warn {
|
||||||
|
background-color: yellow;
|
||||||
|
color: black;
|
||||||
|
}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Report an A/C Defect</h1>
|
<h1>Report an A/C Defect</h1>
|
||||||
|
<a href="/generate-report.html" id="report-link">R</a>
|
||||||
<form id="defectForm" action="POST">
|
<form id="defectForm" action="POST">
|
||||||
<label for="unitNumber">Unit Number</label><br>
|
<label for="unitNumber">Unit Number</label><br>
|
||||||
<input type="number" id="unitNumber" name="unitNumber" required>
|
<input type="number" id="unitNumber" name="unitNumber" required>
|
||||||
@ -16,8 +209,8 @@
|
|||||||
<!-- The contents of this DIV is inserted with JS -->
|
<!-- The contents of this DIV is inserted with JS -->
|
||||||
</div>
|
</div>
|
||||||
<div id="formHidden">
|
<div id="formHidden">
|
||||||
|
<h2>Have you reported this to maintenance?</h2>
|
||||||
<div id="radio-group">
|
<div id="radio-group">
|
||||||
<h2>Have you reported this to maintenance?</h2>
|
|
||||||
<label>
|
<label>
|
||||||
<input class="radio" type="radio" name="reported" value="via Defect Book" required>
|
<input class="radio" type="radio" name="reported" value="via Defect Book" required>
|
||||||
via Defect Book
|
via Defect Book
|
||||||
@ -30,6 +223,10 @@
|
|||||||
<input class="radio" type="radio" name="reported" value="via Telephone">
|
<input class="radio" type="radio" name="reported" value="via Telephone">
|
||||||
via Telephone
|
via Telephone
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input class="radio" type="radio" name="reported" value="via Email">
|
||||||
|
via Email
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input class="radio" type="radio" name="reported" value="no">
|
<input class="radio" type="radio" name="reported" value="no">
|
||||||
No (Explain reason in comments)
|
No (Explain reason in comments)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
let data = {}
|
let data = {}
|
||||||
|
|
||||||
const dataPromise = fetch("units.converted.json")
|
const dataPromise = fetch("/units.converted.json")
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!res.ok) throw new Error("Failed to load JSON");
|
if (!res.ok) throw new Error("Failed to load JSON");
|
||||||
return res.json();
|
return res.json();
|
||||||
|
163
static/style.css
163
static/style.css
@ -1,165 +1,2 @@
|
|||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
width: 100vw;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
color: #f2f3f2;
|
|
||||||
background-color: #525252;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
padding-top: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
margin:auto;
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
width: 100vw;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#unitNumber {
|
|
||||||
width: 40vw;
|
|
||||||
font-size: larger;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: large;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#formExpansion {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.7rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#formExpansion h3 {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-top: 1.0rem;
|
|
||||||
margin-bottom: 0rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#formExpansion h3:first-of-type {
|
|
||||||
margin-top: 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper input[type="checkbox"] {
|
|
||||||
width: 1.8rem;
|
|
||||||
height: 1.8rem;
|
|
||||||
margin-top: 0rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-wrapper label {
|
|
||||||
text-transform: capitalize;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#formHidden {
|
|
||||||
display: none;
|
|
||||||
width: 100vw;
|
|
||||||
margin: auto;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#radio-group {
|
|
||||||
width: 90vw;
|
|
||||||
margin:auto;
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#radio-group p {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#radio-group label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.25rem 0.75rem; /* top/bottom 0.25rem, left/right 0.75rem */
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#radio-group input[type="radio"] {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#commentsLabel {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
width: 90vw;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 60px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#formStatus {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 37.5vh;
|
|
||||||
left: 13vw;
|
|
||||||
width: 74vw;
|
|
||||||
height: 25vh;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 25px;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-success {
|
|
||||||
background-color: green;
|
|
||||||
color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warn {
|
|
||||||
background-color: yellow;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2020",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user