1// Pagina "Le mie prenotazioni" â riepilogo (dispositivo), annullamento, PDF, recupero. 2import { 3 doc, getDoc, runTransaction, serverTimestamp, 4} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js"; 5import { db } from "./firebase-init.js"; 6import { t, getLang } from "./i18n.js"; 7 8const $ = (id) => document.getElementById(id); 9const LS_KEY = "prenotazioni_dispositivo"; 10 11let scanner = null; 12 13// ---------- LocalStorage ---------- 14function prenotazioniLocali() { 15 try { return JSON.parse(localStorage.getItem(LS_KEY)) || []; } catch { return []; } 16} 17function salvaTutte(arr) { localStorage.setItem(LS_KEY, JSON.stringify(arr)); } 18 19// ---------- Utilità ---------- 20function dataLeggibile(iso) { if (!iso) return "â"; const [a, m, g] = iso.split("-"); return `${g}/${m}/${a}`; } 21function oggiISO() { 22 const d = new Date(); 23 return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; 24} 25 26// ---------- Rendering lista ---------- 27function render() { 28 const cont = $("lista-prenotazioni"); 29 const arr = prenotazioniLocali(); 30 cont.innerHTML = ""; 31 $("lista-vuota").hidden = arr.length !== 0; 32 33 // più recenti in alto 34 arr.slice().reverse().forEach((p) => { 35 const annullata = p.stato === "annullata"; 36 const futura = p.data >= oggiISO(); 37 const card = document.createElement("div"); 38 card.className = "card prenot-card" + (annullata ? " prenot-annullata" : ""); 39 card.innerHTML = ` 40 <div class="prenot-top"> 41 <span class="prenot-codice">${p.codice || p.id}</span> 42 <span class="prenot-stato ${annullata ? "st-annullata" : "st-attiva"}">${annullata ? t("mybk_status_annullata") : t("mybk_status_prenotata")}</span> 43 </div> 44 <dl class="biglietto-dati"> 45 <div><dt>${t("conf_from")}</dt><dd>${p.fermataPartenza || "â"} â ${p.orario || ""}</dd></div> 46 <div><dt>${t("conf_to")}</dt><dd>${p.fermataArrivo || "â"} â ${p.orarioArrivo || ""}</dd></div> 47 <div><dt>${t("conf_date")}</dt><dd>${dataLeggibile(p.data)}</dd></div> 48 <div><dt>${t("conf_pax")}</dt><dd>${p.carrozzina ? ("â¿ " + t("carr_badge")) : (p.passeggeri ?? "â")}</dd></div> 49 </dl> 50 <div class="prenot-qr" data-qr="${p.codice || p.id}"></div> 51 <div class="prenot-azioni"> 52 <button type="button" class="btn btn-secondario btn-pdf" data-codice="${p.id}">${t("mybk_pdf")}</button> 53 ${(!annullata && futura) ? `<button type="button" class="btn btn-testo btn-annulla" data-codice="${p.id}">${t("mybk_cancel")}</button>` : ""} 54 </div>`; 55 cont.appendChild(card); 56 }); 57 58 // QR di ogni card 59 cont.querySelectorAll(".prenot-qr").forEach((el) => { 60 const code = el.getAttribute("data-qr"); 61 el.innerHTML = ""; 62 if (window.QRCode && code) new window.QRCode(el, { text: code, width: 128, height: 128, colorDark: "#000000", colorLight: "#ffffff", correctLevel: window.QRCode.CorrectLevel.M }); 63 }); 64 65 cont.querySelectorAll(".btn-pdf").forEach((b) => b.addEventListener("click", () => scaricaPDF(b.dataset.codice))); 66 cont.querySelectorAll(".btn-annulla").forEach((b) => b.addEventListener("click", () => annulla(b.dataset.codice))); 67} 68 69// ---------- Annullamento ---------- 70async function annulla(codice) { 71 if (!confirm(t("mybk_cancel_confirm"))) return; 72 try { 73 await runTransaction(db, async (tx) => { 74 const pRef = doc(db, "prenotazioni", codice); 75 const pSnap = await tx.get(pRef); 76 if (!pSnap.exists()) throw new Error("NOEXIST"); 77 const p = pSnap.data(); 78 if (p.stato === "annullata") throw new Error("ALREADY"); 79 if (p.salito) throw new Error("SALITO"); 80 const cRef = doc(db, "contatori", p.linea + "__" + p.data); 81 if (p.carrozzina) { 82 // Libera il posto carrozzina e, se era l'unica della serata, ripristina i 2 posti. 83 const sRef = doc(db, "serate", p.data); 84 const sSnap = await tx.get(sRef); 85 const carr = sSnap.exists() ? (sSnap.data().carrozzine || 0) : 0; 86 tx.update(pRef, { stato: "annullata", salito: false, annullataAt: serverTimestamp() }); 87 tx.set(cRef, { 88 carrozzina: 0, 89 carrSalite: { [p.fermataPartenza]: 0 }, 90 carrDiscese: { [p.fermataArrivo]: 0 }, 91 }, { merge: true }); 92 tx.set(sRef, { carrozzine: Math.max(0, carr - 1), data: p.data }, { merge: true }); 93 } else { 94 const cSnap = await tx.get(cRef); 95 const c = cSnap.exists() ? cSnap.data() : { passeggeri: 0, salite: {}, discese: {} }; 96 const pax = p.passeggeri || 0; 97 tx.update(pRef, { stato: "annullata", salito: false, annullataAt: serverTimestamp() }); 98 tx.set(cRef, { 99 passeggeri: Math.max(0, (c.passeggeri || 0) - pax), 100 salite: { [p.fermataPartenza]: Math.max(0, ((c.salite || {})[p.fermataPartenza] || 0) - pax) }, 101 discese: { [p.fermataArrivo]: Math.max(0, ((c.discese || {})[p.fermataArrivo] || 0) - pax) }, 102 }, { merge: true }); 103 } 104 }); 105 // aggiorna lo stato in locale 106 const arr = prenotazioniLocali().map((x) => (x.id === codice ? { ...x, stato: "annullata" } : x)); 107 salvaTutte(arr); 108 render(); 109 esitoRecupero(t("mybk_cancelled_ok"), true); 110 } catch (err) { 111 console.error(err); 112 esitoRecupero(t("mybk_cancel_err"), false); 113 } 114} 115 116// ---------- Recupero ---------- 117function normalizzaCodice(v) { 118 let c = (v || "").trim().toUpperCase().replace(/\s/g, ""); 119 if (c && !c.startsWith("FAS-")) c = "FAS-" + c.replace(/^FAS-?/, ""); 120 return c; 121} 122async function recupera(codiceRaw) { 123 const codice = normalizzaCodice(codiceRaw); 124 if (!codice) return; 125 try { 126 const snap = await getDoc(doc(db, "prenotazioni", codice)); 127 if (!snap.exists()) return esitoRecupero(t("mybk_not_found"), false); 128 const arr = prenotazioniLocali(); 129 if (arr.some((x) => x.id === codice)) return esitoRecupero(t("mybk_already"), false); 130 const p = snap.data(); 131 arr.push({ 132 id: codice, codice: p.codice || codice, 133 linea: p.linea, lineaNome: p.lineaNome, direzione: p.direzione, 134 fermataPartenza: p.fermataPartenza, fermataArrivo: p.fermataArrivo, 135 orario: p.orario, orarioArrivo: p.orarioArrivo, 136 data: p.data, passeggeri: p.passeggeri, stato: p.stato || "prenotata", 137 }); 138 salvaTutte(arr); 139 render(); 140 esitoRecupero(t("mybk_recovered"), true); 141 $("recupero-codice").value = ""; 142 } catch (err) { 143 console.error(err); 144 esitoRecupero(t("mybk_not_found"), false); 145 } 146} 147function esitoRecupero(msg, ok) { 148 const box = $("recupero-esito"); 149 box.className = "alert " + (ok ? "alert-ok" : "alert-errore"); 150 box.textContent = msg; 151 box.hidden = false;
152} 153 154// ---------- Scanner QR (recupero) ---------- 155async function toggleScanner() { 156 if (scanner) return stopScanner(); 157 if (!window.Html5Qrcode) return esitoRecupero("Scanner non disponibile.", false); 158 scanner = new window.Html5Qrcode("reader"); 159 try { 160 await scanner.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 220, height: 220 } }, 161 async (testo) => { await stopScanner(); recupera(testo); }, () => {}); 162 $("btn-scan").textContent = t("mybk_scan_stop"); 163 } catch (e) { console.error(e); scanner = null; esitoRecupero("Impossibile avviare la fotocamera.", false); } 164} 165async function stopScanner() { 166 if (scanner) { 167 try { if (scanner.isScanning) await scanner.stop(); } catch (e) { /* noop */ } 168 try { scanner.clear(); } catch (e) { /* noop */ } 169 scanner = null; 170 } 171 $("btn-scan").textContent = t("mybk_scan_btn"); 172} 173 174// ---------- PDF ---------- 175function scaricaPDF(codice) { 176 const p = prenotazioniLocali().find((x) => x.id === codice); 177 if (!p || !window.jspdf) return; 178 const { jsPDF } = window.jspdf; 179 const docp = new jsPDF({ unit: "mm", format: "a5" }); 180 const W = docp.internal.pageSize.getWidth(); 181 182 docp.setFillColor(31, 78, 121); 183 docp.rect(0, 0, W, 26, "F"); 184 docp.setTextColor(255, 255, 255); 185 docp.setFontSize(18); docp.text("Fasano Coast Shuttle", W / 2, 12, { align: "center" }); 186 docp.setFontSize(10); docp.text("Comune di Fasano", W / 2, 20, { align: "center" }); 187 188 docp.setTextColor(28, 39, 51); 189 let y = 40; 190 docp.setFontSize(9); docp.setTextColor(120, 120, 120); 191 docp.text(t("conf_code").toUpperCase(), 15, y); 192 docp.setFontSize(16); docp.setTextColor(22, 58, 90); 193 docp.text(p.codice || p.id, 15, y + 8); 194 195 y += 20; 196 const riga = (label, val) => { 197 docp.setFontSize(9); docp.setTextColor(120, 120, 120); docp.text(label.toUpperCase(), 15, y); 198 docp.setFontSize(12); docp.setTextColor(28, 39, 51); docp.text(String(val), 15, y + 6); 199 y += 14; 200 }; 201 riga(t("conf_from"), `${p.fermataPartenza || "â"} â ${p.orario || ""}`); 202 riga(t("conf_to"), `${p.fermataArrivo || "â"} â ${p.orarioArrivo || ""}`); 203 riga(t("conf_date"), dataLeggibile(p.data)); 204 riga(t("conf_pax"), p.passeggeri ?? "â"); 205 206 // QR 207 try { 208 const tmp = document.createElement("div"); 209 new window.QRCode(tmp, { text: p.codice || p.id, width: 300, height: 300, colorDark: "#000000", colorLight: "#ffffff", correctLevel: window.QRCode.CorrectLevel.M }); 210 const canvas = tmp.querySelector("canvas"); 211 if (canvas) { 212 const dataUrl = canvas.toDataURL("image/png"); 213 docp.addImage(dataUrl, "PNG", W - 55, 40, 40, 40); 214 docp.setFontSize(8); docp.setTextColor(120, 120, 120); 215 docp.text(t("qr_cap"), W - 35, 84, { align: "center", maxWidth: 40 }); 216 } 217 } catch (e) { console.warn("QR PDF:", e); } 218 219 docp.save(`prenotazione-${p.codice || p.id}.pdf`); 220} 221 222// ---------- Avvio ---------- 223$("btn-recupera").addEventListener("click", () => recupera($("recupero-codice").value)); 224$("recupero-codice").addEventListener("keydown", (e) => { if (e.key === "Enter") recupera($("recupero-codice").value); }); 225$("btn-scan").addEventListener("click", toggleScanner); 226window.addEventListener("langchange", render); 227render();
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.