);
}
/* 3-hour TAT after 'Generate Credit Note': the ETA the credit note will be ready,
anchored to when it was generated (cnCreatedAt). Formatted like "3:46pm". */
function etaBy(cnCreatedAt) {
if (!cnCreatedAt) return null;
const t = new Date(cnCreatedAt);
if (isNaN(t.getTime())) return null;
t.setHours(t.getHours() + 3);
let h = t.getHours();
const m = t.getMinutes();
const ap = h >= 12 ? "pm" : "am";
h = h % 12 || 12;
return `${h}:${String(m).padStart(2, "0")}${ap}`;
}
/* Finance can also run Step 1 — upload a debit-note PDF and generate its credit note
(same pipeline as Brand Ops: OCR → Lekha admin → finance review → Tally). Mirrors the
Brand Ops "Debit notes" workbench, reusing the shared store methods. */
function FinanceDebitNotes({ store, who }) {
const { docs, bizById, uploadDebitNote, generateCreditNote } = store;
const [busyId, setBusyId] = React.useState(null);
const [uploading, setUploading] = React.useState(false);
const [err, setErr] = React.useState("");
const [range, setRange] = React.useState("all");
const [filter, setFilter] = React.useState("all");
const fileRef = React.useRef(null);
// Brand Ops resolves the brand as `businesses[0] || bizById(...)` — businesses[0] is the
// scoped biz that carries the finance `email`. bizById NEVER returns falsy (it falls back
// to {email:""}), so it must be the SECOND operand, else the empty-email fallback wins and
// greys the upload button even though the business HAS an approver. (Verified: FMC 29 email
// = accountsecom@fixmycurls.in, returned by /api/businesses.)
const brand = (store.businesses || [])[0] || bizById(who.business_id || "");
const noApprover = brand && !brand.email;
let rows = docs.filter((d) => withinRange(d.createdAt, range));
if (filter === "available") rows = rows.filter((d) => d.status === "received");
if (filter === "done") rows = rows.filter((d) => d.status !== "received");
rows = rows.slice().sort((a, b) =>
(a.status === "received" ? 0 : 1) - (b.status === "received" ? 0 : 1) ||
(b.createdAt || "").localeCompare(a.createdAt || ""));
const availableCount = docs.filter((d) => d.status === "received").length;
const doUpload = async (e) => {
const file = e.target.files && e.target.files[0];
if (e.target) e.target.value = "";
if (!file) return;
setErr(""); setUploading(true);
const res = await uploadDebitNote(file);
setUploading(false);
if (res && !res.ok)
setErr(/already been uploaded/i.test(res.error || "") ? "This exact PDF has already been uploaded."
: res.error === "no_finance_approver" ? "No authorised finance email is configured for your business yet."
: "Couldn't upload that PDF — please try again.");
};
const generate = async (id) => {
setErr(""); setBusyId(id);
const res = await generateCreditNote(id);
setBusyId(null);
if (res && !res.ok) setErr("Couldn't generate the credit note just now — please try again.");
};
return (
<>
Debit notes
Upload a debit-note PDF, then generate its credit note — it runs the same pipeline: OCR → Lekha review → your approval → Tally.
{err ? (
{err}
) : (
{availableCount} debit note{availableCount === 1 ? "" : "s"} ready to generate. Generating moves a note into the pipeline until it returns to your review queue.
Credit notes Lekha AI couldn't fix from your first feedback. Give it more detail — you get 2 tries, then it goes to the Lekha team.
These are only yours — the ones you rejected (or Tally rejected after you approved) that need one more round of your guidance. A successful rework comes back to To review; after 2 tries it moves to the Lekha team, who send it back for your approval.
Approve to post directly into Tally, or reject with a note back to Nakad.
Only your authorised email can approve these. Approving auto-posts the entry to Tally — no field-by-field typing. Anything left unactioned auto-approves after 24 hours.
)}
>
);
}
/* ---- Connect Tally — connector = automatic status, Tally = the one button ----
First principle (PRD 2026-06-22): the connector⇄Lekha link is an automatic system
state the user only needs to SEE; the one human decision is connecting Lekha to
THIS company's Tally (granting permission to auto-post) and disconnecting it again.
So: connector is a status light (auto-claimed, never clicked); "Connect Tally" is
the single control and the posting-permission gate — nothing posts until clicked. */
function ConnGraphic({ connectorLinked, connectorPaired, tallyConnected, tallyUp, company }) {
// Pure status visual — nothing here is clickable. The control is the button below.
const wire = (lit) => ({
flex: 1, position: "relative", height: 4, marginTop: 30, borderRadius: 2, overflow: "hidden",
background: lit ? "var(--green-fg)"
: "repeating-linear-gradient(90deg, var(--border) 0 6px, transparent 6px 13px)",
transition: "background .3s",
});
const Node = ({ icon, label, sub, lit }) => (
);
}
// Right-side panel: a simple, reassuring summary of the connector for the user
// (condensed from docs/connector-onboarding.md).
function ConnectorGuide() {
const Item = ({ ok, children }) => (
{ok ? : }{children}
);
const Sec = ({ title, children }) => (
{title}
{children}
);
// "what's inside" — name + plain-words description
const Part = ({ name, children }) => (
{name} — {children}
);
const Bullet = ({ children }) => (
{children}
);
return (
About the Lekha app
A tiny Windows app · ~5 min to set up
A small (~5 MB) Windows app that runs quietly beside TallyPrime — the secure bridge between your Tally and Lekha. Once you approve a credit note, it posts into Tally automatically, with no manual entry. Think of it as a trusted courier sitting next to Tally: it only acts on what you've approved, and you can send it home (disconnect) any time.
{
` Your Tally PC Lekha AI (cloud)
┌──────────────────────┐ ┌────────────────┐
│ TallyPrime ⇄ Connector│──WSS──▶│ Lekha AI │
│ (localhost, your data)│ outbnd │ review/approve │
└──────────────────────┘ └────────────────┘
stays on your PC encrypted, outbound-only`
}
a tiny background program beside Tally; auto-starts when you sign in to Windows.dials out to Lekha over an encrypted connection — never opens your PC to incoming traffic. No firewall changes, no ports to open.a unique cryptographic ID created on first run — that's how Lekha knows it's really your device. No passwords or tokens to copy-paste.talks to TallyPrime on the same PC; this connection stays entirely on your computer.
It also remembers what it has already done, so it never posts the same credit note twice.
A Windows PC with TallyPrime installed.Tally's data gateway is on (the standard setting that lets apps exchange data on the same PC — usually port 9000).The company you want to use is open in TallyPrime.You're signed in to Lekha in your browser.
First-time Windows warning. Because the app is new, Windows may show “Windows protected your PC.” Click More info → Run anyway — this is the standard prompt for newly published apps.Everyday use. Nothing to do — it runs in the background and starts with Windows. Just keep TallyPrime open with your company during the day so posting can happen.Pausing. Click Disconnect any time — posting stops and approved notes simply queue until you reconnect. Nothing in Tally changes while paused.
Outbound-only, encrypted link (like online banking) — your PC is never exposed to the internet.A private identity key per machine — no passwords or tokens to copy-paste.Per-user install, no admin rights — safe on office PCs.Nothing posts until you approve; disconnect or uninstall any time.
Reads your ledgers when needed & posts approved credit notes.Shows a live status (connected / Tally open / posting).Never changes Tally until you approve.Never opens your PC to incoming connections.Never asks for a Tally password, token, or certificate.
Tally app offline — open the Lekha app on your Tally PC; it reconnects in seconds.
Tally not open — open TallyPrime with your company loaded.
Button greyed out — all three lights must be green.
Notes “queued” — reconnect & keep Tally open; the queue clears on its own.
Restarted Tally — notes pause & queue while it's closed; reopen TallyPrime with your company and posting resumes on its own.
“Unlink this device” — fully unbinds it (for switching PCs), unlike Disconnect which only pauses; press Connect Tally on the PC you want to re-link, then queued notes post.
A brief reconnect now and then — normal; the secure link refreshes itself.
Windows Settings → Apps → Lekha AI → Uninstall. It removes the app and its files cleanly.
);
}
function ConnectTally({ store, who }) {
const [status, setStatus] = React.useState(null); // { paired, connected, tally_online, company_loaded, connector_on, ... }
const [avail, setAvail] = React.useState([]); // live connectors on the WSS hub
const [busy, setBusy] = React.useState(false);
const [confirmOpen, setConfirmOpen] = React.useState(false); // disconnect-Tally confirm
const [connectedPopup, setConnectedPopup] = React.useState(null); // "ready to post" popup (company)
const [replica, setReplica] = React.useState(null); // REQ-U1: sync progress + posting-ready
const reload = React.useCallback(async () => {
const [s, a] = await Promise.all([store.connectorStatus(), store.availableConnectors()]);
setAvail(a || []);
if (who && who.business_id) {
api.get(`/api/tally/replica/${who.business_id}/state`).then(setReplica).catch(() => {});
}
// Connector = automatic status: silently claim the single detected connector so
// the user never picks one (binding is not a decision — posting is). A >1 case
// falls back to the picker below.
if (s && !s.paired) {
const free = (a || []).filter((c) => c.online && !c.mine);
if (free.length === 1) {
const r = await store.claimConnector(free[0].connector_id);
if (r) { setStatus(r); return; }
}
}
setStatus(s);
}, [store, who]);
// The connector dials in over WSS; poll so status + the live list stay fresh.
React.useEffect(() => {
let alive = true;
const tick = () => { if (alive) reload(); };
tick();
const id = setInterval(tick, 8000);
return () => { alive = false; clearInterval(id); };
}, [reload]);
const paired = !!(status && status.paired);
const online = !!(status && status.connected); // Lekha ↔ connector link is live
const connectorLinked = paired && online; // connector status light = green
const tallyUp = !!(status && status.tally_online); // TallyPrime actually open
const companyLoaded = !!(status && status.company_loaded); // …with the bound company
const permission = !!(status && status.connector_on); // finance granted posting
const expectedCompany = (status && status.expected_company) || "";
const companies = (status && status.companies) || [];
const company = expectedCompany || companies[0] || "your company";
const queued = (status && status.queued) || 0;
const claimable = avail.filter((c) => c.online && !c.mine);
// green/posting state and whether the button can be clicked to connect
const tallyConnected = connectorLinked && tallyUp && companyLoaded && permission;
const canConnect = connectorLinked && tallyUp && companyLoaded && !permission;
const blockReason = !connectorLinked
? (paired ? "Tally app offline — open the Lekha app on your Tally PC"
: "Waiting for the Lekha app — open it on your Tally PC")
: !tallyUp ? "Open TallyPrime on your Tally PC"
: !companyLoaded ? ("Open " + (expectedCompany || "your company") + " in TallyPrime")
: "";
const connectTally = async () => {
setBusy(true);
try { await store.setConnector(true); await reload(); setConnectedPopup(company); }
finally { setBusy(false); }
};
const disconnectTally = async () => { // revoke permission; connector stays linked
setBusy(true);
try { await store.setConnector(false); await reload(); }
finally { setBusy(false); }
};
const unlinkConnector = async () => { // rare: switching PCs — fully unbind
setBusy(true);
try { await store.unclaimConnector(); await reload(); }
finally { setBusy(false); }
};
const Step = ({ n, title, done, children }) => (
Install the Lekha desktop app on the PC where Tally runs and open it — it connects to Lekha on its own. Then open your company in TallyPrime and press Connect Tally to let Lekha post approved credit notes automatically. Disconnect any time to take control back.
{/* REQ-U1 — onboarding sync visibility: what Lekha has mirrored from this
Tally and whether posting-critical data is ready. Interruptions are
normal (progress is checkpointed) — say so instead of alarming. */}
{connectorLinked && (() => {
// REMASTER: replica state is per object_type (state.objects[]); readiness
// is "every object type is ready", and voucher progress is on the 'voucher' row.
const rows = (replica && replica.objects) || [];
if (!rows.length) return null;
const ready = rows.every((o) => o.replica_ready);
const anyError = rows.some((o) => o.status === "ERROR");
const vch = (rows.find((o) => o.object_type === "voucher") || {}).progress || {};
const vDone = (vch.done || []).length, vTotal = vch.total || 0;
const pct = ready ? 100 : (vTotal ? Math.round(100 * vDone / vTotal) : 8);
return (
{ready ? "Books synced — ready to post" : "Syncing your Tally books…"}
{connectedPopup && overlay(
"Connected ✓",
<>Lekha AI is now connected to {connectedPopup} and will post approved credit notes into Tally automatically. You can disconnect any time to take control back.>,
)}
{confirmOpen && overlay(
"Disconnect Tally?",
<>Lekha will stop posting and hand control back to you. Approved credit notes will queue until you reconnect — nothing inside Tally changes, and the app stays linked.>,
<>
>
)}
A small Windows agent (~5 MB) that runs beside Tally. The same installer works for every business.
New to Lekha? See “What's inside the Lekha app” in the Docs tab for what it is, how it works, and is it safe.
Launch the Lekha app — it connects to Lekha automatically over a secure outbound link (no token, no certificate) and links itself to your business within a few seconds.
{connectorLinked ? : }
{connectorLinked ? "Lekha app connected." : paired ? "Tally app offline." : "Waiting for the Lekha app to connect…"}
{connectorLinked && companies.length > 0 && <>
{" "}Tally companies on this connector:
{companies.map((c, i) =>
{c}
)}
>}
{paired && !online && " Open the Lekha app on your Tally PC and it'll reconnect."}
{paired &&
(only when switching PCs)
}
{!paired && claimable.length > 1 && (
More than one device is online — pick the one running on your Tally machine.
The accounting impact appears once the credit note is composed.
)}
{/* the decision / final status — sticky, centered below the grid, the primary action */}
{finished ? (
{doc.status === "verified_in_tally" ? : }
{doc.status === "verified_in_tally" ? "Posted to Tally" : "Rejected"}
{doc.status === "verified_in_tally" ? "Entry verified — pipeline complete." : (doc.rejectReason || "Sent back to Nakad.")}
) : rejecting ? (
Reason for rejection
) : (
Your decision
{acting ? (
{doc.status === "posting" ? "Posting to Tally…" : "Approved — queued for Tally"}
This posts to {biz.name}'s Tally automatically the moment the Lekha app is online (app running + Tally open) — nothing more to click. It stays queued until a voucher id comes back.
) : (
<>
One click approves and posts to {biz.name}'s Tally — if the Lekha app is online it posts right away, otherwise it queues and posts automatically when the app connects. If not actioned, it auto-approves in 24 hours.
);
}
/* ---- Guide / Docs tab — self-serve, non-technical onboarding ------------- */
const GUIDE_ARTICLES = [
{
title: "What Lekha does for you",
summary: "Turn the debit notes your buyers send into credit notes in your Tally — automatically.",
paras: [
"When a marketplace or buyer (Swiggy Instamart, Zepto, Blinkit, BigBasket, and others) raises a debit note against you, you normally have to read it, work out the GST, and type a matching credit note into Tally by hand.",
"Lekha does that for you. It reads the debit note, prepares the exact credit note with the right ledgers and taxes, and — once you approve it — records it straight into your Tally. No re-typing, no missed entries.",
"Your job is simply to review and approve. Everything else is handled.",
],
},
{
title: "Quick start — connect your Tally (one time)",
summary: "Three steps, about five minutes. You only do this once per computer.",
steps: [
"Install the Lekha desktop app on the PC where Tally runs, and open it — it connects to Lekha on its own.",
"Install and open it on that PC. It links to Lekha on its own — nothing to type, no codes, no certificates.",
"Open your company in TallyPrime and keep it open — Lekha detects it automatically. Nothing else to click.",
],
},
{
title: "The journey of a credit note",
summary: "From the buyer's debit note to a confirmed entry in your books.",
steps: [
"Arrives — Lekha picks up the debit note and prepares the credit note.",
"Review — it appears in your To review list for you to check.",
"Approve — one click; or reject with a note if something's off.",
"Posts to Tally — Lekha writes the voucher into your books automatically.",
"Confirmed — it shows up under Reviewed with its Tally voucher reference.",
],
},
{
title: "Reviewing and approving",
summary: "What you'll see, and how to approve or reject.",
paras: [
"Open any item in To review. You'll see the amount, the customer, the line items, and the exact accounting impact — which ledgers are debited and credited, and the effect on the customer's balance.",
"If it looks right, click Approve & post — Lekha records it in Tally. If something's wrong, click Reject and add a short reason; it goes back without touching your books.",
],
},
{
title: "What happens after you approve",
summary: "It posts to Tally on its own — and waits safely if anything's offline.",
paras: [
"The moment you approve, Lekha writes the credit note into your Tally and then reads it back to confirm it really landed.",
"If your Tally or the Lekha app happens to be closed, the credit note simply waits in a queue — nothing is lost — and posts automatically the next time they're open.",
"You can see every posted entry, with its Tally voucher reference, under Reviewed → Written to Tally.",
],
},
{
title: "If you do nothing — auto-approve",
summary: "Nothing gets stuck. Items auto-approve after 24 hours.",
paras: [
"So a credit note never sits forgotten, it auto-approves 24 hours after it arrives. You can always review and act sooner.",
"If you'd rather review everything manually, your Lekha contact can adjust this for your business.",
],
},
{
title: "Keeping the connection healthy",
summary: "What the three tiles mean, and how to fix an amber one.",
paras: [
"The Lekha app shows its status at the bottom: whether it is connected to Tally and your company is open.",
"All green = ready to post. If Tally shows offline, open TallyPrime with your company loaded. If the App shows offline, open the Lekha app on your Tally PC.",
"To reconnect, open the Lekha app and TallyPrime with your company loaded — it reconnects automatically; your credit notes queue until then and post on their own.",
],
},
{
title: "Seeing everything written to Tally",
summary: "A running summary of every voucher Lekha has posted for you.",
paras: [
"The Reviewed tab opens with a Written to Tally summary: how many vouchers were posted, their total value, and a list with each credit note's Tally voucher reference and the date it was posted.",
"It's your at-a-glance record of what Lekha has done in your books — handy at month-end or for a quick reconciliation against Tally.",
],
},
{
title: "Emails and notifications",
summary: "Choose what lands in your inbox.",
paras: [
"Under Settings you control which emails you receive — a new credit note to review, a confirmation when one posts, or a rejection notice.",
"Review emails also let you approve or reject with a single click, without opening the app.",
],
},
{
title: "Common questions",
summary: "Quick answers to the things people ask most.",
qa: [
["Tally shows “offline” — what do I do?", "Open TallyPrime with your company loaded, and make sure the Lekha app is running on that PC — it reconnects automatically."],
["A credit note didn't post — is it lost?", "No. The most common reason is that the customer isn't in your Tally yet, so Lekha flags it instead of guessing. Add that customer in Tally and it will retry — nothing is ever silently dropped."],
["Do I need to keep my computer on?", "Only when you want entries to post. If the Lekha app and Tally are closed, credit notes wait safely in a queue and post the next time you're online."],
["Can I undo a voucher Lekha posted?", "Yes — it's an ordinary voucher in your Tally now, so you manage it there like any other entry."],
["Will Lekha change anything I didn't approve?", "Never. Lekha only posts what you (or the 24-hour auto-approve) approve, and it always reads the entry back to confirm it's correct."],
],
},
];
function GuideArticle({ a, open, onToggle }) {
return (
{open && (
{(a.paras || []).map((p, i) => (
{p}
))}
{a.steps && (
{a.steps.map((s, i) =>
{s}
)}
)}
{a.qa && (
{a.qa.map(([q, ans], i) => (
{q}
{ans}
))}
)}
)}
);
}
function DocsTab({ who }) {
const [open, setOpen] = React.useState(0); // first article open by default
return (
<>
Docs
Everything you need to set up and run Lekha yourself — in plain language. Start at the top; each section opens with a tap.
New here? Three steps to go live
Connect Tally — install the Lekha desktop app on your Tally PC and open your company in TallyPrime; Lekha connects automatically.
Review — open a credit note in To review and check it.
Approve — it posts into Tally automatically and appears under Reviewed.