Home
BB Tractors
Bad Boy Mowers
Husqvarna
Orders
Unit Finder
Work Orders
Delivery
Nexus
Brickhouse Services
Pickup & Delivery
Today’s Route
+ Add Sale
Sales Deliveries
Service P&D
Schedule Calendar
Picked Up / Future Service
All Statuses
Refresh
Clear Route
Showing all deliveries
‹ Previous
This Month
Next ›
Schedule
Add Sale
Close
PHPPOS Sale ID
Confirm Imported Sale
Close
Edit Delivery
Close
Status
Delivery Date
Driver
Notes
Delivery Accessories
Close
Select PDI Type
Close
🧰
Mower PDI
Visual mower map, mower hours, and mower serial scanner cards.
🚜
Tractor PDI
Typed model info, serial scanner cards, and tractor checklist sections.
Serial fields can be scanned or entered manually. Nothing is forced if the information is not available.
PDI
Model serial will be loaded from the delivery card.
Close
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1, user-scalable=no" /> <title>Brickhouse PDI Checklist</title> <style> :root{ --navy:#00254a; --teal:#36b1b3; --bg:#eef3f7; --card:#ffffff; --text:#142033; --muted:#6b7280; --good:#16a34a; --warn:#f59e0b; --bad:#dc2626; --line:#dbe3ea; } *{box-sizing:border-box} body{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; background:linear-gradient(180deg,#f8fbfd,#eaf1f6); color:var(--text); -webkit-text-size-adjust:100%; overscroll-behavior:none; } .app{ max-width:920px; margin:0 auto; padding:calc(env(safe-area-inset-top) + 14px) 14px calc(env(safe-area-inset-bottom) + 92px); } .header{ background:linear-gradient(135deg,var(--navy),#07345f); color:white; border-radius:24px; padding:18px; box-shadow:0 12px 30px rgba(0,37,74,.22); } .header-top{ display:flex; justify-content:space-between; align-items:flex-start; gap:12px; } .brand{ font-size:13px; opacity:.85; font-weight:700; letter-spacing:.08em; text-transform:uppercase; } h1{ margin:4px 0 6px; font-size:24px; line-height:1.05; } .sub{ margin:0; opacity:.85; font-size:14px; } .progress-pill{ background:rgba(255,255,255,.15); border:1px solid rgba(255,255,255,.25); padding:8px 10px; border-radius:999px; font-size:13px; font-weight:800; white-space:nowrap; } .progress-wrap{ margin-top:16px; height:12px; background:rgba(255,255,255,.22); border-radius:999px; overflow:hidden; } .progress-bar{ height:100%; width:0%; background:var(--teal); border-radius:999px; transition:.25s ease; } .section-title{ margin:20px 4px 10px; font-size:15px; color:#24364d; font-weight:950; display:flex; align-items:center; justify-content:space-between; gap:10px; } .section-hint{ color:var(--muted); font-size:12px; font-weight:800; } .grid{ display:grid; grid-template-columns:1fr; gap:12px; } @media(min-width:700px){ .grid.two{grid-template-columns:1fr 1fr} .grid.three{grid-template-columns:repeat(3,1fr)} } .card{ background:rgba(255,255,255,.92); border:1px solid rgba(219,227,234,.85); border-radius:22px; padding:14px; box-shadow:0 8px 24px rgba(15,23,42,.06); } .info-grid{ display:grid; grid-template-columns:1fr; gap:12px; } @media(min-width:720px){ .info-grid{grid-template-columns:1fr 1fr} } .field label{ display:block; color:var(--muted); font-size:12px; font-weight:850; margin:0 0 5px 2px; text-transform:uppercase; letter-spacing:.04em; } input,textarea,select{ width:100%; border:1px solid var(--line); border-radius:15px; padding:13px; font-size:16px; outline:none; background:#fbfdff; } input:focus,textarea:focus,select:focus{ border-color:var(--teal); box-shadow:0 0 0 4px rgba(54,177,179,.16); } textarea{min-height:110px;resize:vertical} .scan-card{ display:flex; align-items:center; gap:12px; min-height:92px; cursor:pointer; user-select:none; touch-action:manipulation; border:1px solid rgba(54,177,179,.22); } .scan-icon{ width:54px; height:54px; border-radius:18px; display:grid; place-items:center; background:#e8f7f7; color:var(--teal); font-size:25px; flex:0 0 auto; } .scan-card.done .scan-icon{ background:#eaf8ef; color:var(--good); } .scan-main{ min-width:0; flex:1; } .scan-label{ font-weight:950; margin-bottom:4px; } .scan-value{ color:var(--muted); font-size:13px; word-break:break-all; } .scan-card.done .scan-value{ color:var(--good); font-weight:850; } .small-btn{ border:0; background:#f0f6f9; color:var(--navy); font-weight:950; border-radius:999px; padding:9px 11px; } .mower-map.real{ position:relative; padding:0; overflow:hidden; background:#f4f8fb; min-height:auto; } .mower-img{ width:100%; display:block; user-select:none; pointer-events:none; } .zone{ position:absolute; border:0; border-radius:999px; background:white; color:var(--navy); box-shadow:0 8px 18px rgba(0,0,0,.22); font-weight:1000; padding:10px 13px; font-size:12px; cursor:pointer; z-index:5; } .zone.complete{ background:#eaf8ef; color:var(--good); box-shadow:0 0 0 3px rgba(22,163,74,.18), 0 8px 18px rgba(0,0,0,.18); } .z-final{left:50%;top:13%;transform:translateX(-50%)} .z-deck{left:50%;top:31%;transform:translateX(-50%)} .z-controls{left:50%;top:53%;transform:translateX(-50%)} .z-left{left:9%;top:53%} .z-right{right:9%;top:53%} .z-engine{left:50%;bottom:8%;transform:translateX(-50%)} @media(max-width:420px){ .zone{font-size:11px;padding:8px 10px} .z-left{left:5%} .z-right{right:5%} } .check-section{ display:grid; gap:10px; } .check-section h2{ margin:0; font-size:17px; color:var(--navy); } .check-list{ display:grid; gap:10px; } .check-item{ display:flex; gap:10px; align-items:flex-start; background:#f8fbfd; border:1px solid var(--line); border-radius:16px; padding:12px; } .check-item input{ width:22px; height:22px; accent-color:var(--teal); flex:0 0 auto; margin-top:1px; } .check-item span{ font-size:15px; font-weight:750; line-height:1.35; } .actions{ position:fixed; left:0; right:0; bottom:0; padding:10px 14px calc(env(safe-area-inset-bottom) + 10px); background:rgba(248,251,253,.88); backdrop-filter:blur(16px); border-top:1px solid rgba(219,227,234,.9); z-index:30; } .action-inner{ max-width:920px; margin:0 auto; display:grid; grid-template-columns:1fr 1fr; gap:10px; } .btn{ border:0; border-radius:18px; padding:15px; font-size:16px; font-weight:1000; cursor:pointer; } .btn.primary{background:var(--teal);color:white} .btn.secondary{background:white;color:var(--navy);border:1px solid var(--line)} .modal{ position:fixed; inset:0; background:rgba(0,20,40,.45); display:none; align-items:flex-end; z-index:99; } .modal.show{display:flex} .sheet{ background:white; width:100%; max-width:920px; margin:0 auto; border-radius:28px 28px 0 0; padding:16px; max-height:88vh; overflow:auto; } .sheet-head{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:12px; } .sheet h2{margin:0;font-size:21px} .close{ border:0; width:42px; height:42px; border-radius:50%; background:#f1f5f9; font-size:22px; } .scan-view{ position:relative; display:none; border-radius:20px; overflow:hidden; background:#0b1320; min-height:220px; border:1px solid rgba(255,255,255,.08); margin-bottom:12px; } .scan-view video{ width:100%; height:280px; object-fit:cover; display:block; background:#09111d; } .scan-frame{ position:absolute; inset:50% auto auto 50%; width:min(80%, 320px); height:80px; transform:translate(-50%, -50%); border:2px solid rgba(255,255,255,.9); border-radius:14px; box-shadow:0 0 0 9999px rgba(0,0,0,.18); pointer-events:none; } .scan-help{ position:absolute; left:10px; right:10px; bottom:10px; color:rgba(255,255,255,.94); background:rgba(0,0,0,.36); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); border-radius:12px; padding:9px 10px; font-size:13px; line-height:1.35; } .scan-actions{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:12px; } .hidden{display:none!important} </style> </head> <body> <div class="app"> <section class="header"> <div class="header-top"> <div> <div class="brand">Brickhouse Services</div> <h1 id="pdiTitle">PDI Checklist</h1> <p class="sub" id="pdiSubtitle">Mobile inspection flow.</p> </div> <div class="progress-pill" id="progressText">0%</div> </div> <div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div> </section> <div class="section-title">Inspection Info <span class="section-hint">Typed fields</span></div> <section class="card"> <div class="info-grid" id="infoFields"></div> </section> <div class="section-title">Serial Numbers / Scanner <span class="section-hint">Tap a card to scan or enter manually</span></div> <section class="grid two" id="serialFields"></section> <div id="mowerMapBlock"> <div class="section-title">Graphical Inspection Map</div> <section class="card mower-map real"> <img src="MowerPDI.png" class="mower-img" alt="Bad Boy mower inspection map"> <button class="zone z-final" data-zone="visual">Visual</button> <button class="zone z-engine" data-zone="engine">Engine</button> <button class="zone z-left" data-zone="left">Left Hydro</button> <button class="zone z-right" data-zone="right">Right Hydro</button> <button class="zone z-deck" data-zone="deck">Deck</button> <button class="zone z-controls" data-zone="controls">Controls</button> </section> </div> <div id="checklistBlock"></div> <div class="section-title">Final Notes</div> <section class="card"> <textarea id="inspectionNotes" placeholder="Inspection notes, damage, missing items, adjustments made..."></textarea> </section> </div> <div class="actions"> <div class="action-inner"> <button class="btn secondary" onclick="resetPDI()">Reset</button> <button class="btn primary" onclick="completePDI()">Complete PDI</button> </div> </div> <div class="modal" id="zoneModal"> <div class="sheet"> <div class="sheet-head"> <h2 id="zoneTitle">Inspection</h2> <button class="close" onclick="closeZone()">×</button> </div> <div id="zoneChecks" class="check-list"></div> </div> </div> <div class="modal" id="scanModal"> <div class="sheet"> <div class="sheet-head"> <div> <h2 id="scanTitle">Scan Serial</h2> <p class="sub" style="color:var(--muted);margin-top:3px;">Hold the barcode inside the box, or enter it manually below.</p> </div> <button class="close" onclick="closeScan()">×</button> </div> <div class="scan-view" id="scanView"> <video id="video" playsinline muted></video> <div class="scan-frame"></div> <div class="scan-help" id="scanHelp">Point the camera at a barcode or QR code on the serial label.</div> </div> <div class="field"> <label>Serial Number</label> <input id="manualSerial" placeholder="Enter or scan serial" autocomplete="off"> </div> <div class="scan-actions"> <button class="btn secondary" type="button" onclick="closeScan()">Close</button> <button class="btn primary" type="button" onclick="saveSerial()">Save Serial</button> </div> </div> </div> <script> const PDI_API_URL = ''; // Injected by the parent delivery app. Firebase only. const PDI_TYPE = window.BRICKHOUSE_PDI_TYPE || "mower"; const infoFieldDefs = { mowerHours: { label: "Mower Hours", type: "number", placeholder: "Optional" }, tractorModel: { label: "Tractor Model", type: "text", placeholder: "Enter tractor model" }, loaderModel: { label: "Loader Model", type: "text", placeholder: "Optional" }, backhoeModel: { label: "Backhoe Model", type: "text", placeholder: "Optional" }, bellyMowerModel: { label: "Belly Mower Model", type: "text", placeholder: "Optional" }, hourMeter: { label: "Hour Meter Reading", type: "number", placeholder: "Optional" } }; const serialFieldDefs = { mowerSerial: { label: "Mower Serial", icon: "📷" }, mowerDeckSerial: { label: "Mower Deck Serial", icon: "🧩" }, engineSerial: { label: "Engine Serial", icon: "⚙️" }, leftHydroSerial: { label: "Left Hydro Transmission", icon: "⬅️" }, rightHydroSerial: { label: "Right Hydro Transmission", icon: "➡️" }, tractorSerial: { label: "Tractor Serial #", icon: "🚜" }, loaderSerial: { label: "Loader Serial #", icon: "🏗️" }, backhoeSerial: { label: "Backhoe Serial #", icon: "🪣" }, bellyMowerSerial: { label: "Belly Mower Serial #", icon: "🧩" } }; const pdiConfigs = { mower: { title: "Mower PDI Checklist", subtitle: "Mower inspection with serial scanning, mower hours, and visual checklist map.", equipmentLabel: "Mower", infoFields: ["mowerHours"], serialFields: ["mowerSerial", "mowerDeckSerial", "engineSerial", "leftHydroSerial", "rightHydroSerial"], usesImageMap: true }, tractor: { title: "Tractor Pre-Delivery Checklist", subtitle: "Tractor PDI with typed model info, scannable serials, and checklist sections.", equipmentLabel: "Tractor", infoFields: ["tractorModel", "loaderModel", "backhoeModel", "bellyMowerModel", "hourMeter"], serialFields: ["tractorSerial", "loaderSerial", "backhoeSerial", "bellyMowerSerial"], usesImageMap: false } }; const config = pdiConfigs[PDI_TYPE] || pdiConfigs.mower; const state = { info: { mowerHours: "", tractorModel: "", loaderModel: "", backhoeModel: "", bellyMowerModel: "", hourMeter: "" }, serials: { mowerSerial: "", mowerDeckSerial: "", engineSerial: "", leftHydroSerial: "", rightHydroSerial: "", tractorSerial: "", loaderSerial: "", backhoeSerial: "", bellyMowerSerial: "" }, zones: { visual: {}, engine: {}, left: {}, right: {}, deck: {}, controls: {} }, checklist: {} }; const zones = { visual: { title: "Visual & Walk-Around", items: [ "Owner’s and engine manual / packet present", "Overall visual inspection complete", "Paint damage checked", "Decal damage checked", "Freight damage checked", "General walk-around inspection complete", "Loose fasteners and hoses checked", "ROPS properly installed" ] }, engine: { title: "Engine Area", items: [ "Engine oil level checked", "Engine coolant checked if applicable", "Throttle and choke cables set properly", "Throttle and choke cables routed properly", "Engine operating at correct RPM between 3500–3600", "No engine-area leaks found" ] }, left: { title: "Left Side / Drive", items: [ "Left side hydro serial captured", "Drive system oil level checked", "Left side hoses checked", "Left side pivot points move freely", "Left side pivot points lubricated", "Left tire air pressure checked" ] }, right: { title: "Right Side / Drive", items: [ "Right side hydro serial captured", "Drive system oil level checked", "Right side hoses checked", "Right side pivot points move freely", "Right side pivot points lubricated", "Right tire air pressure checked" ] }, deck: { title: "Deck / Belts / Blades", items: [ "All belts routed properly", "Belt tension checked", "Deck level and pitch correct", "Deck spindles installed properly", "Blades installed properly", "Pulleys installed properly", "Blade engagement tested" ] }, controls: { title: "Controls / Safety / Final Run", items: [ "All lug nuts meet torque specifications", "Parking brake functions properly", "All safety switches function properly", "Mower operated for several minutes", "All systems functioning properly", "Checked for leaks", "Steering tracking checked" ] } }; const tractorSections = { condition: { title: "Condition", items: [ "Tractor is clean, free of scratches / dents", "Safety / operational decals in place, legible, and in good condition", "All guards in position", "Equipped with slow moving sign", "Operator manual(s) present", "ROPS / cab, roof, windshield, wiper assembly checked if equipped", "All grease / lube points checked for correct lubrication", "Wheel mounting bolts and nuts properly torqued", "Tire pressure checked: LF / RF / LR / RR", "Seat and seat belt operation checked" ] }, startWarmUp: { title: "Start & Warm Up", items: [ "Lights and direction indicators checked", "Safety start switches checked", "Engine RPM / low idle / high idle checked", "Heater and air conditioner checked if equipped", "Parking brake operation and adjustment checked", "All operator station controls checked", "Gauges and warning lamps checked" ] }, fluids: { title: "Fluid & General Checks", items: [ "Engine oil level checked", "Fuel level checked", "Coolant level checked", "Transmission / hydraulic oil level checked", "Power steering fluid level checked if equipped", "Brake fluid level checked if equipped", "Front axle and planetary hubs fluid level checked if 4WD", "All drain plugs correctly installed", "All filters in position and correctly tightened", "Battery fully charged and cables secure", "Radiator / screen, coolant lines and clamps checked if equipped", "Belt tension checked", "Air filter, intake hoses and clamps checked", "Transmission / hydraulic, fuel, coolant, heater, and AC hoses checked", "Drawbar and hitch mounting checked" ] }, operation: { title: "Operation", items: [ "Clutch operation checked", "Brake operation checked", "All hydraulic circuits function tested", "All transmission speeds / ranges checked", "HST operation checked", "Power steering checked", "4WD operation checked", "3-point hitch operation checked", "All safety switches checked", "After operation: checked for oil, coolant, and fuel leaks", "After operation: checked there are no current DTCs" ] }, customerAcceptance: { title: "Customer Acceptance / Delivery Review", items: [ "Customer received the Operator Manual(s)", "Customer inspected the tractor and it meets their satisfaction", "Dealer reviewed the importance of proper maintenance", "Dealer explained removal / installation / storage of attachments and implements", "Customer understands only Bad Boy branded attachments and implements are tested and approved for the tractor", "Customer received and understands the Bad Boy Tractor Limited Warranty Policy", "Customer received a copy of this document" ] } }; let currentZone = null; let currentScanField = null; let stream = null; let detector = null; let scanning = false; let scanTimer = null; let lastScannedValue = ""; document.addEventListener("DOMContentLoaded", () => { document.getElementById("datePerformed")?.remove(); renderPDI(); applyPrefill(); loadTechs(); updateUI(); }); function renderPDI(){ document.getElementById("pdiTitle").textContent = config.title; document.getElementById("pdiSubtitle").textContent = config.subtitle; const infoHolder = document.getElementById("infoFields"); infoHolder.innerHTML = ` <div class="field"> <label>Date Performed</label> <input id="datePerformed" type="date"> </div> <div class="field"> <label>Inspection Performed By</label> <select id="techName"><option value="">Loading techs...</option></select> </div> `; config.infoFields.forEach(field => { const def = infoFieldDefs[field]; const wrap = document.createElement("div"); wrap.className = "field"; wrap.innerHTML = ` <label>${def.label}</label> <input id="${field}" type="${def.type || "text"}" placeholder="${def.placeholder || ""}"> `; infoHolder.appendChild(wrap); }); document.getElementById("datePerformed").valueAsDate = new Date(); const serialHolder = document.getElementById("serialFields"); serialHolder.innerHTML = ""; config.serialFields.forEach(field => { const def = serialFieldDefs[field]; const card = document.createElement("div"); card.className = "card scan-card"; card.dataset.scan = field; card.innerHTML = ` <div class="scan-icon">${def.icon || "📷"}</div> <div class="scan-main"> <div class="scan-label">${def.label}</div> <div class="scan-value" id="${field}Text">Tap to scan or enter</div> </div> <button class="small-btn" type="button">Edit</button> `; card.addEventListener("click", () => openScan(field)); serialHolder.appendChild(card); }); document.getElementById("mowerMapBlock").classList.toggle("hidden", !config.usesImageMap); if (config.usesImageMap) { document.querySelectorAll(".zone").forEach(btn => { btn.addEventListener("click", () => openZone(btn.dataset.zone)); }); document.getElementById("checklistBlock").innerHTML = ""; } else { renderChecklistSections(); } ["datePerformed","techName","inspectionNotes", ...config.infoFields].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener("input", () => { if (state.info.hasOwnProperty(id)) state.info[id] = el.value; savePDI(); updateUI(); }); }); } function renderChecklistSections(){ const holder = document.getElementById("checklistBlock"); holder.innerHTML = ""; Object.entries(tractorSections).forEach(([sectionKey, section]) => { if (!state.checklist[sectionKey]) state.checklist[sectionKey] = {}; const block = document.createElement("section"); block.className = "card check-section"; block.innerHTML = `<h2>${section.title}</h2><div class="check-list"></div>`; const list = block.querySelector(".check-list"); section.items.forEach((item, index) => { const key = `item_${index}`; const checked = !!state.checklist[sectionKey][key]; const label = document.createElement("label"); label.className = "check-item"; label.innerHTML = ` <input type="checkbox" ${checked ? "checked" : ""}> <span>${item}</span> `; label.querySelector("input").addEventListener("change", event => { state.checklist[sectionKey][key] = event.target.checked; savePDI(); updateUI(); }); list.appendChild(label); }); const title = document.createElement("div"); title.className = "section-title"; title.textContent = section.title; holder.appendChild(title); holder.appendChild(block); }); } async function loadTechs(){ const select = document.getElementById("techName"); const savedName = select.value || ""; select.innerHTML = '<option value="">Loading technicians from Firebase...</option>'; if (!PDI_API_URL) { select.innerHTML = '<option value="">Firebase PDI API unavailable</option>'; return; } try { const url = new URL(PDI_API_URL); url.searchParams.set("action", "techs"); const res = await fetch(url.toString(), { cache: "no-store" }); const data = await res.json(); select.innerHTML = '<option value="">Select technician</option>'; const names = Array.isArray(data.techs) ? data.techs : Array.isArray(data.data) ? data.data : []; if (!data.ok || !names.length) { select.innerHTML = '<option value="">No active technicians found in Firebase</option>'; return; } names.forEach(item => { const name = typeof item === "string" ? item : (item.name || item.tech_name || item.display_name || ""); if (!name) return; const option = document.createElement("option"); option.value = name; option.textContent = name; select.appendChild(option); }); if (savedName) select.value = savedName; } catch (err) { select.innerHTML = '<option value="">Could not load technicians from Firebase</option>'; } } function applyPrefill(){ const model = window.BRICKHOUSE_EQUIPMENT_MODEL || window.BRICKHOUSE_MOWER_MODEL || ""; const serial = window.BRICKHOUSE_PREFILLED_EQUIPMENT_SERIAL || window.BRICKHOUSE_PREFILLED_MOWER_SERIAL || ""; if (PDI_TYPE === "tractor") { if (model && document.getElementById("tractorModel")) { document.getElementById("tractorModel").value = model; state.info.tractorModel = model; } if (serial) state.serials.tractorSerial = serial; } else { if (serial) state.serials.mowerSerial = serial; } savePDI(); } function openZone(zoneKey){ currentZone = zoneKey; const zone = zones[zoneKey]; document.getElementById("zoneTitle").textContent = zone.title; const holder = document.getElementById("zoneChecks"); holder.innerHTML = ""; zone.items.forEach((item, index) => { const key = `item_${index}`; const checked = !!state.zones[zoneKey][key]; const label = document.createElement("label"); label.className = "check-item"; label.innerHTML = ` <input type="checkbox" ${checked ? "checked" : ""}> <span>${item}</span> `; label.querySelector("input").addEventListener("change", event => { toggleCheck(zoneKey, key, event.target.checked); }); holder.appendChild(label); }); document.getElementById("zoneModal").classList.add("show"); } function closeZone(){ document.getElementById("zoneModal").classList.remove("show"); currentZone = null; } function toggleCheck(zoneKey, key, checked){ state.zones[zoneKey][key] = checked; savePDI(); updateUI(); } function openScan(field){ currentScanField = field; document.getElementById("scanTitle").textContent = serialFieldDefs[field]?.label || "Scan Serial"; document.getElementById("manualSerial").value = state.serials[field] || ""; document.getElementById("scanModal").classList.add("show"); startScanner(); } function sanitizeSerial(value){ return String(value || "") .trim() .replace(/\s+/g, "") .replace(/[^A-Za-z0-9\-._/]/g, "") .toUpperCase(); } function extractSerialCandidates(rawText){ const raw = String(rawText || "").trim(); if (!raw) return []; const out = []; const add = value => { const cleaned = sanitizeSerial(value); if (cleaned && cleaned.length >= 4 && !out.includes(cleaned)) out.push(cleaned); }; [...raw.matchAll(/(?:serial(?:\s*number)?|s\/?n)\s*[:#-]?\s*([A-Za-z0-9\-._/]+)/gi)].forEach(match => add(match[1])); raw.split(/[\r\n,;|]+/).forEach(part => add(part)); add(raw); return out.slice(0, 8); } async function startScanner(){ const scanView = document.getElementById("scanView"); const video = document.getElementById("video"); const help = document.getElementById("scanHelp"); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { scanView.style.display = "none"; if (help) help.textContent = "Camera not supported on this device. Enter serial manually."; return; } try { stopScanner(); stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); video.srcObject = stream; await video.play(); scanView.style.display = "block"; if (help) help.textContent = "Looking for a barcode or QR code..."; if ("BarcodeDetector" in window) { const supported = await BarcodeDetector.getSupportedFormats(); const preferred = ["code_39", "code_93", "code_128", "ean_13", "ean_8", "upc_a", "upc_e", "qr_code", "data_matrix", "pdf417"].filter(f => supported.includes(f)); detector = new BarcodeDetector({ formats: preferred.length ? preferred : supported }); scanning = true; lastScannedValue = ""; scanLoop(); } else if (help) { help.textContent = "Camera started. BarcodeDetector is not available, so enter the serial manually."; } } catch (err) { scanView.style.display = "none"; if (help) help.textContent = "Camera could not start. Check camera permissions."; } } async function scanLoop(){ const video = document.getElementById("video"); const help = document.getElementById("scanHelp"); if (!scanning || !detector || !video.videoWidth) { scanTimer = setTimeout(scanLoop, 240); return; } try { const codes = await detector.detect(video); if (codes && codes.length) { const raw = codes[0].rawValue || ""; const candidates = extractSerialCandidates(raw); const value = candidates[0] || sanitizeSerial(raw); if (value && value !== lastScannedValue) { lastScannedValue = value; document.getElementById("manualSerial").value = value; if (help) help.textContent = "Scanned: " + value; if (navigator.vibrate) navigator.vibrate(80); stopScanner(); return; } } } catch(e){} scanTimer = setTimeout(scanLoop, 200); } function stopScanner(){ scanning = false; detector = null; if (scanTimer) { clearTimeout(scanTimer); scanTimer = null; } if (stream) { stream.getTracks().forEach(track => track.stop()); stream = null; } const video = document.getElementById("video"); if (video) video.srcObject = null; } function closeScan(){ stopScanner(); document.getElementById("scanModal").classList.remove("show"); currentScanField = null; } function saveSerial(){ const value = sanitizeSerial(document.getElementById("manualSerial").value); if (!currentScanField) return; state.serials[currentScanField] = value; savePDI(); updateUI(); closeScan(); } function updateUI(){ config.serialFields.forEach(field => { const value = state.serials[field]; const text = document.getElementById(field + "Text"); const card = document.querySelector(`[data-scan="${field}"]`); if (!text || !card) return; if (value) { text.textContent = value; card.classList.add("done"); } else { text.textContent = "Tap to scan or enter"; card.classList.remove("done"); } }); let total = 0; let done = 0; config.infoFields.forEach(field => { total += 1; const el = document.getElementById(field); if (el) state.info[field] = el.value; if (state.info[field]) done += 1; }); config.serialFields.forEach(field => { total += 1; if (state.serials[field]) done += 1; }); if (config.usesImageMap) { Object.keys(zones).forEach(zoneKey => { const count = zones[zoneKey].items.length; const zoneDone = Object.values(state.zones[zoneKey]).filter(Boolean).length; total += count; done += zoneDone; const btn = document.querySelector(`[data-zone="${zoneKey}"]`); if (btn) btn.classList.toggle("complete", zoneDone === count); }); } else { Object.entries(tractorSections).forEach(([sectionKey, section]) => { total += section.items.length; done += Object.values(state.checklist[sectionKey] || {}).filter(Boolean).length; }); } const percent = total ? Math.round((done / total) * 100) : 0; document.getElementById("progressText").textContent = `${percent}%`; document.getElementById("progressBar").style.width = percent + "%"; return percent; } function buildDraftPayload(){ config.infoFields.forEach(field => { const el = document.getElementById(field); if (el) state.info[field] = el.value; }); return { pdi_type: PDI_TYPE, pdi_label: config.title, equipment_type: config.equipmentLabel, state, info: { datePerformed: document.getElementById("datePerformed").value, techName: document.getElementById("techName").value, inspectionNotes: document.getElementById("inspectionNotes").value, ...state.info }, delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", sale_id: window.BRICKHOUSE_SALE_ID || "", updated_at: new Date().toISOString() }; } function savePDI(){ window.parent.postMessage({ type: "BRICKHOUSE_FIRST_BUILD_PDI_DRAFT", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", payload: buildDraftPayload() }, "*"); } function resetPDI(){ if (!confirm("Reset this PDI checklist?")) return; Object.keys(state.info).forEach(key => state.info[key] = ""); Object.keys(state.serials).forEach(key => state.serials[key] = ""); Object.keys(state.zones).forEach(zoneKey => state.zones[zoneKey] = {}); Object.keys(state.checklist).forEach(sectionKey => state.checklist[sectionKey] = {}); document.getElementById("inspectionNotes").value = ""; renderPDI(); applyPrefill(); loadTechs(); updateUI(); savePDI(); } async function completePDI(){ savePDI(); const percent = updateUI(); const draft = buildDraftPayload(); const payload = { action: "saveFirstBuildPDI", pdi_id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now()), pdi_type: PDI_TYPE, pdi_label: config.title, equipment_type: config.equipmentLabel, date_performed: document.getElementById("datePerformed").value, tech_name: document.getElementById("techName").value, serials: state.serials, info: draft.info, zones: state.zones, checklist: state.checklist, progress: percent, status: "Complete", inspection_notes: document.getElementById("inspectionNotes").value, created_from: "brickhouse_delivery_tracker_first_build_pdi", source: "delivery_tracker_first_build_modal", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", sale_id: window.BRICKHOUSE_SALE_ID || "", customer_name: window.BRICKHOUSE_CUSTOMER_NAME || "", customer_phone: window.BRICKHOUSE_CUSTOMER_PHONE || "", customer_address: window.BRICKHOUSE_CUSTOMER_ADDRESS || "", mower_model: window.BRICKHOUSE_MOWER_MODEL || "", mower_hours: state.info.mowerHours || "", mower_serial: state.serials.mowerSerial || "", mower_deck_serial: state.serials.mowerDeckSerial || "", engine_serial: state.serials.engineSerial || "", left_hydro_serial: state.serials.leftHydroSerial || "", right_hydro_serial: state.serials.rightHydroSerial || "", tractor_model: state.info.tractorModel || "", tractor_serial: state.serials.tractorSerial || "", loader_model: state.info.loaderModel || "", loader_serial: state.serials.loaderSerial || "", backhoe_model: state.info.backhoeModel || "", backhoe_serial: state.serials.backhoeSerial || "", belly_mower_model: state.info.bellyMowerModel || "", belly_mower_serial: state.serials.bellyMowerSerial || "", attachment_serial: state.serials.bellyMowerSerial || "", hour_meter_reading: state.info.hourMeter || "", pdi_status: "Complete", pdi_complete: true, completed_at: new Date().toISOString(), submitted_at: new Date().toISOString() }; if (!PDI_API_URL) { window.parent.postMessage({ type: "BRICKHOUSE_FIRST_BUILD_PDI_ERROR", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", message: "Firebase PDI API URL is not configured." }, "*"); return; } window.parent.postMessage({ type: "BRICKHOUSE_FIRST_BUILD_PDI_SAVING", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", payload }, "*"); try { const response = await fetch(PDI_API_URL, { method: "POST", headers: { "Content-Type": "text/plain;charset=utf-8" }, body: JSON.stringify(payload) }); const raw = await response.text(); let result = {}; try { result = raw ? JSON.parse(raw) : {}; } catch(e) {} if (!response.ok || result.ok === false) { throw new Error(result.error || "Firebase rejected the PDI save."); } window.parent.postMessage({ type: "BRICKHOUSE_FIRST_BUILD_PDI_COMPLETE", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", payload, result }, "*"); } catch (err) { window.parent.postMessage({ type: "BRICKHOUSE_FIRST_BUILD_PDI_ERROR", delivery_id: window.BRICKHOUSE_DELIVERY_ID || "", message: err.message || "Could not save PDI to Firebase." }, "*"); } } </script> </body> </html>
✓
PDI Complete
PDI saved. Returning to the delivery board.