// Role Permissions Mapping const ROLE_PERMISSIONS = { ADMIN: ["/", "/register-customer", "/register-employee", "/assign", "/notes", "/leads"], RECEPTIONIST: ["/register-customer", "/assign", "/notes", "/leads"], EMPLOYEE: ["/notes", "/leads"], OTHER: [] }; // Global HTML Escaping Utility function escapeHtml(str) { if (str === null || str === undefined) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Token Storage & Auth Helpers function getToken() { return sessionStorage.getItem("jwt_token") || localStorage.getItem("jwt_token"); } function getUserRole() { const role = sessionStorage.getItem("user_role") || localStorage.getItem("user_role") || "EMPLOYEE"; return role.toString().toUpperCase().trim(); } function getUserName() { return sessionStorage.getItem("user_name") || localStorage.getItem("user_name") || "User"; } function getSelectedBranch() { return sessionStorage.getItem("user_branch") || localStorage.getItem("user_branch") || ""; } function setSelectedBranch(branch, rememberMe = true) { const storage = rememberMe ? localStorage : sessionStorage; storage.setItem("user_branch", branch); } function setUserData(token, role, name, rememberMe = true, branch = "") { clearToken(); const storage = rememberMe ? localStorage : sessionStorage; storage.setItem("jwt_token", token); storage.setItem("user_role", role); storage.setItem("user_name", name); if (branch) { storage.setItem("user_branch", branch); } } function clearToken() { localStorage.removeItem("jwt_token"); localStorage.removeItem("user_role"); localStorage.removeItem("user_name"); localStorage.removeItem("user_branch"); sessionStorage.removeItem("jwt_token"); sessionStorage.removeItem("user_role"); sessionStorage.removeItem("user_name"); sessionStorage.removeItem("user_branch"); } function logout() { clearToken(); window.location.href = "/login"; } function normalizePath(p) { if (!p) return ""; let clean = p.split("?")[0].split("#")[0]; if (clean.endsWith(".html")) { clean = "/" + clean.split("/").pop().replace(".html", ""); } if (clean === "/index") clean = "/"; return clean.replace(/_/g, "-"); } function checkAuth() { const path = window.location.pathname; const isLoginPage = path.endsWith("/login") || path.endsWith("login.html"); const token = getToken(); if (!isLoginPage && !token) { window.location.href = "/login"; return; } if (token) { const role = getUserRole(); const allowedPages = ROLE_PERMISSIONS[role] || []; const normCurrent = normalizePath(path); if (isLoginPage) { if (allowedPages.length > 0) { window.location.href = allowedPages[0]; } return; } // Check path permission const isAllowed = allowedPages.some(p => normalizePath(p) === normCurrent || p === path); if (!isAllowed && allowedPages.length > 0) { window.location.href = allowedPages[0]; } } } // Run auth check immediately checkAuth(); function renderNav() { const role = getUserRole(); const allowedPages = ROLE_PERMISSIONS[role] || []; const nav = document.querySelector("nav"); if (nav) { const navLeft = nav.querySelector(".nav-left"); if (navLeft) { let mobileHeader = nav.querySelector(".nav-header-mobile"); if (!mobileHeader) { mobileHeader = document.createElement("div"); mobileHeader.className = "nav-header-mobile"; const brand = navLeft.querySelector(".brand"); if (brand) { mobileHeader.appendChild(brand); } else { const brandSpan = document.createElement("span"); brandSpan.className = "brand"; brandSpan.textContent = "🍴 Utensils Wholesale"; mobileHeader.appendChild(brandSpan); } const toggleBtn = document.createElement("button"); toggleBtn.type = "button"; toggleBtn.className = "nav-toggle"; toggleBtn.id = "navToggleBtn"; toggleBtn.setAttribute("aria-label", "Toggle navigation menu"); toggleBtn.innerHTML = "☰ Menu"; toggleBtn.onclick = function (e) { e.stopPropagation(); navLeft.classList.toggle("open"); }; mobileHeader.appendChild(toggleBtn); nav.insertBefore(mobileHeader, nav.firstChild); } } } // Filter links in nav-left const navLinks = document.querySelectorAll(".nav-left a"); navLinks.forEach(link => { let href = link.getAttribute("href"); if (!href) return; const normHref = normalizePath(href); const isAllowed = allowedPages.some(p => normalizePath(p) === normHref || p === href); if (!isAllowed) { link.style.setProperty("display", "none", "important"); } else { link.style.setProperty("display", "inline-block", "important"); } link.addEventListener("click", () => { const navLeft = document.querySelector(".nav-left"); if (navLeft) navLeft.classList.remove("open"); }); }); // Display user badge in nav-right if available const navRight = document.querySelector(".nav-right"); if (navRight) { let badge = document.getElementById("userBadge"); if (!badge) { badge = document.createElement("span"); badge.id = "userBadge"; badge.className = "nav-user"; navRight.insertBefore(badge, navRight.firstChild); } const userName = escapeHtml(getUserName()); const branch = getSelectedBranch(); const branchBadge = branch ? `📍 ${escapeHtml(branch)}` : ""; badge.innerHTML = `${branchBadge}${userName} ${escapeHtml(role)}`; } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", renderNav); } else { renderNav(); } function getAuthHeaders(extraHeaders = {}) { const token = getToken(); const branch = getSelectedBranch(); const headers = { "Content-Type": "application/json", ...extraHeaders }; if (token) { headers["Authorization"] = `Bearer ${token}`; } if (!("X-Selected-Branch" in headers) && !("x-selected-branch" in headers)) { if (branch && branch !== "All" && branch !== "All Branches") { headers["X-Selected-Branch"] = branch; } } return headers; } async function handleResponse(res) { let data = {}; try { data = await res.json(); } catch (_) { } if (res.status === 401) { const isLoginPage = window.location.pathname.endsWith("/login") || window.location.pathname.endsWith("login.html"); if (!isLoginPage) { clearToken(); window.location.href = "/login"; } throw new Error(data.detail || "Invalid email or password"); } if (!res.ok) { let errMsg = "Request failed"; if (typeof data.detail === "string") { errMsg = data.detail; } else if (Array.isArray(data.detail)) { errMsg = data.detail.map(d => (d.msg ? `${d.loc ? d.loc.filter(l => l !== 'body').join('.') + ': ' : ''}${d.msg}` : JSON.stringify(d))).join("; "); } else if (data.detail && typeof data.detail === "object") { errMsg = JSON.stringify(data.detail); } else if (data.message) { errMsg = data.message; } throw new Error(errMsg); } return data; } async function apiGet(url, extraHeaders = {}) { const res = await fetch(url, { headers: getAuthHeaders(extraHeaders), }); return await handleResponse(res); } async function apiPost(url, body, extraHeaders = {}) { const res = await fetch(url, { method: "POST", headers: getAuthHeaders(extraHeaders), body: JSON.stringify(body), }); return await handleResponse(res); } async function apiPut(url, body = {}, extraHeaders = {}) { const res = await fetch(url, { method: "PUT", headers: getAuthHeaders(extraHeaders), body: JSON.stringify(body), }); return await handleResponse(res); } async function apiDelete(url, extraHeaders = {}) { const res = await fetch(url, { method: "DELETE", headers: getAuthHeaders(extraHeaders), }); return await handleResponse(res); } function showMsg(el, text, type = "info") { if (!el) return; el.textContent = text; el.className = `msg ${type}`; el.style.display = "block"; } function fmtTime(iso) { if (!iso) return "-"; if (iso instanceof Date) { return isNaN(iso.getTime()) ? "-" : iso.toLocaleString("en-IN", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: true }); } let str = String(iso).trim(); if (!str) return "-"; let d; if (str.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(str)) { d = new Date(str); } else { // Parse naive ISO local timestamp (e.g. "2026-09-17T16:12:30.123456" or "2026-09-17 16:12:30") const parts = str.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/); if (parts) { const year = parseInt(parts[1], 10); const month = parseInt(parts[2], 10) - 1; const day = parseInt(parts[3], 10); const hours = parseInt(parts[4] || "0", 10); const minutes = parseInt(parts[5] || "0", 10); const seconds = parseInt(parts[6] || "0", 10); d = new Date(year, month, day, hours, minutes, seconds); } else { d = new Date(str); } } if (isNaN(d.getTime())) { return "-"; } return d.toLocaleString("en-IN", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: true }); } // ========================================== // Global Data Export System (PDF & Word) // ========================================== let currentExportConfig = null; function ensureExportModalHtml() { if (document.getElementById("globalExportModal")) return; const modalHtml = ` `; document.body.insertAdjacentHTML("beforeend", modalHtml); document.getElementById("globalExportForm").addEventListener("submit", handleExportFormSubmit); } function toggleCustomDateBox() { const duration = document.getElementById("exportDurationSelect").value; const customBox = document.getElementById("customDateRangeBox"); customBox.style.display = duration === "custom" ? "flex" : "none"; } function openExportModal(config) { ensureExportModalHtml(); currentExportConfig = config; document.getElementById("exportModalTitle").textContent = "Export " + (config.title || "Data"); document.getElementById("exportFormatSelect").value = "pdf"; document.getElementById("exportDurationSelect").value = "all"; document.getElementById("customDateRangeBox").style.display = "none"; document.getElementById("exportFromDate").value = ""; document.getElementById("exportToDate").value = ""; const msgEl = document.getElementById("exportModalMsg"); msgEl.style.display = "none"; document.getElementById("globalExportModal").style.display = "flex"; } function closeExportModal() { const modal = document.getElementById("globalExportModal"); if (modal) modal.style.display = "none"; } async function handleExportFormSubmit(e) { e.preventDefault(); if (!currentExportConfig) return; const format = document.getElementById("exportFormatSelect").value; const duration = document.getElementById("exportDurationSelect").value; const fromDate = document.getElementById("exportFromDate").value; const toDate = document.getElementById("exportToDate").value; const msgEl = document.getElementById("exportModalMsg"); const submitBtn = document.getElementById("downloadExportSubmitBtn"); if (duration === "custom" && (!fromDate || !toDate)) { showMsg(msgEl, "Please select both From Date and To Date for custom range.", "error"); return; } msgEl.style.display = "none"; submitBtn.disabled = true; submitBtn.textContent = "Processing..."; try { let items = typeof currentExportConfig.getDataFn === "function" ? await currentExportConfig.getDataFn() : (currentExportConfig.data || []); const filtered = filterExportData(items, currentExportConfig.dateField || "created_at", duration, fromDate, toDate); if (!filtered || filtered.length === 0) { showMsg(msgEl, "No data records found for the selected date range.", "error"); submitBtn.disabled = false; submitBtn.textContent = "📥 Download File"; return; } if (format === "word") { generateWordExport(currentExportConfig.title, currentExportConfig.columns, filtered, currentExportConfig.defaultFilename || "export"); } else { generatePdfExport(currentExportConfig.title, currentExportConfig.columns, filtered); } closeExportModal(); } catch (err) { showMsg(msgEl, "Export failed: " + err.message, "error"); } finally { submitBtn.disabled = false; submitBtn.textContent = "📥 Download File"; } } function filterExportData(items, dateField, duration, fromDateStr, toDateStr) { if (!items || !items.length || duration === "all") return items; const now = new Date(); let cutoff = null; if (duration === "1m") cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); else if (duration === "2m") cutoff = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000); else if (duration === "3m") cutoff = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); else if (duration === "6m") cutoff = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000); if (cutoff) { return items.filter(item => { const val = item[dateField] || item.created_at || item.assigned_at || item.updated_at; if (!val) return true; return new Date(val) >= cutoff; }); } if (duration === "custom") { const from = fromDateStr ? new Date(fromDateStr + "T00:00:00") : null; const to = toDateStr ? new Date(toDateStr + "T23:59:59") : null; return items.filter(item => { const val = item[dateField] || item.created_at || item.assigned_at || item.updated_at; if (!val) return true; const d = new Date(val); if (from && d < from) return false; if (to && d > to) return false; return true; }); } return items; } function generateWordExport(title, columns, data, filename) { const renderCell = (col, row) => { try { if (col.formatter) return col.formatter(row); const val = row[col.key]; return escapeHtml(val ?? '-'); } catch (_) { return '-'; } }; const html = ` ${escapeHtml(title)}

🍴 Utensils Wholesale — ${escapeHtml(title)}

Exported Date: ${new Date().toLocaleString('en-IN')} | Total Records: ${data.length}
${columns.map(col => ``).join('')} ${data.map(row => `${columns.map(col => ``).join('')}`).join('')}
${escapeHtml(col.header)}
${renderCell(col, row)}
`; const blob = new Blob(['\ufeff' + html], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${filename}_${new Date().toISOString().slice(0, 10)}.doc`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function generatePdfExport(title, columns, data) { const win = window.open('', '_blank'); if (!win) { alert("Please allow popups to generate PDF document."); return; } const renderCell = (col, row) => { try { if (col.formatter) return col.formatter(row); const val = row[col.key]; return escapeHtml(val ?? '-'); } catch (_) { return '-'; } }; win.document.write(` ${escapeHtml(title)}

🍴 Utensils Wholesale — ${escapeHtml(title)}

Generated: ${new Date().toLocaleString('en-IN')} | Total Records: ${data.length}
${columns.map(col => ``).join('')} ${data.map(row => `${columns.map(col => ``).join('')}`).join('')}
${escapeHtml(col.header)}
${renderCell(col, row)}
`); win.document.close(); win.focus(); setTimeout(() => { win.print(); }, 400); }