Preparing your workspace…

Live
AK Aditi Kapoor
`); win.document.close(); win.focus(); setTimeout(() => win.print(), 400); }, }; const Reports = { render() { return `

Reports

Export inventory data in your preferred format.

Inventory Export

Exports the full product catalog (${Utils.number(State.products.length)} records).

Summary

Snapshot of current inventory health.

Total Stock Value${Utils.currency(State.kpi.stockValue)}
Low Stock Items${Utils.number(State.kpi.lowStock)}
Out Of Stock Items${Utils.number(State.kpi.outOfStock)}
`; }, mount() { document.getElementById("repExcel").addEventListener("click", () => Export.exportXLSX(State.products, "inventory_report")); document.getElementById("repCSV").addEventListener("click", () => Export.exportCSV(State.products, "inventory_report")); document.getElementById("repPDF").addEventListener("click", () => Export.exportPDF(State.products, "inventory_report")); document.getElementById("repPrint").addEventListener("click", () => window.print()); }, }; /* ========================================================================== 16. AUDIT LOG PAGE ========================================================================== */ const AuditLog = { render() { return `

Audit Log

Full history of inventory and pricing changes.

Activity

`; }, mount() { document.getElementById("auditSearch").addEventListener("input", Utils.debounce((e) => { this._search = e.target.value.toLowerCase(); State.auditPager.page = 1; this.refresh(); }, 220)); this.refresh(); }, _search: "", refresh() { let rows = State.audit; if (this._search) rows = rows.filter(r => r.product.toLowerCase().includes(this._search) || r.user.toLowerCase().includes(this._search)); const { page, pageSize } = State.auditPager; const totalPages = Math.max(1, Math.ceil(rows.length / pageSize)); State.auditPager.page = Utils.clamp(page, 1, totalPages); const start = (State.auditPager.page - 1) * pageSize; const pageRows = rows.slice(start, start + pageSize); const tableEl = document.getElementById("auditTable"); if (!pageRows.length) { tableEl.innerHTML = `
${Dashboard._icon("search")}

No activity found

`; } else { tableEl.innerHTML = `DateTimeActionProductQtyUser ${pageRows.map(r => `${Utils.formatDate(r.date)}${Utils.formatTime(r.date)} ${this._actionPill(r.action)}${Utils.escapeHTML(r.product)} ${Utils.number(r.qty)}${Utils.escapeHTML(r.user)}`).join("")}`; } document.getElementById("auditInfo").textContent = rows.length ? `Showing ${start+1}-${Math.min(start+pageSize, rows.length)} of ${rows.length}` : "No records"; const pager = document.getElementById("auditPager"); pager.innerHTML = ` ${State.auditPager.page} / ${totalPages} `; document.getElementById("auditPrev").addEventListener("click", () => { State.auditPager.page--; this.refresh(); }); document.getElementById("auditNext").addEventListener("click", () => { State.auditPager.page++; this.refresh(); }); }, _actionPill(action) { const tint = { "Stock Added": "in", "Stock Sold": "out", "Product Updated": "low", "Product Created": "in" }[action] || "in"; return `${Utils.escapeHTML(action)}`; }, }; /* ========================================================================== 17. SETTINGS PAGE ========================================================================== */ const Settings = { render() { return `

Settings

Workspace preferences and integration status.

Appearance

Personalize how StockLedger looks for you.

Dark Mode
Switch between light and dark themes
Compact Sidebar
Collapse navigation labels

Integration

Google Sheets connection via Apps Script Web App.

