"use strict"; // ── Config ────────────────────────────────────────────────────────────── const WEBHOOK = "https://brandize.app.n8n.cloud/webhook/transfer"; const BATCH_WEBHOOK = "https://brandize.app.n8n.cloud/webhook/batch"; // ── State ─────────────────────────────────────────────────────────────────── let creds = null; // { user, pass } let pendingAuthCb = null; // function to retry after auth let scanCount = 0; const scanQueue = []; // { code, user, rowEl } — FIFO let queueBusy = false; // true while drainQueue is running // Batches let allBatches = []; let scanDebounceTimer = null; let sortField = "name"; let sortAsc = true; // ── DOM refs ───────────────────────────────────────────────────────────── const scanInput = document.getElementById("scanInput"); const userSelect = document.getElementById("userSelect"); const tableBody = document.getElementById("tableBody"); const emptyState = document.getElementById("emptyState"); const countPill = document.getElementById("countPill"); const queuePill = document.getElementById("queuePill"); const queuePillText = document.getElementById("queuePillText"); const banner = document.getElementById("statusBanner"); const authModal = document.getElementById("authModal"); const authUser = document.getElementById("authUser"); const authPass = document.getElementById("authPass"); const authBtn = document.getElementById("authBtn"); const modalError = document.getElementById("modalError"); const toast = document.getElementById("toast"); const scanSearch = document.getElementById("scanSearch"); const batchSearch = document.getElementById("batchSearch"); const batchList = document.getElementById("batchList"); const batchStateFilter = document.getElementById("batchStateFilter"); const scanPanel = document.getElementById("scanPanel"); const batchesPanel = document.getElementById("batchesPanel"); // ── Cookie helpers ──────────────────────────────────────────────────────── function saveCreds(user, pass) { const val = btoa(encodeURIComponent(JSON.stringify({ user, pass }))); document.cookie = `hr_auth=${val}; max-age=${30 * 24 * 3600}; SameSite=Strict; path=/`; } function loadCredsFromCookie() { const match = document.cookie.match(/(?:^|;\s*)hr_auth=([^;]+)/); if (match) { try { return JSON.parse(decodeURIComponent(atob(match[1]))); } catch (_) {} } return null; } function clearCredsCookie() { document.cookie = "hr_auth=; max-age=0; path=/"; } // ── JSON parser (handles array, single object, and NDJSON) ─────────────── async function parseJson(res) { const text = await res.text(); // Standard JSON (array or object) try { const parsed = JSON.parse(text); return parsed; } catch (_) {} // NDJSON — n8n sometimes returns one object per line try { const lines = text .trim() .split("\n") .filter((l) => l.trim()); if (lines.length > 0) { return lines.map((l) => JSON.parse(l)); } } catch (_) {} throw new Error("Could not parse server response"); } // ── Helpers ────────────────────────────────────────────────────────────── function esc(str) { return String(str ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function getHeaders(json = true) { const h = {}; if (json) h["Content-Type"] = "application/json"; if (creds) { h["Authorization"] = "Basic " + btoa(creds.user + ":" + creds.pass); } return h; } // ── Toast ──────────────────────────────────────────────────────────────── let toastTimer = null; function showToast(msg, type = "", ms = 3200) { clearTimeout(toastTimer); toast.textContent = msg; toast.className = "visible " + type; toastTimer = setTimeout(() => { toast.className = ""; }, ms); } // ── Status Banner ──────────────────────────────────────────────────────── let bannerTimer = null; function showBanner(msg, type, autohide = 0) { clearTimeout(bannerTimer); banner.textContent = ""; if (type === "loading") { const dots = document.createElement("div"); dots.className = "dot-spinner"; dots.innerHTML = ""; banner.appendChild(dots); const t = document.createTextNode(" " + msg); banner.appendChild(t); } else { banner.textContent = msg; } banner.className = "active " + type; if (autohide) bannerTimer = setTimeout(hideBanner, autohide); } function hideBanner() { banner.className = ""; banner.textContent = ""; } // ── Auth Modal ─────────────────────────────────────────────────────────── function needAuth(retryCb) { pendingAuthCb = retryCb; modalError.style.display = "none"; // Pre-fill from saved cookie so user only needs to confirm const saved = loadCredsFromCookie(); authUser.value = saved?.user || ""; authPass.value = saved?.pass || ""; authModal.classList.remove("hidden"); // Focus password if username already filled, otherwise username setTimeout(() => (saved ? authPass.focus() : authUser.focus()), 50); } function cancelAuth() { authModal.classList.add("hidden"); pendingAuthCb = null; showBanner("Authentication cancelled.", "error", 4000); } async function submitAuth() { const u = authUser.value.trim(); const p = authPass.value; if (!u || !p) { modalError.textContent = "Please enter both username and password."; modalError.style.display = "block"; return; } creds = { user: u, pass: p }; saveCreds(u, p); // persist to cookie authBtn.classList.add("loading"); authBtn.disabled = true; modalError.style.display = "none"; // Close modal first, then retry authModal.classList.add("hidden"); authBtn.classList.remove("loading"); authBtn.disabled = false; if (pendingAuthCb) { const cb = pendingAuthCb; pendingAuthCb = null; await cb(); } } authPass.addEventListener("keydown", (e) => { if (e.key === "Enter") submitAuth(); }); // ── Load Users ─────────────────────────────────────────────────────────── async function loadUsers() { showBanner("Loading users…", "loading"); try { const res = await fetch(WEBHOOK, { method: "GET", headers: getHeaders(false), }); if (res.status === 401) { hideBanner(); needAuth(loadUsers); return; } if (!res.ok) { showBanner(`Could not load users (HTTP ${res.status}).`, "error"); userSelect.innerHTML = ''; return; } const data = await parseJson(res); console.log("[HR] Users response:", data); buildDropdown(data); hideBanner(); } catch (err) { console.error(err); showBanner("Network error – could not reach the server.", "error"); userSelect.innerHTML = ''; } } function buildDropdown(users) { userSelect.innerHTML = ''; // Normalise all shapes n8n may return: // { Name: ["Oliver", "Luka", "Benni"] } ← actual format // [ { Name: "Oliver" }, ... ] ← standard array // { Name: "Oliver" } ← single object let names = []; if (Array.isArray(users)) { // Array of objects: pull .Name from each names = users.map((u) => u.Name).filter(Boolean); } else if (users && Array.isArray(users.Name)) { // Single object with a Name array names = users.Name.filter(Boolean); } else if (users && users.Name) { // Single object with a single Name string names = [users.Name]; } names.forEach((name) => { userSelect.appendChild(new Option(name, name)); }); } // ── Duplicate check ────────────────────────────────────────────────────── // Checks both resolved rows and pending rows (all have .col-code) function isDuplicate(code) { for (const cell of tableBody.querySelectorAll(".col-code")) { if (cell.textContent === code) return true; } return false; } // ── Queue pill ────────────────────────────────────────────────────────── function updateQueuePill() { const n = scanQueue.length; if (n > 0) { queuePillText.textContent = n + (n === 1 ? " pending" : " pending"); queuePill.classList.remove("hidden"); } else { queuePill.classList.add("hidden"); } } // ── Barcode extraction ──────────────────────────────────────────────── // Scanned codes have 8 space-separated blocks; the 3rd through 6th // (0-indexed 2..5) form the real scan code, concatenated without spaces. // E.g. "0003 580 0626 5027 3077 23 328 040" → "06265027307723" function extractCode(raw) { const parts = raw.trim().split(/\s+/); if (parts.length >= 6) { return parts.slice(2, 6).join(""); } return raw.trim(); // fallback: use as-is } // ── Enqueue (called immediately on Enter) ────────────────────────────── function enqueue(raw) { const code = extractCode(raw); if (!code) return; const user = userSelect.value; if (!user) { showToast("Please select a user first!", "fail"); userSelect.style.outline = "2px solid rgba(255,80,80,0.8)"; setTimeout(() => { userSelect.style.outline = ""; }, 1200); return; } if (isDuplicate(code)) { showToast("⚠️ Already scanned: " + code, "fail", 4000); scanInput.select(); return; } // Auto-switch to scan log tab switchToScanTab(); // Clear input immediately so the next scan can happen right away scanInput.value = ""; scanInput.focus(); hideBanner(); // Add a pending row to the table (also registers code for dupe detection) const rowEl = addPendingRow(code); // Push onto queue scanQueue.push({ code, user, rowEl }); updateQueuePill(); // Kick off the queue processor if it isn't running already if (!queueBusy) drainQueue(); } // ── Batch search/filter events ────────────────────────────────────────── batchSearch.addEventListener("input", renderBatches); batchStateFilter.addEventListener("change", renderBatches); // ── Queue drain (processes items sequentially in the background) ──────── async function drainQueue() { if (queueBusy) return; queueBusy = true; while (scanQueue.length > 0) { const item = scanQueue[0]; // peek — only shift after handling const result = await processItem(item); if (result === "auth") { // Credentials needed — pause queue, resume after login queueBusy = false; needAuth(drainQueue); return; } scanQueue.shift(); updateQueuePill(); } queueBusy = false; updateQueuePill(); } // ── Process one queued item ──────────────────────────────────────────── async function processItem({ code, user, rowEl }) { try { const res = await fetch(WEBHOOK, { method: "POST", headers: getHeaders(true), body: JSON.stringify({ user, code }), }); if (res.status === 401) return "auth"; // signal caller to pause if (!res.ok) { let errMsg = `Scan failed (HTTP ${res.status})`; try { const errData = await parseJson(res); if (errData?.error) errMsg = errData.error; } catch (_) {} rowError(rowEl, errMsg); return "ok"; } const item = await parseJson(res); if (item?.error) { rowError(rowEl, item.error); return "ok"; } resolveRow(rowEl, item); return "ok"; } catch (err) { console.error(err); rowError(rowEl, "Network error – could not reach server"); return "ok"; } } // ── Table rows ──────────────────────────────────────────────────────────────── function addPendingRow(code) { emptyState.style.display = "none"; const tr = document.createElement("tr"); tr.className = "row-pending"; tr.innerHTML = `