1/* ===================================================================== 2 Energieloket â quickscan-wizard (één doorlopende vragenlijst) 3 Stappen: voornaam, achternaam, e-mail, telefoon, bedrijf, projectnaam, 4 netaansluiting (bekend? + grootte), opdrachtgever-eisen, 5 locatie, looptijd -> afronden (bijlagen + versturen). 6 Bij een aansluiting > 3Ã80A wordt de aanvrager direct gealarmeerd. 7 Vereist: app.js (window.sb, window.Loket), Leaflet. 8 ===================================================================== */ 9(function () { 10 const root = document.getElementById("qwiz"); 11 if (!root) return; 12 const $ = (id) => document.getElementById(id); 13 const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 14 15 const wz = { 16 key: "first", t0: Date.now(), 17 s: { 18 first: "", last: "", email: "", phone: "", company: "", project: "", 19 netKnown: "", netSize: "", netOver: false, 20 laadKnown: "", laadAantal: 1, eisen: [], 21 addr: "", lat: null, lng: null, start: "", end: "", 22 }, 23 map: null, marker: null, mapReady: false, 24 }; 25 const wzFiles = []; 26 27 // ---- stapvolgorde (één pad, alle vragen) ---- 28 const SEQ = ["first", "last", "email", "phone", "company", "project", "net", "laad", "eisen", "loc", "term"]; 29 function idx() { return SEQ.indexOf(wz.key); } 30 function total() { return SEQ.length; } 31 function curScreen() { return root.querySelector('.qscreen[data-screen="' + wz.key + '"]'); } 32 33 function markChoice(list, active) { list.forEach((el) => el && el.classList.toggle("on", el === active)); } 34 35 function show(focus) { 36 const k = wz.key; 37 root.querySelectorAll(".qscreen").forEach((el) => (el.hidden = el.dataset.screen !== k)); 38 const i = idx(), n = total(); 39 $("qwizBar").style.width = Math.round(((i + 1) / n) * 100) + "%"; 40 $("qwizLabel").textContent = "Stap " + (i + 1) + " van " + n; 41 $("wzBack").style.visibility = i === 0 ? "hidden" : "visible"; 42 $("wzNext").textContent = i === n - 1 ? "Naar afronden" : "Volgende"; 43 $("wzMsg").style.display = "none"; 44 if (k === "loc") ensureMap(); 45 if (focus) { 46 const sc = curScreen(); 47 const inp = sc && sc.querySelector("input:not([type=checkbox])"); 48 if (inp && inp.type !== "date") setTimeout(() => inp.focus(), 30); 49 } 50 } 51 function err(msg) { const m = $("wzMsg"); m.textContent = msg; m.className = "authmsg show err"; m.style.display = "block"; } 52 53 // ---- detectie aansluiting > 3Ã80A ---- 54 function exceeds3x80(raw) { 55 if (!raw) return false; 56 const s = String(raw).toLowerCase().replace(/[Ã*]/g, "x").replace(/,/g, "."); 57 let m = s.match(/([\d.]+)\s*kva/); 58 if (m) return parseFloat(m[1]) > 55.2; // 3Ã80A â 55,2 kVA 59 m = s.match(/(\d+)\s*x\s*(\d+)\s*a?/); 60 if (m) return (+m[1]) * (+m[2]) > 240; // 3Ã80A = 240 61 m = s.match(/(\d+)\s*a/); 62 if (m) return parseFloat(m[1]) > 80; // enkel ampère 63 return false; 64 } 65 function updateNetAlarm() { 66 const inp = $("wzNetSize"), box = $("netAlarm"); 67 if (!inp || !box) return; 68 const over = wz.s.netKnown === "ja" && exceeds3x80(inp.value); 69 wz.s.netOver = over; 70 box.classList.toggle("hideblock", !over); 71 } 72 73 function read() { 74 const k = wz.key, s = wz.s; 75 if (k === "first") s.first = $("wzFirst").value.trim(); 76 if (k === "last") s.last = $("wzLast").value.trim(); 77 if (k === "email") s.email = $("wzEmail").value.trim(); 78 if (k === "phone") s.phone = $("wzPhone").value.trim(); 79 if (k === "company") s.company = $("wzCompany").value.trim(); 80 if (k === "project") s.project = $("wzProject").value.trim(); 81 if (k === "net" && s.netKnown === "ja") { s.netSize = $("wzNetSize").value.trim(); updateNetAlarm(); } 82 if (k === "laad" && s.laadKnown === "ja") { 83 const n = parseInt($("wzLaadAantal").value) || 1; 84 s.laadAantal = Math.max(1, n); $("wzLaadAantal").value = s.laadAantal; 85 } 86 if (k === "eisen") s.eisen = [...document.querySelectorAll('#wzEisen input:checked')].map((c) => c.value); 87 if (k === "loc") s.addr = $("wzAddr").value.trim(); 88 if (k === "term") { s.start = $("wzStart").value; s.end = $("wzEnd").value; } 89 } 90 91 async function valid() {
92 const k = wz.key, s = wz.s; 93 if (k === "first" && !s.first) return err("Vul je voornaam in."), false; 94 if (k === "last" && !s.last) return err("Vul je achternaam in."), false; 95 if (k === "email" && !EMAIL_RE.test(s.email)) return err("Vul een geldig e-mailadres in."), false; 96 if (k === "phone" && (s.phone || "").replace(/[^\d]/g, "").length < 8) return err("Vul een geldig telefoonnummer in."), false; 97 if (k === "company" && !s.company) return err("Vul je bedrijfsnaam in."), false; 98 if (k === "project" && !s.project) return err("Vul de projectnaam in."), false; 99 if (k === "net") { 100 if (!s.netKnown) return err("Geef aan of het type netaansluiting al bekend is."), false; 101 if (s.netKnown === "ja" && !s.netSize) return err("Vul de grootte van de aansluiting in."), false; 102 } 103 if (k === "laad" && !s.laadKnown) return err("Geef aan of er laadpalen nodig zijn."), false; 104 if (k === "loc") { 105 if (s.lat == null && s.addr) { await geocode(); } 106 if (s.lat == null) return err("Zoek het adres of zet de speld op de kaart."), false; 107 } 108 if (k === "term") { 109 if (!s.start) return err("Kies een startdatum."), false; 110 if (s.end && s.end < s.start) return err("De einddatum ligt vóór de startdatum."), false; 111 } 112 return true; 113 } 114 115 async function next() { 116 read(); 117 if (!(await valid())) return; 118 const i = idx(); 119 if (i === SEQ.length - 1) return finish(); 120 wz.key = SEQ[i + 1]; show(true); 121 } 122 function back() { 123 read(); 124 const i = idx(); 125 if (i > 0) { wz.key = SEQ[i - 1]; show(true); } 126 } 127 128 function finish() { 129 root.style.display = "none"; 130 const rest = $("qrest"); rest.style.display = "block"; 131 const q2 = $("q2"), cw = $("calcwrap"), fs = $("finalSubmit"); 132 if (q2) q2.classList.add("hideblock"); // geen laadinfra-vraag 133 if (cw) cw.classList.remove("show"); // geen ERE-calculator 134 if (fs) fs.classList.remove("hideblock"); // direct het verzendblok 135 wireFinal(); 136 rest.scrollIntoView({ behavior: "smooth", block: "start" }); 137 } 138 139 // ---- kaart (Leaflet + PDOK) ---- 140 function pinIcon() { 141 return L.divIcon({ 142 className: "qwiz-pinicon", 143 html: '<svg width="30" height="40" viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 20 12 20s12-11 12-20C24 5.4 18.6 0 12 0z" fill="#A816D9"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>', 144 iconSize: [30, 40], iconAnchor: [15, 40], 145 }); 146 } 147 function ensureMap() { 148 if (wz.mapReady) { setTimeout(() => wz.map.invalidateSize(), 60); return; } 149 if (typeof L === "undefined") return; 150 wz.map = L.map("wzMap", { scrollWheelZoom: false }).setView([52.13, 5.29], 7); 151 L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 152 maxZoom: 19, attribution: "© OpenStreetMap", 153 }).addTo(wz.map); 154 wz.map.on("click", (e) => { setPin(e.latlng.lat, e.latlng.lng); reverse(e.latlng.lat, e.latlng.lng); }); 155 wz.mapReady = true; 156 setTimeout(() => wz.map.invalidateSize(), 60); 157 } 158 function setPin(lat, lng, zoom) { 159 wz.s.lat = +lat.toFixed(6); wz.s.lng = +lng.toFixed(6); 160 if (!wz.marker) { 161 wz.marker = L.marker([lat, lng], { draggable: true, icon: pinIcon() }).addTo(wz.map); 162 wz.marker.on("dragend", () => { const p = wz.marker.getLatLng(); setPin(p.lat, p.lng); reverse(p.lat, p.lng); }); 163 } else wz.marker.setLatLng([lat, lng]); 164 wz.map.setView([lat, lng], zoom || Math.max(wz.map.getZoom(), 15)); 165 $("wzCoord").textContent = "Locatie vastgelegd op de kaart."; 166 } 167 async function geocode() { 168 const q = $("wzAddr").value.trim(); if (!q) return; 169 try { 170 const u = "https://api.pdok.nl/bzk/locatieserver/search/v3_1/free?q=" + encodeURIComponent(q) + "&rows=1&fl=weergavenaam,centroide_ll"; 171 const r = await fetch(u); const j = await r.json(); 172 const d = j.response && j.response.docs && j.response.docs[0]; 173 if (!d) { err("Adres niet gevonden â zet de speld handmatig op de kaart."); return; } 174 const m = /POINT\(([-\d.]+) ([-\d.]+)\)/.exec(d.centroide_ll); 175 if (!m) return; 176 ensureMap(); 177 $("wzAddr").value = d.weergavenaam || q; wz.s.addr = $("wzAddr").value; 178 setPin(parseFloat(m[2]), parseFloat(m[1]), 16); 179 } catch (e) { err("Adres zoeken lukte niet â zet de speld handmatig op de kaart."); } 180 } 181 async function reverse(lat, lng) { 182 try { 183 const u = "https://api.pdok.nl/bzk/locatieserver/search/v3_1/reverse?lat=" + lat + "&lon=" + lng + "&rows=1&fl=weergavenaam"; 184 const r = await fetch(u); const j = await r.json(); 185 const d = j.response && j.response.docs && j.response.docs[0]; 186 if (d && d.weergavenaam) { $("wzAddr").value = d.weergavenaam; wz.s.addr = d.weergavenaam; } 187 } catch (e) { /* stil */ } 188 } 189 window.qwizSearch = geocode; 190 191 // ---- definitieve verzending ---- 192 function wireFinal() { 193 const drop = $("drop"), inp = $("files"), list = $("filelist"), bt
193n = $("btnQuick"); 194 if (drop && !drop.dataset.wired) { 195 drop.dataset.wired = "1"; 196 const render = () => { list.innerHTML = ""; wzFiles.forEach((f, i) => { const li = document.createElement("li"); li.innerHTML = '<span><svg class="icn" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11l-8 8a5 5 0 0 1-7-7l9-9a3.5 3.5 0 0 1 5 5l-9 9a2 2 0 0 1-3-3l8-8"/></svg> ' + Loket.escape(f.name) + '</span><span class="x" title="verwijder">×</span>'; li.querySelector(".x").onclick = () => { wzFiles.splice(i, 1); render(); }; list.appendChild(li); }); }; 197 drop.onclick = () => inp.click(); 198 inp.onchange = (e) => { for (const f of e.target.files) wzFiles.push(f); render(); inp.value = ""; }; 199 ["dragover", "dragenter"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add("over"); })); 200 ["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove("over"); })); 201 drop.addEventListener("drop", (e) => { for (const f of e.dataTransfer.files) wzFiles.push(f); render(); }); 202 } 203 if (btn && !btn.dataset.wired) { btn.dataset.wired = "1"; btn.onclick = submit; } 204 } 205 206 function botBlock() { 207 try { 208 $("drop").parentElement.style.display = "none"; 209 $("btnQuick").parentElement.style.display = "none"; 210 } catch (e) {} 211 $("recap").innerHTML = "<div>Je aanvraag is ontvangen.</div>"; 212 $("success").classList.add("show"); 213 } 214 215 function buildIntake() { 216 const s = wz.s; 217 const intake = { net_known: s.netKnown || null, laad_known: s.laadKnown || null, eisen: s.eisen || [] }; 218 if (s.laadKnown === "ja") intake.laad_aantal = s.laadAantal; 219 if (s.netKnown === "ja") { intake.net_size = s.netSize || null; intake.net_over_3x80 = !!s.netOver; } 220 return intake; 221 } 222 223 async function submit() { 224 const hp = document.getElementById("wzHp"); 225 if ((hp && hp.value.trim()) || Date.now() - wz.t0 < 3000) { botBlock(); return; } 226 if (!window.sb) { Loket.toast("Supabase nog niet ingesteld.", "err"); return; } 227 const btn = $("btnQuick"); btn.disabled = true; const lbl = btn.textContent; 228 btn.innerHTML = '<span class="spinner"></span> Versturenâ¦'; 229 try { 230 const folder = (crypto.randomUUID && crypto.randomUUID()) || ("a" + Date.now() + Math.random().toString(36).slice(2)); 231 const attachments = []; 232 for (const f of wzFiles) { 233 const safe = f.name.replace(/[^\w.\-]+/g, "_"); const path = folder + "/" + safe; 234 const { error } = await window.sb.storage.from("aanvragen").upload(path, f, { upsert: false }); 235 if (error) throw error; 236 attachments.push({ name: f.name, path, size: f.size }); 237 } 238 const s = wz.s; 239 const intake = buildIntake(); 240 const { data, error } = await window.sb.rpc("submit_quickscan", { 241 p_first: s.first, p_last: s.last, p_email: s.email, p_phone: s.phone, p_company: s.company, p_project: s.project, 242 p_address: s.addr, p_lat: s.lat, p_lng: s.lng, p_start: s.start || null, p_end: s.end || null, 243 p_ere: null, p_attachments: attachments, p_intake: intake, 244 }); 245 if (error) throw error; 246 const nr = (data && data.project_no) || "â"; 247 // e-mails versturen via de Edge Function (bevestiging naar aanvrager + interne kopie) 248 try { 249 if (data && data.id) await window.sb.functions.invoke("send-aanvraag", { body: { id: data.id } }); 250 } catch (mailErr) { 251 console.error("Mailfunctie niet bereikbaar:", mailErr); // aanvraag is al opgeslagen; niet blokkeren 252 } 253 $("drop").parentElement.style.display = "none"; 254 btn.parentElement.style.display = "none"; 255 const esc = Loket.escape; 256 const intakeLines = 257 (s.netKnown 258 ? '<div><b>Netaansluiting bekend:</b> ' + (s.netKnown === "ja" ? "Ja â " + esc(s.netSize || "") : "Nee â verdiepende scan gewenst") + "</div>" 259 : "") + 260 (s.netOver ? '<div style="color:var(--red)"><b>Let op:</b> aansluiting > 3Ã80A â energiescan noodzakelijk.</div>' : "") + 261 (s.laadKnown 262 ? '<div><b>Laadpalen nodig:</b> ' + (s.laadKnown === "ja" ? s.laadAantal + " laadplek" + (s.laadAantal === 1 ? "" : "ken") : "Nog niet bekend") + "</div>" 263 : "") + 264 (s.eisen && s.eisen.length ? '<div><b>Eisen / kansen:</b> ' + esc(s.eisen.join(", ")) + "</div>" : ""); 265 const rec = $("recap"); 266 rec.innerHTML = 267 '<div><b>Aanvraagnummer:</b> ' + esc(nr) + "</div>" + 268 '<div><b>Naam:</b> ' + esc(s.first + " " + s.last) + "</div>" + 269 '<div><b>Telefoon:</b> ' + esc(s.phone) + "</div>" + 270 '<div><b>Bedrijf:</b> ' + esc(s.company) + "</div>" + 271 '<div><b>Project:</b> ' + esc(s.project) + "</div>" + 272 '<div><b>Locatie:</b> ' + esc(s.addr) + "</div>" + 273 (s.start ? '<div><b>Looptijd:</b> ' + esc(s.start) + (s.end ? " t/m " + esc(s.end) : "") + "</div>" : "") + 274 intakeLines + 275 (attachments.length ? '<div><b>Bijlagen:</b> ' + attachments.length + "</div>" : "") + 276 '<p class="hint" style="margin-top:10px">Bewaar je aanvraagnummer. Je ontvangt een bevestiging per e-mail en een expert neemt binnen 2 werkdagen contact met je op.</p>'; 277 $("success").classList.add("show"); 278 $("success").scrollIntoView({ behavior: "smooth", block: "center" }); 279 } catch (e) { 280 Loket.toast("Versturen mislukt: " + (e.message || e), "err"); 281 btn.disabled = false; btn.textContent = lbl; 282 } 283 } 284 285 // ---- init ---- 286 $("wzNext").onclick = next; 287 $("wzBack").onclick = back; 288 $("wzAddrBtn").onclick = geocode; 289 290 // netaansluiting bekend? ja/nee
291 [["netJaBtn", "ja"], ["netNeeBtn", "nee"]].forEach(([id, val]) => { 292 const b = $(id); 293 if (b) b.onclick = () => { 294 wz.s.netKnown = val; markChoice([$("netJaBtn"), $("netNeeBtn")], b); 295 $("netJa").classList.toggle("hideblock", val !== "ja"); 296 $("netNee").classList.toggle("hideblock", val !== "nee"); 297 $("wzMsg").style.display = "none"; 298 updateNetAlarm(); 299 if (val === "ja") setTimeout(() => $("wzNetSize").focus(), 30); 300 }; 301 }); 302 // laadpalen nodig? ja/nee 303 [["laadJaBtn", "ja"], ["laadNeeBtn", "nee"]].forEach(([id, val]) => { 304 const b = $(id); 305 if (b) b.onclick = () => { 306 wz.s.laadKnown = val; markChoice([$("laadJaBtn"), $("laadNeeBtn")], b); 307 $("laadJa").classList.toggle("hideblock", val !== "ja"); 308 $("laadNee").classList.toggle("hideblock", val !== "nee"); 309 $("wzMsg").style.display = "none"; 310 if (val === "ja") setTimeout(() => $("wzLaadAantal").focus(), 30); 311 }; 312 }); 313 window.wzLaadBump = function (n) { 314 const el = $("wzLaadAantal"); if (!el) return; 315 const v = Math.max(1, (parseInt(el.value) || 1) + n); 316 el.value = v; wz.s.laadAantal = v; 317 }; 318 319 if ($("wzNetSize")) $("wzNetSize").addEventListener("input", updateNetAlarm); 320 if ($("wzNetAsk")) $("wzNetAsk").onclick = () => next(); 321 322 root.querySelectorAll("input").forEach((i) => { 323 if (i.type !== "date" && i.type !== "checkbox") { 324 i.addEventListener("keydown", (e) => { if (e.key === "Enter" && i.id !== "wzAddr") { e.preventDefault(); next(); } }); 325 } 326 }); 327 show(false); 328})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.