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
|
||||
dist
|
||||
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",
|
||||
"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"
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
37
src/index.ts
37
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);
|
||||
});
|
@ -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();
|
||||
}
|
@ -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 {
|
||||
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>
|
||||
|
@ -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();
|
||||
|
@ -1,28 +1,66 @@
|
||||
<!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>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>TrACreport</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
<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>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
<div>
|
||||
<label for="end">End Date</label>
|
||||
<input type="date" id="end" name="end" required>
|
||||
</div>
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
<button type="submit">Generate Report</button>
|
||||
</form>
|
||||
-->
|
||||
</body>
|
||||
input[type="date"],
|
||||
input[type="password"],
|
||||
button {
|
||||
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>
|
||||
|
||||
|
@ -5,10 +5,203 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<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>
|
||||
<body>
|
||||
<h1>Report an A/C Defect</h1>
|
||||
<a href="/generate-report.html" id="report-link">R</a>
|
||||
<form id="defectForm" action="POST">
|
||||
<label for="unitNumber">Unit Number</label><br>
|
||||
<input type="number" id="unitNumber" name="unitNumber" required>
|
||||
@ -16,8 +209,8 @@
|
||||
<!-- The contents of this DIV is inserted with JS -->
|
||||
</div>
|
||||
<div id="formHidden">
|
||||
<h2>Have you reported this to maintenance?</h2>
|
||||
<div id="radio-group">
|
||||
<h2>Have you reported this to maintenance?</h2>
|
||||
<label>
|
||||
<input class="radio" type="radio" name="reported" value="via Defect Book" required>
|
||||
via Defect Book
|
||||
@ -30,6 +223,10 @@
|
||||
<input class="radio" type="radio" name="reported" value="via Telephone">
|
||||
via Telephone
|
||||
</label>
|
||||
<label>
|
||||
<input class="radio" type="radio" name="reported" value="via Email">
|
||||
via Email
|
||||
</label>
|
||||
<label>
|
||||
<input class="radio" type="radio" name="reported" value="no">
|
||||
No (Explain reason in comments)
|
||||
|
@ -1,6 +1,6 @@
|
||||
let data = {}
|
||||
|
||||
const dataPromise = fetch("units.converted.json")
|
||||
const dataPromise = fetch("/units.converted.json")
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error("Failed to load 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": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user