This commit is contained in:
Fred Boniface
2025-06-26 16:26:04 +01:00
commit eb580132cc
10 changed files with 3166 additions and 0 deletions

52
static/index.html Normal file
View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<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>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>Report an A/C Defect</h1>
<form id="defectForm" action="POST">
<label for="unitNumber">Unit Number</label><br>
<input type="number" id="unitNumber" name="unitNumber" required>
<div id="formExpansion">
<!-- The contents of this DIV is inserted with JS -->
</div>
<div id="formHidden">
<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
</label>
<label>
<input class="radio" type="radio" name="reported" value="via App">
via App
</label>
<label>
<input class="radio" type="radio" name="reported" value="via Telephone">
via Telephone
</label>
<label>
<input class="radio" type="radio" name="reported" value="no">
No (Explain reason in comments)
</label>
</div>
<label id="commentsLabel" for="comments">Comments:</label>
<p>eg. If you did not report, why? Did a fitter meet the train? Is there already a response in the defect book?</p>
<textarea id="comments" name="comments" rows="5" cols="35"></textarea>
<br><br>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</div>
</form>
<div id="formStatus" class="status-message"></div>
<script src="script.js"></script>
</body>
</html>

176
static/script.js Normal file
View File

@@ -0,0 +1,176 @@
let data = {}
const dataPromise = fetch("units.converted.json")
.then(res => {
if (!res.ok) throw new Error("Failed to load JSON");
return res.json();
})
.then(json => {
data = json;
console.log("Loaded unit data");
})
.catch(err => {
console.error("Error loading unit data", err);
window.location.reload();
});
window.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('unitNumber');
const resultDiv = document.getElementById('formExpansion');
input.addEventListener('input', async () => {
await dataPromise;
const value = input.value.trim();
if (value in data) {
const match = data[value]; // this is now an array of { id, zones }
resultDiv.textContent = "";
loadForm(match);
} else {
resultDiv.textContent = "Enter a valid unit number";
document.getElementById('formHidden').style.display = "none";
}
})
});
document.getElementById("defectForm").addEventListener("submit", formSubmit);
window.addEventListener("load", retryOfflineReports);
async function loadForm(values) {
const formExpansion = document.getElementById('formExpansion');
const formHidden = document.getElementById('formHidden');
formExpansion.innerHTML = '';
const heading = document.createElement('h2');
heading.textContent = "Choose all areas where A/C failed";
formExpansion.appendChild(heading);
for (const vehicle of values) {
const coachTitle = document.createElement('h3');
coachTitle.textContent = vehicle.id;
formExpansion.appendChild(coachTitle);
for (const zone of vehicle.zones) {
const checkboxId = `${zone}-${vehicle.id}`;
const wrapper = document.createElement('div');
wrapper.className = 'checkbox-wrapper';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = checkboxId;
checkbox.name = checkboxId;
checkbox.title = zone;
const label = document.createElement('label');
label.htmlFor = checkboxId;
label.textContent = zone;
wrapper.appendChild(checkbox);
wrapper.appendChild(label);
formExpansion.appendChild(wrapper);
}
}
formHidden.style = 'display:block';
}
function reset() {
document.getElementById('formHidden').style = 'display:hidden';
document.getElementById('formExpansion').innerHTML = '';
}
async function formSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const submitButton = form.querySelector('button[type="submit"]');
submitButton.disabled = true;
const data = {};
for (const [key, value] of formData.entries()) {
if (data[key]) {
if (!Array.isArray(data[key])) data[key] = [data[key]];
data[key].push(value);
} else {
data[key] = value;
}
}
data.utcTimestamp = new Date().toISOString();
try {
const res = await fetch("/submit", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Submission failed");
console.log("Form Submitted")
showStatus("✅ Form received, thank-you.", type="success");
} catch (err) {
console.warn("Form Sending failed, saving to localStorage");
saveReportOffline(data);
showStatus("⚠️ Unable to send form, it will be automatically sent with the next report you submit.", type="warn");
}
form.reset();
reset();
submitButton.disabled = false;
}
function saveReportOffline(report) {
const key = "offlineReports";
const reports = JSON.parse(localStorage.getItem(key) || "[]");
reports.push(report);
localStorage.setItem(key, JSON.stringify(reports));
}
async function retryOfflineReports() {
const key = "offlineReports";
const stored = JSON.parse(localStorage.getItem(key) || "[]");
if (stored.length === 0) return;
console.log(`Attempting to resend ${stored.length} stored report(s)...`);
const remaining = [];
for (const report of stored) {
try {
const res = await fetch("/submit", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(report),
});
if (!res.ok) throw new Error ("Submission Failed");
console.log("Resent one stored report");
} catch (err) {
console.warn("Failed to resend stored report");
remaining.push(report);
}
}
if (remaining.length === 0) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(remaining));
}
}
function showStatus(message, type) {
const statusDiv = document.getElementById('formStatus');
statusDiv.textContent = message;
statusDiv.className = `status-${type}`;
statusDiv.style = 'display:flex';
setTimeout(() => {
statusDiv.style = 'display:none';
}, 5000);
}

165
static/style.css Normal file
View File

@@ -0,0 +1,165 @@
* {
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;
}

1435
static/units.converted.json Normal file

File diff suppressed because it is too large Load Diff