PageSourceSearch

https://bus-fasano.web.app/js/le-mie-prenotazioni.js

js bus-fasano.web.app collected 2026-10-03 12:19:03 UTC 9,884 bytes, 227 lines download raw bytes

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.