`;
}
function buildDefectIntel(label) {
const intel = defectIntel[label];
if (!intel) return '';
const minorItems = (intel.minor || []).map(x => `${esc(x)} `).join('');
const majorItems = (intel.major || []).map(x => `${esc(x)} `).join('');
let passCtx = '';
if (label === 'Fire Extinguisher') {
const p = S.contextPassengers;
if (p === 'yes') passCtx = '
`;
}
function buildChecklist(scheduleKey) {
const container = $id('sto-checklist-container');
if (!container) return;
const items = scheduleItems[scheduleKey] || [];
if (!items.length) {
container.innerHTML = '
`;
}).join('');
container.innerHTML = `
`;
wireChecklistEvents(container);
wireDefectToggles();
}
function wireChecklistEvents(container) {
const banner = $id('sto-major-banner');
container.addEventListener('change', e => {
if (e.target.type !== 'radio') return;
const grp = e.target.name;
container.querySelectorAll(`input[name="${grp}"]`).forEach(r => {
r.parentElement.classList.toggle('sto-checked', r.checked);
});
const hasMajor = Array.from(container.querySelectorAll('input[type="radio"]:checked')).some(r => r.value === 'major');
if (banner) banner.style.display = hasMajor ? 'block' : 'none';
});
}
function wireDefectToggles() {
app.querySelectorAll('[data-defect-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
const body = btn.parentElement.querySelector('.sto-defect-body');
if (body) body.style.display = body.style.display === 'block' ? 'none' : 'block';
});
});
}
// ── VALIDATION ─────────────────────────────────────────────────────────────
function validateInspection() {
const required = ['sto-driver-name', 'sto-supervisor-email', 'sto-unit-number', 'sto-date'];
for (const id of required) {
const el = $id(id);
if (!el || !el.value.trim()) return { ok: false, msg: `Required field missing: ${id.replace('sto-','').replace(/-/g,' ')}` };
}
if (!S.sig.hasContent) return { ok: false, msg: 'Driver signature is required. Please sign on the pad.' };
const cert = $id('sto-certify-check');
if (!cert || !cert.checked) return { ok: false, msg: 'You must certify the inspection before saving.' };
const radios = app.querySelectorAll('#sto-checklist-container input[type="radio"]');
const names = new Set(Array.from(radios).map(r => r.name));
for (const name of names) {
if (!Array.from(radios).filter(r => r.name === name).some(r => r.checked)) {
return { ok: false, msg: 'All checklist items must be marked (OK / Minor / Major / N/A).' };
}
}
return { ok: true };
}
// ── SNAPSHOT ───────────────────────────────────────────────────────────────
function getChecklistSnapshot() {
const rows = [];
const table = app.querySelector('#sto-checklist-container table');
if (!table) return rows;
table.querySelectorAll('tbody tr').forEach(tr => {
const labelEl = tr.querySelector('.sto-item-label');
const label = labelEl ? labelEl.textContent.trim() : 'Item';
const checked = tr.querySelector('input[type="radio"]:checked');
rows.push({ item: label, status: checked ? checked.value : '' });
});
return rows;
}
// ── SAVE INSPECTION ────────────────────────────────────────────────────────
function saveInspection() {
const v = validateInspection();
if (!v.ok) { alert(v.msg); return; }
const canvas = $id('sto-sig-canvas');
const sigData = canvas ? canvas.toDataURL() : '';
const checklist = getChecklistSnapshot();
const hasMajor = checklist.some(x => x.status === 'major');
const record = {
id: 'insp_' + Date.now(),
schedule: S.schedule,
passengers: S.contextPassengers,
driver: $id('sto-driver-name').value.trim(),
driverEmail: ($id('sto-driver-email') || {}).value || '',
supervisorEmail: ($id('sto-supervisor-email') || {}).value || '',
unit: $id('sto-unit-number').value.trim(),
plate: ($id('sto-plate-number') || {}).value || '',
odometer: ($id('sto-odometer') || {}).value || '',
location: ($id('sto-location') || {}).value || '',
date: $id('sto-date').value,
time: ($id('sto-time') || {}).value || '',
notes: ($id('sto-defect-notes') || {}).value || '',
signature: sigData,
checklist,
hasMajor
};
S.history.unshift(record);
const cutoff = Date.now() - 14 * 24 * 60 * 60 * 1000;
S.history = S.history.filter(r => {
const ts = parseInt(String(r.id).replace('insp_', ''), 10);
return isNaN(ts) ? true : ts >= cutoff;
});
saveHistory();
alert('Inspection saved and locked on this device.');
showScreen('history');
}
// ── CLEAR FORM ─────────────────────────────────────────────────────────────
function clearForm() {
if (!confirm('Clear the current inspection form?')) return;
['sto-driver-name','sto-driver-email','sto-supervisor-email','sto-unit-number',
'sto-plate-number','sto-odometer','sto-location','sto-defect-notes'].forEach(id => {
const el = $id(id); if (el) el.value = '';
});
resetSig(true);
const cert = $id('sto-certify-check'); if (cert) cert.checked = false;
app.querySelectorAll('#sto-checklist-container input[type="radio"]').forEach(r => {
r.checked = false;
r.parentElement.classList.remove('sto-checked');
});
const banner = $id('sto-major-banner'); if (banner) banner.style.display = 'none';
prefillDateTime();
prefillFromSettings();
}
// ── HISTORY RENDERING ──────────────────────────────────────────────────────
function renderHistoryList(containerId) {
const list = $id(containerId);
if (!list) return;
if (!S.history.length) {
list.innerHTML = '
`;
}).join('');
list.querySelectorAll('[data-view]').forEach(btn => {
btn.addEventListener('click', () => viewInspection(btn.getAttribute('data-view')));
});
}
// ── VIEW SAVED INSPECTION ──────────────────────────────────────────────────
function viewInspection(id) {
const rec = S.history.find(r => r.id === id);
if (!rec) return;
showScreen('inspection');
setSchedule(rec.schedule);
const passSel = $id('sto-ctx-passengers');
if (passSel) { passSel.value = rec.passengers || 'unknown'; S.contextPassengers = passSel.value; }
buildChecklist(rec.schedule);
[
['sto-driver-name', rec.driver],
['sto-driver-email', rec.driverEmail],
['sto-supervisor-email', rec.supervisorEmail],
['sto-unit-number', rec.unit],
['sto-plate-number', rec.plate],
['sto-odometer', rec.odometer],
['sto-location', rec.location],
['sto-date', rec.date],
['sto-time', rec.time],
['sto-defect-notes', rec.notes]
].forEach(([id, val]) => { const el = $id(id); if (el) el.value = val || ''; });
// Signature
const sigC = $id('sto-sig-container'), sigI = $id('sto-sig-image');
if (rec.signature && rec.signature.startsWith('data:image')) {
if (sigC) sigC.style.display = 'none';
if (sigI) { sigI.src = rec.signature; sigI.style.display = 'block'; }
} else {
if (sigC) sigC.style.display = 'none';
}
const cert = $id('sto-certify-check'); if (cert) cert.checked = true;
// Read-only
const screenEl = app.querySelector('.sto-screen[data-screen="inspection"]');
if (screenEl) screenEl.classList.add('sto-readonly');
const acts = $id('sto-inspection-actions'); if (acts) acts.style.display = 'none';
// Restore checklist values
const savedMap = new Map();
if (Array.isArray(rec.checklist)) rec.checklist.forEach(it => { if (it && it.item) savedMap.set(it.item.trim(), it.status); });
const table = app.querySelector('#sto-checklist-container table');
if (table) {
table.querySelectorAll('tbody tr').forEach(tr => {
tr.querySelectorAll('.sto-status-option').forEach(l => l.classList.remove('sto-checked'));
const labelEl = tr.querySelector('.sto-item-label');
const label = labelEl ? labelEl.textContent.trim() : '';
const status = savedMap.get(label);
if (status) {
const inp = tr.querySelector(`input[type="radio"][value="${status}"]`);
if (inp) { inp.checked = true; inp.parentElement.classList.add('sto-checked'); }
}
});
}
const banner = $id('sto-major-banner');
if (banner) banner.style.display = rec.hasMajor ? 'block' : 'none';
}
// ── AIR BRAKE — FULL 8-STEP TEST ──────────────────────────────────────────
const AB = {
results: {}, // stepNum -> 'pass'|'fail'
timers: { rel: null, app: null },
timeLeft: { rel: 60, app: 60 },
duration: 60
};
function formatTime(secs) {
const m = Math.floor(secs / 60), s = secs % 60;
return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}
function abSetResult(step, result) {
AB.results[step] = result;
const chip = $id(`ab-chip-${step}`);
if (chip) {
chip.textContent = result === 'pass' ? 'PASS' : 'FAIL';
chip.className = `ab-result-chip ${result === 'pass' ? 'ab-chip-pass' : 'ab-chip-fail'}`;
}
// Check if all 8 done
const allDone = [1,2,3,4,5,6,7,8].every(n => AB.results[n]);
if (allDone) abShowSummary();
// Update progress bar
const done = Object.keys(AB.results).length;
const bar = $id('ab-step-bar');
const lbl = $id('ab-step-label');
if (bar) bar.style.width = Math.min(100, Math.round(done / 8 * 100)) + '%';
if (lbl) lbl.textContent = `${done} of 8 complete`;
}
function abShowSummary() {
const summary = $id('ab-summary');
const body = $id('ab-summary-body');
if (!summary || !body) return;
const stepNames = {
1: 'Setup & Initial Build',
2: 'Pressure Build-Up Rate',
3: 'Governor Cut-In',
4: 'Low Air Warning',
5: 'Spring Brake Auto-Apply',
6: 'Leak-Down (Brakes Released)',
7: 'Leak-Down (Brakes Applied)',
8: 'Visual Inspection'
};
const overallPass = [1,2,3,4,5,6,7,8].every(n => AB.results[n] === 'pass');
const rows = [1,2,3,4,5,6,7,8].map(n => {
const r = AB.results[n] || 'not recorded';
const color = r === 'pass' ? 'var(--sto-success)' : r === 'fail' ? 'var(--sto-danger)' : 'var(--sto-text-soft)';
return `
Context: Passengers onboard → treat any readiness issue as Major.
';
if (p === 'no') passCtx = 'Context: No passengers → still required present and mounted. Missing/loose = Major.
';
}
return `
Minor
- ${minorItems}
Major
- ${majorItems}
Note: Verify against applicable FMCSA or Ontario regulations before operational use.
No checklist available for this schedule.
';
return;
}
const rows = items.map((it, idx) => {
const name = `${scheduleKey}_${idx}`;
return `
${esc(it.label)}
${esc(it.help)}
${buildDefectIntel(it.label)}
${buildOilCallout(it.oilNote)}
| Item | Status |
|---|
No inspections saved yet.
';
return;
}
list.innerHTML = S.history.map(r => {
const chip = r.hasMajor
? 'Major Defect'
: 'No Major';
const sched = r.schedule === 'fmcsa' ? 'FMCSA' : `Sched. ${r.schedule}`;
return `
${esc(r.date)} ${esc(r.time)} • ${sched} • ${esc(r.unit)}
Driver: ${esc(r.driver)}
${chip}
Step ${n}: ${stepNames[n]}
${r.toUpperCase()}
`;
}).join('');
const overallColor = overallPass ? 'var(--sto-success)' : 'var(--sto-danger)';
const overallText = overallPass ? 'ALL TESTS PASSED — Vehicle cleared for air brake operation.' : 'ONE OR MORE TESTS FAILED — Do not operate vehicle. Contact maintenance immediately.';
body.innerHTML = `
${rows}
${overallText}
`;
summary.style.display = 'block';
summary.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function abRestart() {
AB.results = {};
AB.timers.rel && clearInterval(AB.timers.rel);
AB.timers.app && clearInterval(AB.timers.app);
AB.timers = { rel: null, app: null };
AB.timeLeft = { rel: 60, app: 60 };
const bar = $id('ab-step-bar');
const lbl = $id('ab-step-label');
if (bar) bar.style.width = '0%';
if (lbl) lbl.textContent = 'Step 1 of 8';
[1,2,3,4,5,6,7,8].forEach(n => {
const chip = $id(`ab-chip-${n}`);
if (chip) { chip.textContent = ''; chip.className = 'ab-result-chip'; }
});
abResetLeakTimer('rel');
abResetLeakTimer('app');
const summary = $id('ab-summary');
if (summary) summary.style.display = 'none';
}
function abStartLeakTimer(which) {
if (AB.timers[which]) return;
const timerEl = $id(`sto-air-timer-${which}`);
const progEl = $id(`sto-air-progress-${which}`);
const startBtn = $id(`sto-btn-air-start-${which}`);
const passBtn = $id(`ab-pass-${which === 'rel' ? 6 : 7}`);
const failBtn = $id(`ab-fail-${which === 'rel' ? 6 : 7}`);
AB.timeLeft[which] = AB.duration;
if (startBtn) startBtn.disabled = true;
if (timerEl) { timerEl.textContent = formatTime(AB.duration); timerEl.classList.remove('sto-timer-done'); }
if (progEl) { progEl.style.width = '0%'; progEl.classList.remove('sto-timer-done'); }
AB.timers[which] = setInterval(() => {
AB.timeLeft[which] = Math.max(0, AB.timeLeft[which] - 1);
const pct = ((AB.duration - AB.timeLeft[which]) / AB.duration) * 100;
if (timerEl) timerEl.textContent = formatTime(AB.timeLeft[which]);
if (progEl) progEl.style.width = pct + '%';
if (AB.timeLeft[which] === 0) {
clearInterval(AB.timers[which]); AB.timers[which] = null;
if (timerEl) timerEl.classList.add('sto-timer-done');
if (progEl) progEl.classList.add('sto-timer-done');
if (passBtn) passBtn.disabled = false;
if (failBtn) failBtn.disabled = false;
}
}, 1000);
}
function abResetLeakTimer(which) {
if (AB.timers[which]) { clearInterval(AB.timers[which]); AB.timers[which] = null; }
AB.timeLeft[which] = AB.duration;
const timerEl = $id(`sto-air-timer-${which}`);
const progEl = $id(`sto-air-progress-${which}`);
const startBtn = $id(`sto-btn-air-start-${which}`);
const passBtn = $id(`ab-pass-${which === 'rel' ? 6 : 7}`);
const failBtn = $id(`ab-fail-${which === 'rel' ? 6 : 7}`);
if (timerEl) { timerEl.textContent = formatTime(AB.duration); timerEl.classList.remove('sto-timer-done'); }
if (progEl) { progEl.style.width = '0%'; progEl.classList.remove('sto-timer-done'); }
if (startBtn) startBtn.disabled = false;
if (passBtn) passBtn.disabled = true;
if (failBtn) failBtn.disabled = true;
}
function wireAirBrakeEvents() {
// Pass/fail buttons for steps 1–5 and 8
app.querySelectorAll('.ab-pass-btn[data-ab-pass]').forEach(btn => {
btn.addEventListener('click', () => abSetResult(parseInt(btn.dataset.abPass), 'pass'));
});
app.querySelectorAll('.ab-fail-btn[data-ab-fail]').forEach(btn => {
btn.addEventListener('click', () => abSetResult(parseInt(btn.dataset.abFail), 'fail'));
});
// Leak-down timers
const rStart = $id('sto-btn-air-start-rel');
const rReset = $id('sto-btn-air-reset-rel');
const aStart = $id('sto-btn-air-start-app');
const aReset = $id('sto-btn-air-reset-app');
if (rStart) rStart.addEventListener('click', () => abStartLeakTimer('rel'));
if (rReset) rReset.addEventListener('click', () => abResetLeakTimer('rel'));
if (aStart) aStart.addEventListener('click', () => abStartLeakTimer('app'));
if (aReset) aReset.addEventListener('click', () => abResetLeakTimer('app'));
// Restart button
const restart = $id('ab-btn-restart');
if (restart) restart.addEventListener('click', abRestart);
}
// ── SIGNATURE PAD ──────────────────────────────────────────────────────────
function resetSig(wipe) {
S.sig.hasContent = false;
S.sig.points = 0;
S.sig.drawing = false;
if (wipe) {
const canvas = $id('sto-sig-canvas');
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
}
function initSig() {
const canvas = $id('sto-sig-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#111827';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
function pos(e) {
const r = canvas.getBoundingClientRect();
const scX = canvas.width / r.width, scY = canvas.height / r.height;
if (e.touches) return { x: (e.touches[0].clientX - r.left) * scX, y: (e.touches[0].clientY - r.top) * scY };
return { x: (e.clientX - r.left) * scX, y: (e.clientY - r.top) * scY };
}
function start(e) { e.preventDefault(); S.sig.drawing = true; const p = pos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); }
function draw(e) {
e.preventDefault();
if (!S.sig.drawing) return;
const p = pos(e);
ctx.lineTo(p.x, p.y); ctx.stroke();
S.sig.points++;
if (S.sig.points >= MIN_SIG_PTS) S.sig.hasContent = true;
}
function end(e) { if (e) e.preventDefault(); S.sig.drawing = false; }
canvas.addEventListener('mousedown', start);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', end);
canvas.addEventListener('mouseleave', end);
canvas.addEventListener('touchstart', start, { passive: false });
canvas.addEventListener('touchmove', draw, { passive: false });
canvas.addEventListener('touchend', end, { passive: false });
const clearBtn = $id('sto-sig-clear');
if (clearBtn) clearBtn.addEventListener('click', () => resetSig(true));
}
// ── WIRE UI EVENTS ─────────────────────────────────────────────────────────
function wireEvents() {
// Nav
app.querySelectorAll('.sto-nav-btn').forEach(btn => {
btn.addEventListener('click', () => showScreen(btn.dataset.screen));
});
// Home tiles / pill jumps
app.querySelectorAll('[data-jump]').forEach(el => {
el.addEventListener('click', () => showScreen(el.dataset.jump));
});
// Schedule pills
app.querySelectorAll('#sto-schedule-btns .sto-pill-button').forEach(btn => {
btn.addEventListener('click', () => setSchedule(btn.dataset.schedule));
});
// Passenger context
const passCtx = $id('sto-ctx-passengers');
if (passCtx) passCtx.addEventListener('change', () => {
S.contextPassengers = passCtx.value;
buildChecklist(S.schedule);
});
// Inspection buttons
const btnSave = $id('sto-btn-save');
const btnPrint = $id('sto-btn-print');
const btnClear = $id('sto-btn-clear');
if (btnSave) btnSave.addEventListener('click', saveInspection);
if (btnPrint) btnPrint.addEventListener('click', () => window.print());
if (btnClear) btnClear.addEventListener('click', clearForm);
// Air brake
wireAirBrakeEvents();
// Officer mode
const offStart = $id('sto-btn-officer-start');
const offExit = $id('sto-btn-officer-exit');
if (offStart) offStart.addEventListener('click', () => {
S.officerMode = true; saveOfficerMode(); applyOfficerModeUI(); alert('Officer Mode active. Hand device to officer.');
});
if (offExit) offExit.addEventListener('click', () => {
const pinInput = $id('sto-officer-pin-input');
const entered = pinInput ? pinInput.value.trim() : '';
const stored = (S.settings.officerPin || '').trim();
if (!stored) { S.officerMode = false; saveOfficerMode(); applyOfficerModeUI(); alert('Officer Mode deactivated.'); return; }
if (entered === stored) {
S.officerMode = false; saveOfficerMode(); applyOfficerModeUI();
if (pinInput) pinInput.value = '';
alert('Officer Mode deactivated.');
} else {
alert('Incorrect PIN.');
}
});
// Settings
const btnSaveSettings = $id('sto-btn-save-settings');
const btnResetSettings = $id('sto-btn-reset-settings');
if (btnSaveSettings) btnSaveSettings.addEventListener('click', saveSettingsFromForm);
if (btnResetSettings) btnResetSettings.addEventListener('click', () => {
if (!confirm('Reset ALL settings and history from this device?')) return;
try { localStorage.removeItem(KEY_HISTORY); localStorage.removeItem(KEY_SETTINGS); localStorage.removeItem(KEY_OFFICER); } catch(e){}
S.history = []; S.settings = {}; S.officerMode = false;
alert('All data cleared.');
showScreen('home');
});
}
// ── INIT ───────────────────────────────────────────────────────────────────
function init() {
loadSettings();
loadHistory();
loadOfficerMode();
applyOfficerModeUI();
setSchedule('fmcsa');
prefillDateTime();
prefillFromSettings();
initSig();
wireEvents();
}
init();
})();
