// 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 => `| ${escapeHtml(col.header)} | `).join('')}
${data.map(row => `${columns.map(col => `| ${renderCell(col, row)} | `).join('')}
`).join('')}
`;
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 => `| ${escapeHtml(col.header)} | `).join('')}
${data.map(row => `${columns.map(col => `| ${renderCell(col, row)} | `).join('')}
`).join('')}
`);
win.document.close();
win.focus();
setTimeout(() => {
win.print();
}, 400);
}