/* ============================================================ data.jsx — shared store backed by the real /api endpoints. Replaces the original in-browser mock store: every action calls FastAPI, then re-reads documents so all personas stay in sync. STATUS / STEPS / fmtINR are kept identical to the design. ============================================================ */ /* ---- status model -------------------------------------------------------- */ // step index reference: 1 Fetch · 2 CN Created · 3 Portal · 4 Notify · 5 Review · 6 Tally · 7 Confirm // REMASTERED: keyed on lekha.dn_status ∪ lekha.cn_status — a VERBATIM mirror of // backend app/mock/store.py STATUS (R13). Renaming a key here breaks // STATUS[doc.status] + Object.keys(STATUS) filters, so keep the two in lockstep. const STATUS = { received: { label: "Received", tone: "gray", step: 1, blurb: "Debit note parsed — ready to validate & generate a credit note" }, validating: { label: "Validating…", tone: "blue", step: 1, spin: true, blurb: "Checking the debit note against the Tally mirror" }, validation_failed: { label: "Validation Failed", tone: "amber", step: 1, blurb: "A blocking check failed — flagged to the Lekha exceptions tab; resolving it retries automatically" }, validated: { label: "Validated", tone: "gray", step: 1, blurb: "Passed validation — ready for credit-note drafting" }, cn_drafted: { label: "CN Drafted", tone: "blue", step: 2, blurb: "Credit note drafted from the debit note" }, closed: { label: "Closed", tone: "green", step: 7, blurb: "Credit note posted & verified — pipeline complete" }, draft: { label: "Lekha Review", tone: "amber", step: 3, blurb: "Draft composed on Tally actuals — awaiting Lekha admin review (G1)" }, pending_approval: { label: "Pending Review", tone: "amber", step: 4, blurb: "Awaiting finance team approval" }, in_exception: { label: "In Exception", tone: "amber", step: 4, blurb: "Held in the finance Exceptions tab — being reworked from feedback (out of the review queue)" }, approved: { label: "Approved", tone: "teal", step: 5, blurb: "Approved by finance — queued for e-invoice + Tally" }, irn_pending: { label: "IRN Pending…", tone: "blue", step: 5, spin: true, blurb: "Requesting the e-invoice IRN" }, irn_generated: { label: "IRN Generated", tone: "teal", step: 5, blurb: "IRN received — queued for Tally" }, irn_failed: { label: "IRN Failed", tone: "red", step: 5, blurb: "E-invoice IRN request failed" }, posting: { label: "Posting…", tone: "blue", step: 6, spin: true, blurb: "Writing the voucher into Tally" }, posted: { label: "Posted", tone: "teal", step: 6, blurb: "Voucher created in Tally" }, post_failed: { label: "Post Failed", tone: "red", step: 6, blurb: "Tally rejected the voucher — flagged to exceptions" }, verified_in_tally: { label: "Verified", tone: "green", step: 7, blurb: "Seen again on a later sync — round-trip closed" }, missing_in_tally: { label: "Missing in Tally", tone: "red", step: 7, blurb: "Phantom: we posted it but a later sync can't find it" }, drifted: { label: "Drifted", tone: "amber", step: 7, blurb: "Found in Tally but fields no longer match ours" }, cancelled: { label: "Cancelled", tone: "red", step: 5, blurb: "Rejected / cancelled" }, }; const STEPS = ["Fetch", "CN Drafted", "Lekha Review", "Notify Finance", "Finance Review", "Post to Tally", "Confirm"]; const fmtINR = (n) => "₹" + Number(n || 0).toLocaleString("en-IN", { minimumFractionDigits: n % 1 ? 1 : 0, maximumFractionDigits: 2 }); /* ---- tiny API helper (attaches the bearer token) ------------------------ */ const TOKEN_KEY = "lekha_token"; const api = { async req(method, path, body) { const headers = { "Content-Type": "application/json" }; const token = localStorage.getItem(TOKEN_KEY); if (token) headers.Authorization = `Bearer ${token}`; const opt = { method, headers }; if (body !== undefined) opt.body = JSON.stringify(body); const r = await fetch(path, opt); // session expired/revoked -> bounce to login, but ONLY when a real session // token exists (public pages like the email-approval landing have none and // must never reload-loop on a 401). if (r.status === 401 && !path.includes("/auth/login") && token) { let reason = ""; try { const j = await r.clone().json(); reason = (j && j.detail) || ""; } catch (_) {} localStorage.removeItem(TOKEN_KEY); // single-session: a newer login on another device superseded this one if (reason === "session_superseded") sessionStorage.setItem("lekha_signout", "superseded"); if (!window.__bouncing) { window.__bouncing = true; window.location.reload(); } } if (!r.ok) { // surface the server's human message (e.g. "email already exists") so // signup / team forms can show it; fall back to a terse status line. const e = new Error(`${method} ${path} -> ${r.status}`); e.status = r.status; try { const j = await r.json(); if (j && j.detail) e.message = j.detail; } catch (_) {} throw e; } const ct = r.headers.get("content-type") || ""; return ct.includes("application/json") ? r.json() : null; }, get(p) { return this.req("GET", p); }, post(p, b) { return this.req("POST", p, b); }, put(p, b) { return this.req("PUT", p, b); }, patch(p, b) { return this.req("PATCH", p, b); }, del(p) { return this.req("DELETE", p); }, // Build a same-origin media URL (e.g. a PDF for an