/
home
/
learningaswinslo
/
public_html
/
Daily Prestart Demo
/
/home/learningaswinslo/public_html/Daily Prestart Demo
mkdir
upload
Name
Size
Mode
Actions
Index.html
28297
0644
edit
dl
rm
Edit:
/home/learningaswinslo/public_html/Daily Prestart Demo/Index.html
(28297B)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="Prestart"> <meta name="theme-color" content="#000000"> <title>Daily Plant Prestart Checklist</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;700&family=Archivo+Narrow:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet"> <style> :root{ /* Winslow primary palette */ --yellow:#EAAB00; --yellow-dark:#ECB319; --yellow-10:#FDF7E5; --yellow-20:#FBEECC; --black:#000000; --grey:#888B95; --grey-dusk:#D9E1F2; --grey-10:#F3F3F4; --grey-20:#E7E8EA; --grey-line:#DBDCDF; --bg:#FFFFFF; --card:#FFFFFF; --ink:#000000; --muted:#5F5E5A; --line:#DBDCDF; --danger:#D50037; --radius:0px; --font-head:'Oswald',Arial,sans-serif; --font-body:'Archivo Narrow',Arial,sans-serif; } *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;} html{-webkit-text-size-adjust:100%;} body{margin:0;font-family:var(--font-body); background:var(--bg);color:var(--ink);line-height:1.5;font-size:16px; -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility; -webkit-overflow-scrolling:touch;overscroll-behavior-y:none; -webkit-user-select:none;user-select:none;-webkit-touch-callout:none; padding-bottom:calc(40px + env(safe-area-inset-bottom));} /* Allow text selection/callout only where it's useful */ input, .transcript, #locationDisplay, #machineDisplay{ -webkit-user-select:text;user-select:text;-webkit-touch-callout:default;} /* Buttons feel native: no zoom, crisp press feedback */ .btn{ -webkit-appearance:none;appearance:none;touch-action:manipulation; transition:transform .06s ease, background .15s ease, box-shadow .08s ease;} .btn:active{transform:scale(.985);box-shadow:inset 0 3px 8px rgba(0,0,0,.45);} header{background:var(--black);color:#fff;position:sticky;top:0;z-index:50; border-bottom:3px solid var(--yellow); padding:18px 16px; padding-top:calc(18px + env(safe-area-inset-top)); padding-left:calc(16px + env(safe-area-inset-left)); padding-right:calc(16px + env(safe-area-inset-right));} .header-inner{display:flex;align-items:center;gap:12px;} header h1{margin:0;font-family:var(--font-head);font-weight:700;font-size:20px; text-transform:uppercase;letter-spacing:.04em;} header .sub{font-family:var(--font-head);font-weight:400;font-size:12px;text-transform:uppercase; letter-spacing:.18em;color:var(--yellow);margin-top:4px;} .logomark{width:26px;height:26px;flex:none;} .wrap{max-width:680px;margin:0 auto;padding:16px; padding-left:calc(16px + env(safe-area-inset-left)); padding-right:calc(16px + env(safe-area-inset-right));} .tabs{display:flex;gap:0;background:var(--grey-10); padding:4px;margin:16px;border:1px solid var(--line);position:sticky;top:70px;z-index:40;} .tab{flex:1;text-align:center;padding:11px;font-family:var(--font-head);font-size:13px;font-weight:500; text-transform:uppercase;letter-spacing:.06em;color:var(--muted);cursor:pointer;border:none;background:none;} .tab.active{background:var(--yellow);color:var(--black);} .card{background:var(--card);border:1px solid var(--line); padding:18px;margin-bottom:14px;} .machine-badge{background:var(--black);color:#fff; padding:16px 18px;display:flex;align-items:center;justify-content:space-between;margin-bottom:16px; border-left:5px solid var(--yellow);} .machine-badge .lbl{font-family:var(--font-head);font-size:11px;text-transform:uppercase; letter-spacing:.14em;color:var(--yellow);} .machine-badge .val{font-family:var(--font-head);font-size:26px;font-weight:700;letter-spacing:.02em;} .machine-row{display:flex;align-items:baseline;gap:14px;width:100%;} .machine-row .lbl{flex:none;} .machine-badge.align-grid .machine-row .lbl{width:140px;} .machine-row .val{line-height:1;} .machine-row .machine-input-inline{text-align:left;width:auto;flex:1;} .machine-badge .val.loc{font-size:16px;font-weight:500;line-height:1.3;letter-spacing:.01em;} .machine-badge input{font-family:var(--font-head);font-size:20px;font-weight:700;border:2px solid var(--yellow); padding:6px 10px;width:140px;color:var(--black);background:#fff;text-align:right;} .op-field{width:100%;} .machine-badge .op-input{font-family:var(--font-head);font-size:20px;font-weight:700; border:2px solid var(--yellow);padding:8px 10px;width:100%;color:var(--black);background:#fff; text-align:left;margin-top:6px;text-transform:uppercase;} .op-input::placeholder{color:var(--grey);font-weight:400;text-transform:none;} .machine-row .op-input{margin-top:0;width:auto;flex:1;font-size:20px;} label.fld{display:block;font-family:var(--font-head);font-size:14px;font-weight:500; text-transform:uppercase;letter-spacing:.04em;margin:0 0 8px;} .btn{border:none;padding:12px 16px;font-family:var(--font-head);font-size:14px;font-weight:500; text-transform:uppercase;letter-spacing:.05em;cursor:pointer;display:inline-flex; align-items:center;justify-content:center;gap:6px;} @keyframes pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.08);}} .btn-primary{background:var(--yellow);color:var(--black);width:100%;font-size:16px;padding:16px;font-weight:700;} .btn-primary:active{background:var(--yellow-dark);} .btn-ghost{background:#fff;color:var(--black);border:1px solid var(--black);} .btn-row{display:flex;gap:10px;margin-top:8px;} input.search{width:100%;border:1px solid var(--line);padding:13px 14px; font-family:var(--font-body);font-size:16px;background:#fff;margin-bottom:14px;} input.search:focus{outline:none;border-color:var(--yellow);} table{width:100%;border-collapse:collapse;font-size:13px;} .tablewrap{overflow-x:auto;border:1px solid var(--line);background:#fff;} th{background:var(--black);color:#fff;text-align:left;padding:9px 10px;font-family:var(--font-head); font-weight:500;text-transform:uppercase;letter-spacing:.03em;white-space:nowrap; border-bottom:2px solid var(--yellow);position:sticky;top:0;font-size:12px;} td{padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top;} tr:last-child td{border-bottom:none;} tr:nth-child(even) td{background:var(--grey-10);} .pill{background:var(--yellow);color:var(--black);padding:2px 8px; font-family:var(--font-head);font-weight:500;white-space:nowrap;} .empty{text-align:center;color:var(--muted);padding:30px 16px;font-size:15px;} .hint{font-size:13px;color:var(--muted);margin:4px 0 0;} .banner{background:var(--yellow-20);color:var(--black);padding:11px 13px; border-left:4px solid var(--yellow);font-size:14px;margin-bottom:14px;} .pay-warning{color:var(--danger);font-family:var(--font-head);font-weight:700; text-transform:uppercase;letter-spacing:.02em;font-size:19px;line-height:1.25; text-align:justify;text-align-last:justify;-moz-text-align-last:justify; margin:0 0 14px;} .listening{font-size:13px;color:var(--danger);font-weight:700;margin-top:6px;display:none;} .listening.on{display:block;} .hide{display:none;} .progress{height:6px;background:var(--grey-20);overflow:hidden;margin-bottom:16px;} #progressBar{height:100%;background:var(--yellow);width:0%;transition:width .25s ease;} .num{font-family:var(--font-head);font-size:11px;color:var(--muted);font-weight:500; text-transform:uppercase;letter-spacing:.1em;} .prompt{font-family:var(--font-head);font-size:24px;font-weight:500;line-height:1.25; text-transform:uppercase;margin:8px 0 18px;} .transcript-box{border:1px solid var(--line);border-left:4px solid var(--yellow); padding:14px;background:var(--grey-10);min-height:80px;} .transcript{font-size:17px;line-height:1.5;width:100%;border:none;background:transparent; font-family:var(--font-body);color:var(--ink);resize:vertical;padding:0;display:block; min-height:54px;-webkit-user-select:text;user-select:text;} .transcript:focus{outline:none;} .transcript::placeholder{color:var(--muted);font-style:italic;} .mic-area{display:flex;flex-direction:row;align-items:center;gap:14px;margin:22px 0;} .evidence{flex:none;display:flex;flex-direction:column;align-items:center;} .camera-btn{flex-direction:column;gap:6px;background:#fff;color:var(--black); border:2px solid var(--black);width:96px;height:84px;font-size:11px;font-weight:700; letter-spacing:.04em;line-height:1;} .camera-btn span{font-family:var(--font-head);text-transform:uppercase;} .camera-btn.has{border-color:var(--yellow-dark);} .viz-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:10px;min-width:0;} .viz{display:flex;align-items:center;justify-content:center;gap:5px;height:64px;width:100%;} .viz .bar{width:7px;height:8px;background:var(--grey);border-radius:0; transition:height .08s linear, background .2s ease;} .viz.live .bar{background:var(--yellow);} .viz.idle .bar{background:var(--grey-20);} .viz-label{font-family:var(--font-head);font-size:13px;color:var(--muted);font-weight:500; text-transform:uppercase;letter-spacing:.06em;} .viz-label.live{color:var(--yellow-dark);} .thumbs{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 4px;} .thumb{position:relative;width:64px;height:64px;border:1px solid var(--line);overflow:hidden;} .thumb img{width:100%;height:100%;object-fit:cover;display:block;} .thumb .rm{position:absolute;top:0;right:0;background:var(--danger);color:#fff; border:none;width:20px;height:20px;font-size:13px;line-height:20px;cursor:pointer;padding:0; font-family:var(--font-head);} .start-gate{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--yellow); padding:18px;margin-bottom:14px;text-align:center;} .gate-text{font-size:15px;color:var(--muted);margin:0 0 14px;line-height:1.55;} </style> </head> <body> <header> <div class="header-inner"> <svg class="logomark" viewBox="0 0 100 100" aria-hidden="true"> <circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="5"/> <g fill="#EAAB00" transform="skewX(-18)"> <rect x="34" y="22" width="9" height="56"/> <rect x="50" y="22" width="9" height="56"/> <rect x="66" y="22" width="9" height="56"/> </g> </svg> <div class="header-text"> <h1>Daily Plant Prestart Checklist</h1> <div class="sub">Winslow · Scan · Speak · Log</div> </div> </div> </header> <div class="wrap"> <!-- CHECKLIST --> <section id="tab-check"> <div id="speechWarn" class="banner hide"> Voice input isn't supported in this browser. Open this checklist in Safari on iPhone to use transcription. </div> <p class="pay-warning">Warning: failure to submit a completed checklist may affect your pay.</p> <div class="machine-badge align-grid" style="margin-bottom:0"> <div class="machine-row" style="align-items:center"> <div class="lbl">Machine number</div> <div id="machineDisplay" class="val loc">—</div> </div> </div> <div class="machine-badge align-grid" style="padding-top:0;margin-top:-4px;flex-direction:column;align-items:stretch"> <div class="machine-row" style="margin-bottom:12px;align-items:center"> <div class="lbl">Location</div> <div id="locationDisplay" class="val loc">Enable to detect</div> </div> <div class="machine-row" style="align-items:center"> <div class="lbl">Operator name</div> <input id="operatorInput" class="op-input" placeholder="Type your name" inputmode="text" autocomplete="name"> </div> </div> <div id="startGate" class="start-gate"> <button id="startBtn" class="btn btn-primary" type="button">Press here to enable location, microphone & start</button> </div> <div class="card" id="stepCard"> <div class="progress"><div id="progressBar"></div></div> <div class="num" id="stepNum">Item 1 of 8</div> <div class="prompt" id="stepPrompt">—</div> <div class="transcript-box"> <textarea id="transcript" class="transcript" rows="3" placeholder="Type or speak your answer and include any faults."></textarea> </div> <div class="mic-area"> <div class="evidence"> <button id="cameraBtn" class="btn camera-btn" type="button" aria-label="Add photo evidence"> <svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"> <path fill="currentColor" d="M9 3l-1.8 2H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-3.2L15 3H9zm3 5.5A4.5 4.5 0 1 1 7.5 13 4.5 4.5 0 0 1 12 8.5zm0 2A2.5 2.5 0 1 0 14.5 13 2.5 2.5 0 0 0 12 10.5z"/> </svg> <span>Add photo</span> </button> <input id="cameraInput" type="file" accept="image/*" capture="environment" class="hide"> </div> <div class="viz-wrap"> <div id="viz" class="viz idle" role="img" aria-label="Microphone audio level"></div> <div id="micLabel" class="viz-label">Microphone off</div> </div> </div> <div id="thumbs" class="thumbs"></div> <div class="btn-row"> <button id="backBtn" class="btn btn-ghost" style="flex:1">← Back</button> <button id="nextBtn" class="btn btn-primary" style="flex:2">Next →</button> </div> <p id="submitMsg" class="hint" style="text-align:center"></p> </div> <p class="hint" style="text-align:center" id="countMsg"></p> </section> </div> <script> /* ====== CONFIG: the checklist prompts (these are your spreadsheet headers) ====== */ const PROMPTS = [ "Brakes, steering, gauges, lights, warning devices (check)", "Visibility — windscreen, wipers, washer, demister, mirrors, window (visual)", "Cabin — access / egress, seating, seatbelts, loose objects, control levers (check)", "Wheels / tracks — tyres, nuts, damage, wear, pressure (visual)", "Guards — in place, secure, warning signs (visual)", "Fluid levels / hydraulics — engine, hydraulics, rams, hose leaks, excessive wear (check)", "Excessive wear — hooks, chains, pins, pivots, track, ground engage. tools (visual)", "Misc. — electrical, flashing light, beepers, communications (check)" ]; const STORE_KEY = "prestart_records_v1"; /* ====== STATE ====== */ let records = load(); let currentMachine = getMachineFromURL(); function getMachineFromURL(){ const p = new URLSearchParams(location.search); return p.get("machine") || p.get("m") || ""; } function load(){ try{ return JSON.parse(localStorage.getItem(STORE_KEY)) || []; }catch(e){ return []; } } function save(){ localStorage.setItem(STORE_KEY, JSON.stringify(records)); } /* ====== MACHINE BADGE (scan-fed, display-only) ====== */ const machineDisplay = document.getElementById("machineDisplay"); function renderMachine(){ machineDisplay.textContent = currentMachine || "QR code generated machine number"; } renderMachine(); /* ====== LOCATION (auto, address if possible, else coordinates) ====== */ let currentLocation = ""; const locationDisplay = document.getElementById("locationDisplay"); function detectLocation(){ if(!("geolocation" in navigator)){ currentLocation = "Location unavailable"; locationDisplay.textContent = currentLocation; return; } navigator.geolocation.getCurrentPosition(async (pos)=>{ const lat = pos.coords.latitude, lon = pos.coords.longitude; const coords = lat.toFixed(5) + ", " + lon.toFixed(5); /* Show coordinates immediately, then upgrade to an address if the lookup succeeds. */ currentLocation = coords; locationDisplay.textContent = coords; try{ const url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=" + lat + "&lon=" + lon + "&zoom=18"; const res = await fetch(url, { headers: { "Accept":"application/json" } }); if(res.ok){ const data = await res.json(); if(data && data.display_name){ const a = data.address || {}; const parts = [ a.road || a.suburb || a.neighbourhood || a.hamlet, a.town || a.city || a.village || a.municipality, a.state ].filter(Boolean); const label = parts.length ? parts.join(", ") : data.display_name; currentLocation = label; locationDisplay.textContent = label; } } }catch(e){ /* keep coordinates on any failure */ } }, (err)=>{ currentLocation = (err && err.code===1) ? "Location permission denied" : "Location unavailable"; locationDisplay.textContent = currentLocation; }, { enableHighAccuracy:true, timeout:10000, maximumAge:60000 }); } /* ====== SPEECH ====== */ const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if(!SR){ document.getElementById("speechWarn").classList.remove("hide"); } /* ====== STEPPER STATE ====== */ let stepIdx = 0; const answers = new Array(PROMPTS.length).fill(""); const photos = Array.from({length: PROMPTS.length}, ()=> []); /* per-prompt array of dataURLs */ const progressBar = document.getElementById("progressBar"); const stepNum = document.getElementById("stepNum"); const stepPrompt = document.getElementById("stepPrompt"); const transcriptEl = document.getElementById("transcript"); const viz = document.getElementById("viz"); const micLabel = document.getElementById("micLabel"); const backBtn = document.getElementById("backBtn"); const nextBtn = document.getElementById("nextBtn"); const cameraBtn = document.getElementById("cameraBtn"); const cameraInput = document.getElementById("cameraInput"); const thumbsEl = document.getElementById("thumbs"); /* Typing into the field updates the current prompt's answer directly. */ transcriptEl.addEventListener("input", ()=>{ answers[stepIdx] = transcriptEl.value; }); function renderStep(){ const total = PROMPTS.length; stepNum.textContent = `Item ${stepIdx+1} of ${total}`; stepPrompt.textContent = PROMPTS[stepIdx]; progressBar.style.width = ((stepIdx)/total*100) + "%"; showAnswer(); renderThumbs(); backBtn.disabled = (stepIdx===0); backBtn.style.opacity = stepIdx===0 ? .4 : 1; nextBtn.textContent = (stepIdx===total-1) ? "Submit checklist" : "Next →"; } function showAnswer(interim){ const text = (answers[stepIdx] + (interim ? " " + interim : "")).trim(); transcriptEl.value = text; } /* ====== PHOTO EVIDENCE (per prompt) ====== */ function renderThumbs(){ const list = photos[stepIdx]; thumbsEl.innerHTML = ""; list.forEach((src, i)=>{ const d = document.createElement("div"); d.className = "thumb"; const img = document.createElement("img"); img.src = src; img.alt = "Evidence photo"; const rm = document.createElement("button"); rm.className="rm"; rm.type="button"; rm.textContent = "×"; rm.setAttribute("aria-label","Remove photo"); rm.addEventListener("click", ()=>{ photos[stepIdx].splice(i,1); renderThumbs(); }); d.appendChild(img); d.appendChild(rm); thumbsEl.appendChild(d); }); cameraBtn.classList.toggle("has", list.length>0); const span = cameraBtn.querySelector("span"); if(span) span.textContent = list.length ? ("Add photo ("+list.length+")") : "Add photo"; } cameraBtn.addEventListener("click", ()=> cameraInput.click()); cameraInput.addEventListener("change", (e)=>{ const file = e.target.files && e.target.files[0]; if(!file) return; /* Downscale for storage/export, then keep as dataURL on the current prompt. */ const reader = new FileReader(); reader.onload = ()=>{ const img = new Image(); img.onload = ()=>{ const max = 1024; let w = img.width, h = img.height; if(w>h && w>max){ h = h*max/w; w = max; } else if(h>=w && h>max){ w = w*max/h; h = max; } const c = document.createElement("canvas"); c.width=w; c.height=h; c.getContext("2d").drawImage(img,0,0,w,h); photos[stepIdx].push(c.toDataURL("image/jpeg", 0.8)); renderThumbs(); }; img.src = reader.result; }; reader.readAsDataURL(file); cameraInput.value = ""; /* allow re-selecting the same file */ }); /* ====== CONTINUOUS MIC (one permission prompt, runs for whole session) ====== */ let rec=null, listening=false, micEnabled=false; /* Build visualizer bars */ const BAR_COUNT = 13; const bars = []; for(let i=0;i<BAR_COUNT;i++){ const b = document.createElement("div"); b.className = "bar"; viz.appendChild(b); bars.push(b); } /* Idle ambient animation when not yet listening */ let idleT = 0, idleRAF = null; function idleAnimate(){ idleT += 0.08; bars.forEach((b,i)=>{ const h = 8 + Math.abs(Math.sin(idleT + i*0.5)) * 6; b.style.height = h.toFixed(0) + "px"; }); idleRAF = requestAnimationFrame(idleAnimate); } idleAnimate(); /* Web Audio analyser → drives bar heights from real mic volume */ let audioCtx=null, analyser=null, freqData=null, levelRAF=null, micStream=null; async function startAudioMeter(){ try{ micStream = await navigator.mediaDevices.getUserMedia({ audio:true }); audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const src = audioCtx.createMediaStreamSource(micStream); analyser = audioCtx.createAnalyser(); analyser.fftSize = 64; analyser.smoothingTimeConstant = 0.75; freqData = new Uint8Array(analyser.frequencyBinCount); src.connect(analyser); meterLoop(); }catch(e){ /* meter is decorative; transcription still works without it */ } } function meterLoop(){ analyser.getByteFrequencyData(freqData); const step = Math.floor(freqData.length / BAR_COUNT) || 1; for(let i=0;i<BAR_COUNT;i++){ const v = freqData[i*step] / 255; const h = 8 + v * 52; bars[i].style.height = h.toFixed(0) + "px"; } levelRAF = requestAnimationFrame(meterLoop); } function setMicUI(on){ viz.classList.toggle("live", on); viz.classList.toggle("idle", !on); micLabel.classList.toggle("live", on); micLabel.textContent = on ? "Listening" : "Microphone off"; } function startMic(){ if(!SR){ return; } micEnabled = true; if(idleRAF){ cancelAnimationFrame(idleRAF); idleRAF=null; } startAudioMeter(); beginRecognition(); } function beginRecognition(){ if(!micEnabled || listening) return; rec = new SR(); rec.lang = "en-AU"; rec.continuous = true; rec.interimResults = true; rec.onstart = ()=>{ listening=true; setMicUI(true); }; rec.onresult = (e)=>{ let interim=""; for(let i=e.resultIndex;i<e.results.length;i++){ const t = e.results[i][0].transcript; if(e.results[i].isFinal){ answers[stepIdx] = (answers[stepIdx] + " " + t).replace(/\s+/g," ").trim(); } else { interim += t; } } showAnswer(interim); }; rec.onerror = (e)=>{ listening=false; if(e.error === "not-allowed" || e.error === "service-not-allowed"){ micEnabled=false; setMicUI(false); document.getElementById("speechWarn").textContent = "Microphone access was denied. Enable it in your browser settings and reload to use voice transcription."; document.getElementById("speechWarn").classList.remove("hide"); } }; /* Keep the session alive for the whole app: auto-restart whenever it stops. */ rec.onend = ()=>{ listening=false; setMicUI(false); if(micEnabled){ setTimeout(beginRecognition, 250); } }; try{ rec.start(); }catch(e){} } /* Single tap on the start button grants mic permission and begins. After this, no more tapping. */ const startBtn = document.getElementById("startBtn"); const startGate = document.getElementById("startGate"); startBtn.addEventListener("click", ()=>{ if(!SR){ document.getElementById("speechWarn").classList.remove("hide"); return; } startGate.classList.add("hide"); locationDisplay.textContent = "Detecting…"; detectLocation(); startMic(); }); backBtn.addEventListener("click", ()=>{ if(stepIdx>0){ stepIdx--; renderStep(); } }); nextBtn.addEventListener("click", ()=>{ if(stepIdx < PROMPTS.length-1){ stepIdx++; renderStep(); } else submitChecklist(); }); /* ====== SUBMIT ====== */ function submitChecklist(){ const machine = currentMachine; if(!machine){ alert("No machine number detected. Please open this checklist by scanning the machine's QR code."); return; } const operator = document.getElementById("operatorInput").value.trim(); if(!operator){ alert("Please type the operator name before submitting."); return; } const ans = {}; const pics = {}; PROMPTS.forEach((p,i)=>{ ans[p] = answers[i].trim(); pics[p] = photos[i].slice(); }); const now = new Date(); const record = { timestamp: now.toISOString(), timestampLocal: now.toLocaleString(), location: currentLocation, machine, operator, answers: ans, photos: pics }; records.unshift(record); save(); answers.fill(""); photos.forEach(a=> a.length = 0); stepIdx = 0; renderStep(); progressBar.style.width = "100%"; const msg = document.getElementById("submitMsg"); msg.textContent = "Saved & timestamped " + now.toLocaleString(); setTimeout(()=>{ msg.textContent=""; progressBar.style.width="0%"; }, 4000); updateCount(); downloadRecords([record]); } function updateCount(){ const c = records.length; document.getElementById("countMsg").textContent = c ? (c + (c===1?" checklist":" checklists") + " saved on this device") : "No checklists saved yet"; } if(!SR){ document.getElementById("startGate").style.display = "none"; } renderStep(); updateCount(); /* ====== DOWNLOAD (spreadsheet + photos as a ZIP) ====== */ function safe(s){ return String(s||"").replace(/[^\w.-]+/g,"_").slice(0,40); } async function downloadRecords(list){ if(!list || list.length===0){ return; } /* Build the spreadsheet rows */ const rows = list.map(r=>{ const row = { Timestamp: r.timestampLocal, "Location": r.location||"", "Machine Number": r.machine, "Operator": r.operator||"" }; PROMPTS.forEach(p=>{ row[p] = (r.answers && r.answers[p]) || ""; const n = (r.photos && r.photos[p] && r.photos[p].length) || 0; row[p + " — photos"] = n; }); return row; }); const ws = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Prestart Checklist"); const xlsxArray = XLSX.write(wb, { bookType:"xlsx", type:"array" }); const zip = new JSZip(); zip.file("prestart_checklist.xlsx", xlsxArray); const photoRoot = zip.folder("photos"); list.forEach((r, ri)=>{ const stamp = (r.timestamp||"").replace(/[:.]/g,"-").slice(0,19) || ("record_"+ri); const folderName = safe(r.machine) + "__" + safe(stamp); let any = false; const recFolder = photoRoot.folder(folderName); PROMPTS.forEach((p, pi)=>{ const arr = (r.photos && r.photos[p]) || []; arr.forEach((dataUrl, k)=>{ any = true; const b64 = dataUrl.split(",")[1] || ""; recFolder.file("item" + (pi+1) + "_" + safe(p) + "_" + (k+1) + ".jpg", b64, { base64:true }); }); }); if(!any) photoRoot.remove(folderName); }); /* Name the file after the checklist when it's a single record. */ let fname = "prestart_checklist_" + new Date().toISOString().slice(0,10) + ".zip"; if(list.length === 1){ const r = list[0]; const stamp = (r.timestamp||"").replace(/[:.]/g,"-").slice(0,19); fname = "prestart_" + safe(r.machine) + "_" + safe(stamp) + ".zip"; } const blob = await zip.generateAsync({ type:"blob" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fname; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=> URL.revokeObjectURL(url), 4000); } </script> </body> </html>
Save
cmd:
run