Live Connection
${State.online ? "Connected to live Google Sheet" : "Using local fallback data"}
${State.online ? "Online" : "Offline"}
Auto Refresh
Every ${CONFIG.AUTO_REFRESH_MS/1000}s
Enabled
`; }, mount() { document.getElementById("settingsThemeToggle").addEventListener("click", () => Theme.toggle()); document.getElementById("settingsSidebarToggle").addEventListener("click", () => App.toggleSidebar()); document.getElementById("settingsApiUrl").addEventListener("change", (e) => { Toast.show("Note", "Set CONFIG.API_URL in the script to persist this endpoint.", "info"); }); }, }; /* ========================================================================== 18. ROUTER + APP BOOTSTRAP ========================================================================== */ const App = { pages: { dashboard: { render: () => Dashboard.render(), mount: () => Dashboard.mount() }, inventory: { render: () => Inventory.render(), mount: () => Inventory.mount() }, stockin: { render: () => SimplePages.stockin(), mount: () => SimplePages.mount("stockin") }, stockout: { render: () => SimplePages.stockout(), mount: () => SimplePages.mount("stockout") }, sales: { render: () => SimplePages.sales(), mount: () => SimplePages.mount("sales") }, purchase: { render: () => SimplePages.purchase(), mount: () => SimplePages.mount("purchase") }, reports: { render: () => Reports.render(), mount: () => Reports.mount() }, auditlog: { render: () => AuditLog.render(), mount: () => AuditLog.mount() }, settings: { render: () => Settings.render(), mount: () => Settings.mount() }, }, async init() { Theme.init(); Modal.init(); this._bindChrome(); await this.loadData(); this.renderRoute(); this._tickDate(); setInterval(() => this._tickDate(), 30000); this.startAutoRefresh(); Loader.hide(); }, async loadData() { const [products, kpi, audit] = await Promise.all([API.getInventory(), API.getKPI(), API.getAuditLog()]); State.products = products; State.kpi = kpi; State.audit = audit.map(a => ({ ...a, date: new Date(a.date) })); this._updateSyncStatus(); }, refreshKPIFromProducts() { const p = State.products; State.kpi = { ...State.kpi, totalProducts: p.length, currentStock: p.reduce((s,x)=>s+x.currentStock,0), stockValue: p.reduce((s,x)=>s+x.stockValue,0), lowStock: p.filter(x=>x.status==="low").length, outOfStock: p.filter(x=>x.status==="out").length, }; }, renderRoute() { const page = this.pages[State.route] || this.pages.dashboard; document.getElementById("slContent").innerHTML = page.render(); page.mount(); document.querySelectorAll(".sl-nav__item").forEach(el => el.classList.toggle("is-active", el.dataset.route === State.route)); }, navigate(route) { if (!this.pages[route]) return; State.route = route; document.getElementById("slContent").scrollTop = 0; window.scrollTo({ top: 0, behavior: "smooth" }); this.renderRoute(); document.getElementById("sl-root").classList.remove("sl-mobile-open"); }, toggleSidebar() { State.sidebarCollapsed = !State.sidebarCollapsed; document.getElementById("sl-root").classList.toggle("sl-sidebar-collapsed", State.sidebarCollapsed); }, _bindChrome() { document.getElementById("slNav").addEventListener("click", (e) => { const btn = e.target.closest(".sl-nav__item"); if (btn) this.navigate(btn.dataset.route); }); document.getElementById("slSidebarToggle").addEventListener("click", () => this.toggleSidebar()); document.getElementById("slMobileMenuBtn").addEventListener("click", () => document.getElementById("sl-root").classList.add("sl-mobile-open")); document.getElementById("slBackdrop").addEventListener("click", () => document.getElementById("sl-root").classList.remove("sl-mobile-open")); document.getElementById("slRefreshBtn").addEventListener("click", () => this.manualRefresh()); document.getElementById("slGlobalSearch").addEventListener("input", Utils.debounce((e) => { const q = e.target.value.trim(); if (!q) return; if (State.route !== "inventory") this.navigate("inventory"); requestAnimationFrame(() => { const box = document.getElementById("invSearch"); if (box) { box.value = q; box.dispatchEvent(new Event("input")); } }); }, 350)); document.getElementById("slNotifBtn").addEventListener("click", () => { Toast.show("Low stock alert", `${State.kpi.lowStock ?? 0} products are running low on inventory.`, "warning"); document.getElementById("slNotifBadge").style.display = "none"; }); }, async manualRefresh() { const btn = document.getElementById("slRefreshBtn"); btn.classList.add("is-spinning"); await this.loadData(); this.renderRoute(); btn.classList.remove("is-spinning"); Toast.show("Dashboard refreshed", "Latest data has been loaded.", "success", 2600); }, startAutoRefresh() { if (State.refreshTimer) clearInterval(State.refreshTimer); State.refreshTimer = setInterval(async () => { await this.loadData(); if (["dashboard","inventory","auditlog"].includes(State.route)) this.renderRoute(); }, CONFIG.AUTO_REFRESH_MS); }, _updateSyncStatus() { const dot = document.querySelector("#slSyncStatus .sl-syncDot"); const label = document.querySelector("#slSyncStatus .sl-syncLabel"); if (!dot) return; dot.classList.toggle("is-offline", !State.online); label.textContent = State.online ? "Live" : "Offline"; }, _tickDate() { const el = document.getElementById("slHeaderDate"); if (el) el.textContent = new Date().toLocaleString("en-IN", { weekday: "short", day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }); }, }; document.addEventListener("DOMContentLoaded", () => App.init()); if (document.readyState !== "loading") App.init(); })();