diff --git a/.dockerignore b/.dockerignore index 6e070ff..fa71064 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ node_modules dist dockercompose -dockerfile \ No newline at end of file +dockerfile +auto-report-email \ No newline at end of file diff --git a/auto-report-mail/.dockerignore b/auto-report-mail/.dockerignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/auto-report-mail/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/auto-report-mail/Dockerfile b/auto-report-mail/Dockerfile new file mode 100644 index 0000000..c18042d --- /dev/null +++ b/auto-report-mail/Dockerfile @@ -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"] \ No newline at end of file diff --git a/auto-report-mail/index.js b/auto-report-mail/index.js new file mode 100644 index 0000000..2a94e6c --- /dev/null +++ b/auto-report-mail/index.js @@ -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'); \ No newline at end of file diff --git a/auto-report-mail/package-lock.json b/auto-report-mail/package-lock.json new file mode 100644 index 0000000..7dfeafd --- /dev/null +++ b/auto-report-mail/package-lock.json @@ -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" + } + } + } +} diff --git a/auto-report-mail/package.json b/auto-report-mail/package.json new file mode 100644 index 0000000..d9b1779 --- /dev/null +++ b/auto-report-mail/package.json @@ -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" + } +} diff --git a/package.json b/package.json index 7c56084..1352ea8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "start": "npx tsx --watch src/index.ts", + "report": "npx tsc src/reports/reports.ts", "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc" }, diff --git a/src/database.ts b/src/database.ts index b8c7c7f..57c0ed2 100644 --- a/src/database.ts +++ b/src/database.ts @@ -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 { 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); diff --git a/src/formHandler.ts b/src/formHandler.ts index b692cb0..f9fc6c7 100644 --- a/src/formHandler.ts +++ b/src/formHandler.ts @@ -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); } diff --git a/src/index.ts b/src/index.ts index 6a333a8..9e7ceae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); }); \ No newline at end of file diff --git a/src/models/report.ts b/src/models/report.ts index c5a94cc..6d3ccae 100644 --- a/src/models/report.ts +++ b/src/models/report.ts @@ -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({ faults: { type: [FaultSchema], required: true }, }); -export const Report = model('Report', ReportSchema); \ No newline at end of file +ReportSchema.index({ utcTimestamp: 1 }); +ReportSchema.index({ unitNumber: 1 }); + +export const Report = model('Report', ReportSchema); + +export async function fetchReportsInRange(start: Date, end: Date): Promise { + return Report.find({ + utcTimestamp: { + $gte: start, + $lte: end, + } + }).exec(); +} \ No newline at end of file diff --git a/src/reports/report.html b/src/reports/report.html deleted file mode 100644 index 8043750..0000000 --- a/src/reports/report.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - TrACreport - Report Output - - - -
-

TrACreport

-

Report period: 09/06/2025-09/07/2025

-

Total reports received for each vehicle in the West fleet

-

158 cabs are not recorded as cab cooling is disabled for Guards

-
-
-

158747

-
-
-
57747
-
-
0
-
2
-
-
-
-
51747
-
-
4
-
-
-
-
52747
-
-
5
-
0
-
-
-
-
-

Comments

-

Why bother reporting?

-

Blue button in cab not working

-
-
- - \ No newline at end of file diff --git a/src/reports/reportHtml.ts b/src/reports/reportHtml.ts index 90e4e0c..c5108ba 100644 --- a/src/reports/reportHtml.ts +++ b/src/reports/reportHtml.ts @@ -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 => `

${c}

`).join('\n') - : '

No comments submitted.

'; +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 => + `

Reported: ${c.reportedBy} on: ${formatDate(c.timestamp)}
${c.comment}

` + ).join('\n') + : '

No comment

'; + return `
-

Comments

- ${renderedComments} +

Comments

+ ${renderedComments}
- `; + `; } function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports: number): string { @@ -167,6 +178,7 @@ function wrapHtml(content: string, startDate: Date, endDate: Date, totalReports:

TrACreport

Report period: ${formatDate(startDate)} - ${formatDate(endDate)}

+

All times are in Universal Time Coordinated

Total reports received: ${totalReports}

Reports are shown per saloon or cab

158 cabs are not recorded as cab cooling is disabled for Guards

diff --git a/src/reports/reports.ts b/src/reports/reports.ts index 5dede8a..77589c8 100644 --- a/src/reports/reports.ts +++ b/src/reports/reports.ts @@ -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 { + 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(); diff --git a/static/generate-report.html b/static/generate-report.html index 55dbace..80b458c 100644 --- a/static/generate-report.html +++ b/static/generate-report.html @@ -1,28 +1,66 @@ - - - - Generate Report - - + + + + + TrACreport + + + +

TrACreport

+

Generate Fault Report

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + + diff --git a/static/index.html b/static/index.html index f1ddef4..73fdaca 100644 --- a/static/index.html +++ b/static/index.html @@ -5,10 +5,203 @@ TrACreport - +

Report an A/C Defect

+ R

@@ -16,8 +209,8 @@
+

Have you reported this to maintenance?

-

Have you reported this to maintenance?

+