PageSourceSearch

https://www.habeo.cz/user/documents/upload/AZStudio/scripts/PSCModul_Doprava/psc-doprava-admin-only7.js

js habeo.cz collected 2026-10-08 13:51:06 UTC 14,197 bytes, 439 lines download raw bytes

1console.log("PSC script LOADED (TXT lists + cache)");
2(function () {
3  // ==================================================
4  // OMEZENÍ: běží pouze v ADMIN režimu (admin-bar)
5  // ==================================================
6  const ADMIN_ONLY = false; // <- pro zákazníky přepni na false
7  if (ADMIN_ONLY && !document.querySelector(".admin-bar")) return;
8
9  const PSC_STORAGE_KEY = "shoptet_psc_filter_v1";
10
11  function savePscToStorage(psc) {
12    try { localStorage.setItem(PSC_STORAGE_KEY, psc); } catch (e) {}
13  }
14  function loadPscFromStorage() {
15    try { return localStorage.getItem(PSC_STORAGE_KEY) || ""; } catch (e) { return ""; }
16  }
17
18  const ALWAYS_AFTER_PSC = [445, 253, 409, 490, 228, 225, 510, 424, 436, 415, 231, 418, 451, 448, 507, 412, 526];
19
20  const PSC_DEPENDENT_SHIPPINGS = {
21    // PŘÍKLADY:
22    251: { file: true }, // načte /psc/251.txt
23    77: { file: true }
24  };
25
26  const ZONE_PRIORITY = []; // prázdné = nevynucuje auto-výběr zónové dopravy
27
28  const PSC_FILES_BASE_URL = "/user/documents/upload/AZStudio/scripts/PSCModul_Doprava/PSC/"; // např. https://www.domena.cz/psc/ (může být i absolutní URL)
29
30
31  const PSC_FILE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
32  const PSC_FILE_CACHE_PREFIX = "psc_shipping_list_v1:"; // localStorage prefix
33
34  // ==================================================
35  // HELPERS
36  // ==================================================
37  function normalizePsc(raw) {
38    if (!raw) return "";
39    const digits = String(raw).replace(/\s+/g, "").replace(/\D+/g, "");
40    return digits.length === 5 ? digits : "";
41  }
42
43  function debounce(fn, ms) {
44    let t;
45    return function (...args) {
46      clearTimeout(t);
47      t = setTimeout(() => fn.apply(this, args), ms);
48    };
49  }
50
51  function getShippingWrappers() {
52    const c = document.querySelector("#order-shipping-methods");
53    if (!c) return [];
54    return Array.from(c.querySelectorAll(".radio-wrapper"));
55  }
56
57  function getShippingId(wrap) {
58    const input = wrap.querySelector("input[name='shippingId']");
59    return input ? parseInt(input.value, 10) : null;
60  }
61
62  function getRadioById(id) {
63    return document.querySelector(`input[name="shippingId"][value="${id}"]`);
64  }
65
66  function getWrapperById(id) {
67    const r = getRadioById(id);
68    return r ? r.closest(".radio-wrapper") : null;
69  }
70
71  function isVisible(el) {
72    return !!el && el.style.display !== "none";
73  }
74
75  function isPscInRanges(psc, ranges) {
76    return (ranges || []).some(([a, b]) => psc >= a && psc <= b);
77  }
78
79  // ==================================================
80  // TXT LIST CACHE (in-memory + localStorage TTL)
81  // ==================================================
82  const inMemoryListCache = new Map(); // id -> Promise<Set>
83
84  function storageKeyForId(id) {
85    return `${PSC_FILE_CACHE_PREFIX}${id}`;
86  }
87
88  function getCachedFromLocalStorage(id) {
89    try {
90      const raw = localStorage.getItem(storageKeyForId(id));
91      if (!raw) return null;
92      const parsed = JSON.parse(raw);
93      if (!parsed || !parsed.ts || !Array.isArray(parsed.list)) return null;
94      if (Date.now() - parsed.ts > PSC_FILE_CACHE_TTL_MS) return null;
95      return new Set(parsed.list);
96    } catch (e) {
97      return null;
98    }
99  }
100
101  function saveCachedToLocalStorage(id, set) {
102    try {
103      const payload = { ts: Date.now(), list: Array.from(set) };
104      localStorage.setItem(storageKeyForId(id), JSON.stringify(payload));
105    } catch (e) {}
106  }
107
108  function resolveFileUrl(id, conf) {
109    if (conf && typeof conf.file === "string") return conf.file;
110    if (conf && conf.file === true) return `${PSC_FILES_BASE_URL}${id}.txt`;
111    // default: pokud file není definované, tak null
112    return null;
113  }
114
115  async function loadPscSetFromFile(id, conf) {
116    const url = resolveFileUrl(id, conf);
117    if (!url) return new Set();
118
119    // localStorage cache
120    const cached = getCachedFromLocalStorage(id);
121    if (cached) return cached;
122
123    // fetch
124    const res = await fetch(url, { cache: "force-cache" });
125    if (!res.ok) throw new Error(`PSC file ${url} HTTP ${res.status}`);
126    const text = await res.text();
127
128    const set = new Set(
129      text
130        .split(/\r?\n/)
131        .map(x => x.trim())
132        .filter(x => /^\d{5}
132$/.test(x))
133    );
134
135    saveCachedToLocalStorage(id, set);
136    return set;
137  }
138
139  function getPscSetForShipping(id) {
140    const conf = PSC_DEPENDENT_SHIPPINGS[id] || {};
141    const url = resolveFileUrl(id, conf);
142    if (!url) return Promise.resolve(new Set()); // žádný soubor
143
144    if (inMemoryListCache.has(id)) return inMemoryListCache.get(id);
145
146    const p = loadPscSetFromFile(id, conf)
147      .catch(err => {
148        console.warn("PSC file load failed for shipping", id, err);
149        return new Set();
150      });
151
152    inMemoryListCache.set(id, p);
153    return p;
154  }
155
156  // ==================================================
157  // UI
158  // ==================================================
159  function ensureUI() {
160    if (document.getElementById("pscFilter")) return;
161
162    const methods = document.querySelector("#order-shipping-methods");
163    if (!methods) return;
164
165    const d = document.createElement("div");
166    d.className = "psc-filter-wrap";
167    d.style.margin = "10px 0 15px";
168    d.innerHTML = `
169      <div style="padding:10px 12px;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#f7f7f7">
170        <label for="pscFilter" style="font-weight:600;display:block;margin-bottom:6px">
171          Zadejte PSČ pro zobrazení možností dopravy
172        </label>
173        <input id="pscFilter" type="text" inputmode="numeric" autocomplete="postal-code"
174          placeholder="110 00 / 11000" maxlength="6"
175          style="max-width:220px;width:100%;padding:8px 10px;border-radius:6px;border:1px solid #ccc">
176        <div id="pscHint" style="margin-top:6px;font-size:12px;opacity:.75"></div>
177      </div>
178    `;
179
180    methods.parentNode.insertBefore(d, methods);
181  }
182
183  // ==================================================
184  // POKRAČOVAT button lock + uložení
185  // ==================================================
186  function setNextButtonEnabled(enabled) {
187    const btn = document.getElementById("orderFormButton");
188    if (!btn) return;
189
190    btn.disabled = !enabled;
191    btn.style.pointerEvents = enabled ? "" : "none";
192    btn.style.opacity = enabled ? "" : "0.45";
193    btn.style.filter = enabled ? "" : "grayscale(1)";
194    btn.setAttribute("aria-disabled", enabled ? "false" : "true");
195  }
196
197  function hookNextButtonSave(inputEl) {
198    const btn = document.getElementById("orderFormButton");
199    if (!btn || btn.dataset.pscHooked) return;
200
201    btn.addEventListener("click", () => {
202      const psc = normalizePsc(inputEl?.value || "");
203      if (psc) savePscToStorage(psc);
204    }, { capture: true });
205
206    btn.dataset.pscHooked = "1";
207  }
208
209  // ==================================================
210  // Pravidla dostupnosti dopravy pro PSČ
211  // ==================================================
212  async function isShippingAllowedForPsc(id, psc) {
213    const conf = PSC_DEPENDENT_SHIPPINGS[id];
214    if (!conf) return false;
215
216    // exact list in config
217    if (Array.isArray(conf.exact) && conf.exact.includes(psc)) return true;
218
219    // ranges in config
220    if (Array.isArray(conf.ranges) && isPscInRanges(psc, conf.ranges)) return true;
221
222    // file list
223    const url = resolveFileUrl(id, conf);
224    if (url) {
225      const set = await getPscSetForShipping(id);
226      return set.has(psc);
227    }
228
229    return false;
230  }
231
232  function getAllowedZoneIdsForPscSync(psc) {
233    // sync pokus pouze pro range/exact (bez file) — pro auto-select to stačí, file varianty budou vyřešeny níže v async části
234    if (!psc) return [];
235    return ZONE_PRIORITY.filter(id => {
236      const conf = PSC_DEPENDENT_SHIPPINGS[id];
237      if (!conf) return false;
238      if (Array.isArray(conf.exact) && conf.exact.includes(psc)) return true;
239      if (Array.isArray(conf.ranges) && isPscInRanges(psc, conf.ranges)) return true;
240      return false;
241    });
242  }
243
244  function autoSelectZoneIfAvailableSync(psc) {
245    if (!ZONE_PRIORITY.length) return false;
246    const zoneIds = getAllowedZoneIdsForPscSync(psc);
247    if (!zoneIds.length) return false;
248
249    for (const id of zoneIds) {
250      const w = getWrapperById(id);
251      if (isVisible(w)) {
252        const r = getRadioById(id);
253        if (r && !r.checked) {
254          r.checked = true;
255          r.dispatchEvent(new Event("change", { bubbles: true }));
256          r.dispatchEvent(new Event("click", { bubbles: true }));
257        }
258        return true;
259      }
260    }
261    return false;
262  }
263
264  // ==================================================
265  // FILTRACE (async, kvůli TXT file)
266  // ==================================================
267  let applyToken = 0;
268
269  async function applyFilter(psc) {
270    const token = ++applyToken;
271    const wrappers = getShippingWrappers();
272
273    if (!psc) {
274      wrappers.forEach(w => (w.style.display = "none"));
275      setNextButtonEnabled(false);
276      return;
277    }
278
279    setNextButtonEnabled(true);
280
281    // Nejprve ukaž "always" a schovej ostatní (aby UI nereagovalo pozdě)
282    wrappers.forEach(w => {
283      const id = getShippingId(w);
284      if (!id) return;
285
286      if (ALWAYS_AFTER_PSC.includes(id)) {
287        w.style.display = "";
288        return;
289      }
290
291      if (PSC_DEPENDENT_SHIPPINGS[id]) {
292        // dočasně schovat, dokud nevyhodnotíme (kvůli async)
293        w.style.display = "none";
294        return;
295      }
296
297      w.style.display = "none";
298    });
299
300    // auto-select zónové (range/exact) ještě před file načtením
301    const zoneSelectedSync = autoSelectZoneIfAvailableSync(psc);
302
303    // Vyhodnoť PSČ-řízené dopravy (range/exact/file)
304    const dependentIds = Object.keys(PSC_DEPENDENT_SHIPPINGS).map(x => parseInt(x, 10)).filter(Number.isFinite);
305
306    // Preload file seznamy paralelně (jen ty, co mají file)
307    await Promise.all(dependentIds.map(id => getPscSetForShipping(id).catch(() => new Set())));
308
309    // Pokud mezitím přišel nový input, skonči (race protection)
310    if (token !== applyToken) return;
311
312    for (const id of dependentIds) {
313      const wrap = getWrapperById(id);
314      if (!wrap) continue;
315
316      const allowed = await isShippingAllowedForPsc(id, psc);
317
318      // Pokud mezitím přišel nový input, skonči
319      if (token !== applyToken) return;
320
321      wrap.style.display = allowed ? "" : "none";
322    }
323
324    // auto-select zónové i pro "file" variantu (pokud je nastavená priority)
325    if (ZONE_PRIORITY.length) {
326      for (const id of ZONE_PRIORITY) {
327        const wrap = getWrapperById(id);
328        if (isVisible(wrap)) {
329          const allowed = await isShippingAllowedForPsc(id, psc);
330          if (token !== applyToken) return;
331          if (allowed) {
332            const r = getRadioById(id);
333            if (r && !r.checked) {
334              r.checked = true;
335              r.dispatchEvent(new Event("change", { bubbles: true }));
336              r.dispatchEvent(new Event("click", { bubbles: true }));
337            }
338            break;
339          }
340        }
341      }
342    }
343
344    // Pokud je vybraná doprava skrytá, přepni na první viditelnou
345    const selected = document.querySelector('input[name="shippingId"]:checked');
346    if (selected) {
347      const wrap = selected.closest(".radio-wrapper");
348      if (wrap && wrap.style.display === "none") {
349        const firstVisible = wrappers.find(w => w.style.display !== "none");
350        const r = firstVisible?.querySelector('input[name="shippingId"]');
351        if (r) {
352          r.checked = true;
353          r.dispatchEvent(new Event("change", { bubbles: true }));
354          r.dispatchEvent(new Event("click", { bubbles: true }));
355        }
356      }
357    } else if (!zoneSelectedSync) {
358      const firstVisible = wrappers.find(w => w.style.display !== "none");
359      const r = firstVisible?.querySelector('input[name="shippingId"]');
360      if (r) {
361        r.checked = true;
362        r.dispatchEvent(new Event("change", { bubbles: true }));
363        r.dispatchEvent(new Event("click", { bubbles: true }));
364      }
365    }
366  }
367
368  // ==================================================
369  // INIT
370  // ==================================================
371  function init() {
372    ensureUI();
373
374    const input = document.getElementById("pscFilter");
375    const hint = document.getElementById("pscHint");
376    if (!input) return;
377
378    hookNextButtonSave(input);
379
380    const onInput = debounce(() => {
381      const psc = normalizePsc(input.value);
382
383      if (hint) {
384        if (!input.value) hint.textContent = "Zadejte PSČ – bez PSČ nejdou zobrazit dopravy ani pokračovat.";
385        else if (!psc) hint.textContent = "Neplatné PSČ — zadejte 5 číslic (např. 11000 nebo 110 00).";
386        else hint.textContent = "Zobrazeny dopravy pro PSČ: " + psc;
387      }
388
389      if (psc) savePscToStorage(psc);
390
391      applyFilter(psc);
392    }, 250);
393
394    if (!input.dataset.listenerAttached) {
395      input.addEventListener("input", onInput);
396      input.dataset.listenerAttached = "1";
397    }
398
399// ZABRÁNIT potvrzení ENTEREM v poli PSČ
400input.addEventListener("keydown", function (e) {
401  if (e.key === "Enter") {
402    e.preventDefault();
403    e.stopPropagation();
404    return false;
405  }
406});
407
408    // Předvyplnění z paměti – JEN JEDNOU
409if (!input.dataset.prefilledOnce) {
410  const stored = loadPscFromStorage();
411  if (stored && !input.value) {
412    input.value = stored;
413  }
414  input.dataset.prefilledOnce = "1";
415}
416
417    // aplikace stavu při startu
418    const initial = normalizePsc(input.value);
419    applyFilter(initial);
420
421    if (hint) {
422      if (!input.value) hint.textContent = "Zadejte PSČ – bez PSČ nejdou zobrazit dopravy ani pokračovat.";
423      else if (!initial) hint.textContent = "Neplatné PSČ — zadejte 5 číslic (např. 11000 nebo 110 00).";
424      else hint.textContent = "Zobrazeny dopravy pro PSČ: " + initial;
425    }
426  }
427
428  // Checkout se může překreslovat – init opakovaně
429  let tries = 0;
430  const iv = setInterval(() => {
431    tries++;
432    init();
433    if (tries > 12) clearInterval(iv);
434  }, 700);
435
436  document.readyState === "loading"
437    ? document.addEventListener("DOMContentLoaded", init)
438    : init();
439})();

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.