You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/docs/09-UX-FLOWS-AND-MENUS.md

378 lines
61 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 09 — Pages, Menus & Flows (UX Blueprint)
This is the screen-level blueprint for all four surfaces — POS Counter (Electron), Back Office (web), Owner App (React Native), and Customer Display — plus the cross-cutting systems they share. It applies the locked decisions everywhere: local-first POS, keyboard-first flows, config-driven content (labels, menus, thresholds, keymaps, and messages are DB rows per 07-DB-AND-CONFIG — nothing below implies a frontend release to change), immutable documents, and edition gating via `plan_feature`. Keystroke convention: a scan = 0 keystrokes (wedge scanner sends code + Enter); every key press = 1; lane budget 40 items/min means any per-item UI interaction beyond zero keystrokes in the common path is a defect.
---
## 1. POS Counter
### 1.1 Billing screen — zones
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ HEADER SiMS · MegaMart T.Nagar · Counter 2 · Ramesh (Cashier) · Shift #14 │
│ Bill: (assigned on close) · Token 07 Tue 08 Jul 2026 6:42 PM │
├──────────────────────────────────────────────┬───────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │ CUSTOMER STRIP (F6) │
│ │ SCAN / SEARCH ▌ (48px, always focused) │ │ 98400 12345 · Lakshmi │
│ └──────────────────────────────────────────┘ │ Khata ₹1,240 dr · 320 pts │
│ LINES GRID (virtualized, 36px rows) ├───────────────────────────────┤
│ # Item Qty MRP Rate Amt │ TOTALS Items 12 · Qty 14.750 │
│ 1 Aashirvaad Atta 5kg 1 285 270 270 │ Gross 1,412.00 · Disc 42.00 │
│ 2 Tomato (loose) 1.250 — 32/kg 40 │ GST (breakup ▾) +61.20 │
│ ▶ last-added row highlighted │ ┌───────────────────────────┐ │
│ │ │ PAYABLE ₹1,431 (64pt) │ │
│ │ └───────────────────────────┘ │
│ QUICK-KEYS ROW (Ctrl+1…Ctrl+0, touch tiles) │ You saved ₹57 vs MRP │
│ [Tomato][Onion][Potato][Bag ₹5] … │ [F12 Cash] [F11 Pay] [F7 Hold]│
├──────────────────────────────────────────────┴───────────────────────────────┤
│ STATUS BAR ● Synced 2m ago · Printer OK · Scale 0.000kg · Held: 2 · │
│ Drawer ₹8,140 · Keymap: Classic · v2.3.1 │
└──────────────────────────────────────────────────────────────────────────────┘
```
| Zone | Notes |
|---|---|
| **Scan/search box** | The sacred line. Focus always returns here after any action, overlay, or error — enforced by a focus sentinel; no dialog, toast, or sync event may steal it. |
| **Lines grid** (~62%) | Columns: #, name (+ regional secondary name, HSN small, batch/expiry chip when tracked), qty, unit, MRP, rate, disc, GST%, amount. Last-added row highlighted, auto-scrolled; ↑/↓ selects for edits. Repeat scans merge into one line (default ON; auto OFF for batch-tracked items). |
| **Totals panel** (~38%) | Mirrors the customer display so both parties read the same numbers. PAYABLE huge (cashier speaks it aloud); GST breakup collapsible — compliance lives on the print. "You saved ₹X vs MRP" is the trust line. |
| **Customer strip** | Optional per bill: phone, name, khata balance chip, loyalty points. Sits by totals because khata/loyalty affect payment. F6 focuses. |
| **Quick-keys row** | 10 configurable tiles (produce PLUs, carry bag), Ctrl+1…Ctrl+0 and touch. Per-counter DB config; hidden if unconfigured (typical Lite). |
| **Status bar** | Sync dot, printer, live scale weight, held-bills badge, drawer cash (role-visible), keymap preset, version. Everything informs, nothing interrupts. |
| **Header** | Store, counter, cashier, shift, clock. Bill number reads "(assigned on close)" — the per-counter GST series number is allocated only at payment commit, so held/abandoned carts never create series gaps. Held carts carry a Token number instead. |
Touch: totals buttons and quick-key tiles ≥ 48 px; touch never adds steps to the keyboard flow.
### 1.2 The smart scan box — four grammars, no modes
| Input | Behavior |
|---|---|
| Digits + Enter (or scan) | Barcode/item-code lookup → line added. Weighing-scale EAN-13 (`20``29` prefix) parsed per counter profile (PLU digits + embedded weight/price, configurable divisor). |
| Letters | Live fuzzy/phonetic search < 150 ms (see §5.1); ↑/↓ + Enter picks; Esc clears. |
| `n*` prefix then scan/code | Quantity multiplier: `3*` + scan = qty 3 on one line. |
| Enter on empty box | No-op (double-Enter guard). |
### 1.3 Keymap — "Next" preset (DB profile; per preset / counter / user override)
| Key | Action | Gate |
|---|---|---|
| Enter / Esc | Confirm · Cancel/back one level (Esc never destroys lines) | |
| PgUp PgDn | Line/list navigation | |
| `*` | Qty-multiplier prefix | |
| F1 | Keymap cheat-sheet overlay (printable card) | |
| F2 / F3 | Edit qty / price override on selected line | F3: supervisor beyond ±X% band |
| F4 / F5 | Line / bill discount (`5` = ₹, `5%` = percent) | Supervisor above X% |
| F6 | Customer strip: attach by phone | |
| F7 / F8 | Hold bill / resume tray | |
| F9 | Returns/exchange mode | Setting: open vs supervisor |
| F10 | Bill history & reprint (day-scoped) | |
| F11 | Payment overlay (all tenders, split) | |
| **F12 / Numpad +** | **Cash close — the one key** | |
| Del / Ctrl+Del | Remove line (inline confirm) / cancel cart (reason) | Supervisor per setting |
| Ctrl+P | Reprint last bill | Copies > N → supervisor |
| Ctrl+F / Ctrl+M | Open drawer no-sale / cash in-out | Supervisor (drawer always audit-logged; paid-out above ₹X) |
| Ctrl+1…Ctrl+0 | Quick-key tiles | — |
| Ctrl+L / Alt+S / Ctrl+, | Lock counter · salesman tag (Standard+) · settings | Settings: supervisor/manager |
Numpad-only right hand is deliberate: scanner in left hand, right hand never leaves the numpad (digits, `*`, `+` = cash close, Enter). A second shipped preset, **Classic keys**, mirrors SiMS Classic's Oracle Forms bindings key-for-key — migrating cashiers keep 100% muscle memory on day one. Rebinds are per-action with conflict detection; changes sync as counter-scope settings.
### 1.4 Billing flows (keystrokes excl. scans and free-text typing)
**Normal cash sale (the 90% path):** scan items (0) → state total → **F12**: series number assigned, bill committed (immutable document + stock movements + outbox row in one SQLite transaction), drawer kicks, print spools async, customer display thanks, scan box refocused. **1 keystroke.** With change: type tendered digits first, then F12 → change flashes 4 s on both displays. **5 keystrokes.**
**Weighed produce:** (A) integrated scale, Pro — quick-key tile + Enter on live stable weight = **2**; (B) manual weight, all editions day 1 — tile + `.750` Enter ≈ **6**; (C) label-scale barcode (`2x` EAN-13) — **0**, the correct answer for high-volume supermarket produce.
**Edits:** re-scan merges qty (0) · `3*`+scan (2) · F2 qty (3) · F3 price (5, +PIN out of band) · F4/F5 discount (45, +PIN over threshold) · Del remove (23). All overrides write before/after values to the append-only audit log with cashier + approver identity.
**Unknown barcode (errors never trap):** no match → non-blocking inline panel under the scan box: "Unknown code… [Enter] Quick-add · [Esc] Skip" — further scans queue behind it. Enter → 3-field micro-form (Name, Price/MRP, GST picklist defaulting to store slab) → item saved as `draft`, line added, flagged for back-office completion. **3 keystrokes + typing**; skip = 1.
**Customer attach:** F6 → 10-digit phone (live match after 4 digits) → Enter. **≤12.** No match → Enter again creates the customer phone-only. Attached customer ⇒ WhatsApp e-bill offered at close (auto/ask/off setting).
**Hold/resume:** F7 parks the cart under its Token instantly (**1**); held carts are journaled — they survive restarts and power cuts, and have no invoice number. F8 → tray → Enter (**23**). Day-end forces the tray empty.
**Payment overlay (F11)** — for everything that isn't exact cash. Payable on top; tender legs left; amount field prefilled with remainder; method hotkeys **1** Cash **2** UPI **3** Card **4** Khata **5** Other. Grammar: *(optional amount) + method key* adds a leg; Enter on zero remainder commits + prints; Esc abandons, cart untouched.
| Tender | Flow | Keys |
|---|---|---|
| Cash w/ change via overlay | F11 → `2000``1` → Enter | 7 |
| UPI dynamic QR (Standard+, online) | F11 → `2` → amount-embedded QR on customer display → webhook auto-confirms → auto-close | **2** |
| UPI offline | Static VPA QR; cashier sights success screen → Enter (payment flagged `manually_confirmed`) | 3 |
| Card | F11 → `3` → amount pushed to terminal → Enter on approval | 3 |
| Khata | F11 → `4` → requires attached customer; shows new balance vs credit limit → Enter; over limit → supervisor PIN | 3 (+PIN) |
| Split ₹500 cash + UPI | F11 → `500``1``2` → QR remainder | 6 |
Bill-close latency budget: commit + print spool < 1 s; the lane never waits on the printer.
**Cancel/void (bills are immutable — a "void" is a new document):** Ctrl+Del cancels an open cart with reason picklist (+supervisor PIN if it has lines; abandoned carts logged with line snapshot for shrink analytics). Voiding a closed bill (same day, pre-filing): F10 find `V` reason supervisor PIN a **cancellation document** is appended referencing the bill; the bill stays in the series marked CANCELLED (series stays consecutive GST-safe), stock reversed by counter-movements, payments reversed. Registered IRN IRN-cancel queued within the 24 h window; outside it the flow refuses and routes to Returns (credit note).
**Returns/exchange (F9):** find the bill by scanning the receipt QR (0), bill no, or phone lookup original lines listed with returnable qty netted Space toggles, F2 partial grid enters RETURN mode (red accent, watermark, negative lines; batch items return to their original batch). Exchange = keep scanning positive lines in the same document. Settle net via F11 (cash refund / khata credit); committed as a **credit note** in its own per-counter series referencing the original. No-receipt return: supervisor PIN, current-rate floor, audit-flagged. Simple 1-line refund **6 keystrokes**.
**Reprint:** Ctrl+P = last bill (**1**). Older: F10 find `P`; `W` resends the WhatsApp e-bill. Beyond N copies supervisor; every reprint audit-logged and printed "DUPLICATE".
### 1.5 Offline-degraded state — almost nothing changes
Offline is the architecture, not a mode. Scanning, search, lines, totals, print, drawer, cash/card/khata/hold/returns/reprint: identical. What differs: status dot goes grey ("Offline 34 queued", count ticks that's the whole ceremony); UPI falls back to static VPA QR + manual confirm; WhatsApp e-bills queue; e-Invoice bills print with "IRN pending" and register on reconnect; customer/khata lookups use the local replica (limit checked against last-synced balance, flagged; multi-counter stores keep a live LAN view via Store Hub); offline loyalty redemption capped per setting. No banners, no modals, no focus theft. Reconnect drains the outbox silently.
### 1.6 Shift & day flows
Lite skips the ceremony login straight to billing, day-end report auto-generates. Standard+:
- **Login:** cashier tiles + 46 digit PIN, < 5 s cold start; 5 wrong PINs 60 s lockout + audit event. Ctrl+L locks mid-day (held carts and open bill preserved).
- **Shift open:** denomination grid (₹500/200/100/50/20/10/coins, Tab advances, live total 25 keys); variance vs previous shift's declared float; over tolerance supervisor + reason. Quick-open setting for small stores: single declared amount, 6 keys. Emits immutable `shift_opened`.
- **Cash in/out (Ctrl+M):** direction amount reason picklist (float top-up, safe drop, petty expense, owner drawing) drawer kicks, voucher slip prints. Above X supervisor. 610 keys.
- **Shift close:** **blind count** by default (expected hidden until submitted) variance with reason + supervisor PIN outside tolerance held-cart tray must be empty declare leave-behind float X-report prints (bills, ABV, tenders, discounts, voids, returns, cash movements, variance, names). Synced; owner app gets the summary.
- **Day-end:** one Enter Z-report consolidating all counters (via Store Hub): GST summary by rate, tenders vs counted cash, top items, voids/returns, khata sales, pending-sync count. Prints + queues to owner digest; triggers the cloud backup snapshot. Fully offline-capable (report syncs later, with a pending-sync warning line if the outbox isn't empty).
### 1.7 Supervisor overlay — the one sanctioned interruption (always user-initiated)
Gated actions (all thresholds are DB settings per tenant/store): price override beyond band, discounts above X%, cancel-with-lines/void, no-receipt return, khata over limit, drawer no-sale, reprint > N, paid-out > ₹X, shift variance, offline loyalty above cap, tax-exempt sale, settings/keymap entry, training-mode toggle (manager).
Flow: compact overlay atop the visible bill — action description, before → after values, reason picklist (list is DB config), PIN field. Supervisor authorizes by user-code + PIN, **badge barcode/card swipe** (0 keys — the fast path), or (Phase 3, online) **remote approve** pushed to the owner app — overlay shows "awaiting approval", cashier can Esc and hold the bill so the queue keeps moving. Approve → executes; audit row (action, cashier, approver, counter, bill ref, before/after, timestamp) syncs to the back-office audit browser and owner feed. Deny/30 s timeout → clean cancel, focus → scan box. 3 wrong PINs → 60 s counter lockout + owner alert. Supervisor authority is itself a role permission from the DB matrix.
### 1.8 POS settings (Ctrl+, — supervisor gated; never required mid-bill)
Counter-scope DB settings (port/device bindings stay local-only): **Printers** (profiles, per-document routing, test print/drawer kick, template preview — templates edited in back office); **Scale** (port + protocol presets shipped as data, live readout, label-barcode scheme editor with test-parse); **Customer display** (monitor, orientation, playlist preview, language); **Sync status** (pending ops by type, per-item retry, force-sync, Store Hub state, one-click "send diagnostics" bundle — no PII); **Keymap** (preset picker Next/Classic/Custom, rebind grid, quick-key tile editor, print cheat card); **Hardware wizard** (first-run guided detection, re-runnable); **About/License** (edition, locked features as discoverable upsells, update channel).
### 1.9 Customer display (Pro; second monitor, same Electron app, local)
| State | Content |
|---|---|
| Idle/promo | DB-driven playlist: offers, house ads, WhatsApp e-bill opt-in card. |
| Billing | Last 56 lines large (customer's script, MRP struck-through when rate < MRP kills above-MRP disputes), running total huge, "You saved X", updates inside the 150 ms scan budget. |
| QR payment | Full-screen dynamic QR with amount ("Scan to pay 1,431") green PAID on webhook; offline: static VPA QR + amount. |
| Change due / Thank-you | "Change 569" 4 s · total, saved, loyalty earned, "bill sent on WhatsApp", catalog message per language, 5 s. |
| Return mode | Refund lines + net, red accent transparency during returns. |
The display never shows sync/printer/internal errors.
### 1.10 Keystroke scoreboard (published, telemetry-verified)
Cash exact **1** · cash w/ change 5 · UPI 2 (offline 3) · card 3 · khata 3 · split 6 · scale produce 2 (manual ~6, label 0) · qty 0/2/3 · price/discount 45 · quick-add 3+typing · customer attach 12 · hold 1 / resume 23 · cancel cart 23+PIN · void 510+PIN · 1-line return ~6 · reprint 1 · login 58 · shift open/close ~2535 · day-end 1. Telemetry: per-bill timing, scan-to-line histogram, keystrokes-per-bill, overlay dwell the "fastest counter in India" number we publish.
---
## 2. Back-Office IA & Menu Tree
### 2.1 Design stance (defaults, not options)
Table-first workbench: every module opens on a virtualized, keyboard-navigable grid; details are drawers by default, full pages on demand. **Two levels of navigation, never three** Module Page; anything deeper is a tab or filter. **Hide by role, lock by edition**: a user never sees a page their role can't use; owners/managers see edition-locked pages as discoverable upsells. The whole menu is data: `menu_item(code, module_code, route, icon, sort, min_edition, label_key)` + `menu_item_role` support reorders/relabels/regates with zero frontend release.
### 2.2 Menu tree — eleven modules, ordered by daily usage `[L/S/P/E]` = min edition
- **🏠 Dashboard** Overview `L` (today's sales/payments live; alert cards deep-linking to the fixing page) · Approvals & Tasks `S` (discount/void approvals, adjustment approvals, POS quick-adds needing completion, failed IRNs).
- **🧾 Sales** Bills `L` · Returns & Credit Notes `L` · Shifts & Day-End `L` (recon features `S`).
- **📥 Purchases** Purchase Inbox `P` (flagship queue, unread badge, first item) · Purchase Invoices `L` · Orders & GRNs `P` · Purchase Returns / Debit Notes `L` · Suppliers `L` · GSTR-2B Reconciliation `P`. (Label printing stays under Catalog purchases hand off to it.)
- **📦 Inventory** Stock on Hand `L` · Stock Movements `L` (the append-only ledger behind every number stock is derived, never typed) · Adjustments `L` (reason-coded, approval workflow) · Stock Takes `P` · Transfers `E` (in-transit state, e-Way hook) · Expiry & Ageing `P` (action lists, not charts) · Reorder `S` (velocity suggestions one-click draft PO).
- **🏷️ Catalog** Items `L` · Categories & Brands `L` · Price Lists `S` (effective-dated, store overrides) · Schemes & Offers `S` · Label Printing `S`.
- **👥 Customers** Customers `L` (phone-keyed) · Khata & Collections `L` (WhatsApp reminders + UPI collect links, single or bulk) · Loyalty `P` · Campaigns `P`.
- **📒 Accounting** Ledgers `L` · Vouchers `S` · Cash & Bank Books `S` · Day Book `P` · Financial Statements `P` · Tally Export `S` · Year Close & Locks `S`.
- **🏛️ GST & Compliance** GST Returns `S` (GSTR-1/3B prep, JSON export, filing status) · e-Invoice Queue `P` · e-Way Bills `P` · HSN & Tax Rates `L` (effective-dated the GST-2.0-proof page).
- **📊 Reports** Sales `L` · Stock & Valuation `L` · Party Outstanding `L` · GST Registers `L` (the CA hand-off pack) · Staff & Counters `S` · Analytics `P` · Scheduled Reports `S`.
- **🏢 Head Office** `E` Store Comparison · Central Pricing · Central Purchasing (Ph 4) · Consolidated GST (multi-GSTIN) · Outlet P&L (Ph 4).
- **⚙️ Admin** Users & Roles `S` · Stores & Counters `L` (per-counter series, print profiles, Store Hub role) · Settings `L` (tenantstorecounteruser hierarchy) · Templates `L` (visual editor + preview) · Integrations `L` (WhatsApp, UPI, GSP, email-in address) · Subscription & Plan `L` · Audit Log `S` · Sync & Devices `L` · Backups `L` · Data Import `L` (Excel/CSV + Classic migration tool with verification report) · API Access `E`.
**Not in the tenant menu:** plan/price-book editing, the global message catalog, staged rollouts, and cross-tenant telemetry live in the internal **SiMS Control Panel** same engine, vendor tenant scope. Never expose vendor-level config in customer IA.
### 2.3 Navigation model
- **Left sidebar** (240 px, collapsible to icon rail): accordion modules, active one auto-expands. **Top bar**: scope switcher (store selector "All stores" for Enterprise owners plus a date-range chip every page inherits), Ctrl+K, notifications bell (fed by Approvals & Tasks), sync dot, user menu (per-user language). **Breadcrumb** `Module / Page / Record`; every record has a stable, copyable URL.
- **Ctrl+K command palette** the second navigation system. Four ranked groups: Records (bill no, item, party, GSTIN same fuzzy/phonetic engine as POS), Pages, Actions ("Export GSTR-1 June", "Send khata reminders"), Recents (last 15 records per user, server-side, cross-device). Prefix operators: `#` bill/doc no · `@` party · `>` action · `?` help/settings. The palette respects role visibility and edition gating locked actions show a lock glyph and open the upsell sheet; role-hidden items never appear.
- **Keyboard:** `g` + module key (`g s` Sales, `g p` Purchases, `g i` Inventory, `g c` Catalog, `g a` Accounting, `g g` GST, `g r` Reports, `g x` Admin); `/` focuses the grid filter; `n` = page's primary create; `e` = export. Keymaps are DB data, same mechanism as POS.
### 2.4 Visibility & gating rules
| Rule | Behavior |
|---|---|
| Role lacks access | Page absent from sidebar, palette, routes (deep link 403 with catalog message). |
| Read-only role | Page visible; create/edit/post disabled with tooltip; export still works. |
| Below edition, viewer owner/manager | Lock badge; click **upsell sheet**: 30-sec demo, one-paragraph pitch, "Included in Pro Upgrade" upgrading in place. |
| Below edition, viewer staff | Hidden upsells pitch buyers, not cashiers. |
| Cashier | Zero back-office pages; login rejected with "use the counter app". Cashiers exist only in POS. |
| Module empties after filtering | Module header disappears too except Purchases, GST, and Reports keep their locked flagship pages (Purchase Inbox, e-Invoice, Analytics) visible to owners: the top three upgrade drivers. |
**Default role → module map** (the Admin permission matrix refines to page level with None/Read/Full cells; Auditor is read-everything by definition; Owner is Full at their edition): Supervisor Dashboard, Sales, Inventory (no Transfers), Customers, Catalog (read), sales/stock Reports. Purchaser Purchases, Catalog, Inventory (Stock/Reorder/Expiry), stock/purchase Reports. Accountant Accounting, GST, Sales/Purchases (read), Khata, GST/party Reports. Net effect: the Lite solo owner sees a 7-module ~20-page app; a Pro accountant sees a compliance cockpit; the Enterprise owner sees all eleven same engine, same routes, different rows in `menu_item_role` + `plan_feature`.
### 2.5 Anatomy of the pages that matter most
- **Catalog Items** (biggest grid): virtualized columns (name, code, barcodes, category, HSN, GST, MRP, cost, margin %, stock, status incl. **incomplete** POS quick-adds land here); saved-filter tabs; side-peek preview on row focus; bulk actions (effective-dated category/GST/price edits, labels, merge duplicates); empty state = *Import from Excel / Migrate from Classic / Add first item* replaced by the "N counter quick-adds need details" worklist when applicable.
- **Item editor**: sections Identity (multi-language names, HSN with rate auto-fill), Barcodes (many, incl. `2x` PLU config on Pro), Units & Packs conversion matrix, Pricing (per-price-list effective-dated rows), Batch & Expiry toggle (Pro), Reorder. Right rail: live stock by store, last 5 purchases/sales, audit drawer. Only name + GST rate hard-required everything else completable later. Effective-dated fields edit via a "Change from…" timeline dialog, never in-place.
- **Sales Bills**: grid with payment-mode chips, e-bill and IRN status; side-peek renders the bill exactly as printed (same template engine) with the banner "Bills are permanent records corrections create a credit note." Actions: Reprint, Send e-Bill, Create Return, Generate e-Way. Per-counter sync-lag indicator so "no data" and "not synced yet" are never confused.
- **Inventory Stock on Hand**: group-by item/category/store/batch; valuation (FIFO/weighted avg per tenant setting); row expansion shows the last 30 movements inline "why is this number what it is" is a first-class answer. Filters include negative!, zero, below-reorder, dead > 90 days, expiry windows.
- **GST → GST Returns**: period board Draft → Reviewed → Exported → Filed (ARN stored); **exceptions worklist first** (missing GSTIN, rate mismatches, missing HSN, unregistered IRNs) — the return can't reach Reviewed with open exceptions (owner-PIN override, audit-logged). `Prepare` locks a snapshot; fixes deep-link and re-snapshot deltas. Cross-link to GSTR-2B Recon for the inward side.
- **Admin → Users & Roles**: Users tab (surfaces allowed: POS/back office/owner app) + Roles tab (permission matrix + sensitive-action gates: max discount %, void rights, override band, adjustment threshold, PIN policy). Deactivate, never delete; at least one owner must remain; matrix edits show "affects N users" and land in the audit log.
### 2.6 Cross-page conventions (the engine's grammar)
| Convention | Behavior |
|---|---|
| List → detail | `↵` = side-peek drawer; `Shift+↵` = full page with own URL; browser back restores grid scroll/filter state. |
| Saved filters | Named views as tabs above the grid; personal by default, shareable tenant-wide; support can seed per-vertical presets as DB rows. |
| Export everywhere | `e` → Excel/CSV/PDF honoring filters/columns/sort; > 10k rows async with bell notification; exports audit-logged. |
| Audit drawer | Every record: chronological who/what/when with field-level before → after diffs from `audit_log`; same component everywhere. |
| Effective-dated edits | Timeline dialog: past (grayed, immutable) → current → "new value from [date]"; scheduled changes show as a chip; deleting a scheduled change is allowed, rewriting history is not. |
| Immutable documents | Posted docs render read-only with "Permanent record — correct via Credit Note / Debit Note / Reversal"; the correction pre-fills a linked document; both cross-link forever. |
| Import on every master | Typed template → upload → staged preview with row-level errors → partial commit; error rows re-downloadable. Same component serves Classic-migration verification. |
| Empty states teach | One sentence (from `message_catalog`, localized) + primary CTA + import path. Never a blank table. |
| Sync honesty | Store-originated data shows a freshness chip ("Store 2: synced 2 min ago"); stale > 15 min turns amber and links to Sync & Devices. |
| Money & scope | Tabular figures, Indian grouping from the one shared formatter; pack context ("2 box + 3 pc"); top-bar store + date scope follows the user across pages. |
---
## 3. Purchases & Purchase Inbox
All three entry paths — manual, PO/GRN, Inbox — converge on **one posting pipeline** producing the same immutable `purchase_invoice` document.
### 3.1 Placement — POS "Back Room" mode
The Electron POS gets a supervisor-gated **Back Room mode** (separate from the sacred billing screen) running the *same React screens* as the web back office, but against local SQLite, syncing up. Rationale: Lite shops have one machine and shaky internet; goods receiving happens where the scanner and label printer are; purchase documents flow up cleanly and can never conflict. Zero new frontend surface.
| Function | POS Back Room (offline) | Back office web | Owner app |
|---|---|---|---|
| Manual entry, GRN, purchase returns, label printing | ✔ default home (scanner + label printer here) | ✔ identical screens (label queue mgmt only) | — |
| PO create/approve | — | ✔ | approve/deny |
| **Purchase Inbox review** | — (AI + raw docs live in cloud) | ✔ only | badge + camera capture |
| Suppliers | read-only ledger lookup | ✔ full | outstanding summary |
| GSTR-2B recon | — | ✔ only | — |
Inbox postings sync the document *down* to the assigned store, which applies stock-in movements locally — stock stays derived from local events.
### 3.2 Manual purchase entry — the speed flow (MVP, Lite+)
Creed mirrors the counter: nothing interrupts line entry; all prompts (price changes, labels, mismatches) batch to post time; focus returns to the scan box. Layout: header strip (supplier · invoice no · date · Local/Interstate auto from GSTIN state · terms); lines grid `Item | Batch | Expiry | Qty | Free | Unit | Cost | Disc% | Tax% | MRP | Margin% | Sell | Amount` — which columns the cursor **stops in** is a per-store "column profile" setting (a kirana stops at Qty+Cost; a pharmacy adds Batch/Expiry); right rail item card (photo, stock, **last 3 costs from this supplier with dates**, live margin %); footer tax breakup + **"Bill total (as printed)"** field — the single most effective error catcher in purchase entry.
Flow: `Ctrl+P` new → supplier by 3-char fuzzy match → invoice no (inline duplicate check on supplier + no + FY) → date defaults today → scan (line pre-fills last cost, HSN tax, current MRP) → qty → cost (deviation beyond a % setting shows an inline chip `↑ ₹2.10 vs 04-Jun`, never a dialog) → Enter blows through skipped columns; new MRP silently queues a price decision → `Ctrl+↵` close → **totals-check overlay**: type the printed grand total; delta beyond tolerance (default ₹0.99) shows per-line hints and an inline "other charges" line → Post → **one consolidated price-change dialog** (suggested sell per the item's pricing rule, checkboxes default ON, audit-logged) → label prompt ("Print labels for 5 new/changed items?", quantities default to received qty, jobs to the Label Print Queue).
**Budget: ~5 keys/line, ~24 keys overhead → a 12-line bill in ≈ 85 keystrokes, under 2.5 minutes.** Publish this like scan-to-line < 150 ms. Unknown barcode inline quick-create flagged `needs_completion`, exactly like POS quick-add. Unit conversion from the pack master (CASE(24) PCS), cost per stocking unit live. Below-floor margin renders amber, never blocks. Posting = one local transaction: immutable document, batch-wise stock-in movements, supplier ledger credit, price-change audit entries, label jobs. Corrections are debit notes or reversal + re-entry never edits.
### 3.3 PO → GRN → Invoice (Pro, Phase 3)
Direct entry stays the default; PO mode is a store setting (`purchases.mode = direct | po_optional | po_required`). **PO**: `Draft → Approved (threshold-gated; owner-app approve/deny) → Sent (PDF via email/WhatsApp from template) → Partially Received → Received → Closed | Short-closed | Cancelled`, lines carrying ordered/received/invoiced qtys; reorder suggestions can prefill from sales velocity. **GRN** (POS Back Room, scanner-first): pick PO lines prefill remaining qty scan to verify (scan-vs-typed mismatch highlighted) over-receipt beyond tolerance % supervisor PIN batch/expiry per flagged line, damaged-on-arrival to wastage post: **stock-in immediately** (goods are sellable the moment they hit the shelf never make the counter wait for accounts) + "GRN pending invoice" accrual at PO cost; the label prompt here is the natural supermarket labeling moment. **Invoice** matches one or more GRNs (or one partially) and does only the financial part ledger credit, cost true-up via weighted-avg adjustment movement, tax capture; no double stock-in. Inbox drafts auto-propose open GRNs for the supplier; variances beyond tolerance route to approval.
### 3.4 Purchase Inbox (Pro, Phase 3 — the flagship)
North star: ** 70% of purchase lines via Inbox** (Phase-3 exit 50%). The pitch: *forward the bill, confirm, done.*
**Intake:** auto-provisioned email address `bills-{tenant}@in.sims.app` (+`+{store}` aliases for routing; printed on our POs so suppliers self-onboard; sender learned per supplier) · drag-drop/bulk upload with auto-split of stacked-scan PDFs · owner-app camera (fast-follow) · WhatsApp forward (fast-follow) · **e-Invoice JSON/QR parsed deterministically** no AI, 100% confidence; scanning the IRN QR with the Back Room scanner also creates a doc.
**Pipeline:** SHA-256 dedupe object storage (Mumbai) classify (invoice / CDN / statement / junk non-invoices to an "Other" tab) parse (deterministic for e-invoice JSON and text-layer PDFs; Claude vision for photos) yielding header + lines, **each field with confidence + source bounding box** supplier match (GSTIN exact auto; fuzzy + learned sender suggested) item match in three tiers: **A** alias-memory hit via `supplier_item_alias` (mapped once, remembered forever, green) · **B** barcode or HSN+fuzzy suggestion (amber, one-key confirm) · **C** unmatched (red) validations (duplicate by supplier+no+FY plus fuzzy same-amount-same-week, totals recompute, line tax vs our dated rate, CGST/SGST vs IGST vs state, MRP/cost jumps) state `Ready` (all green) or `Needs Review`. `inbox.autopost_trusted_suppliers` (default off) auto-posts clean docs from whitelisted suppliers the endgame for FMCG distributors. States are an append-only log: `Received → Parsing → Needs Review | Ready → Posted | Discarded | Duplicate`.
**Review screen (the demo moment):** three zones queue rail (`J/K` next/prev, source badge, age, confidence %), center source viewer (zoom/rotate/thumbnails), right draft form (**the same grid component as manual entry**). Clicking any field highlights its source region in the PDF and vice versa trust is built by making verification effortless. Confidence tinting: green 95 auto, amber 7094 (`↵` accepts, `Tab` edits), red < 70. Header verdict math: `Printed ₹12,480.50 · Computed ₹12,480.50 ✓`. Tax mismatch banner offers three actions: *use bill rate for this purchase* (ITC follows the bill; default), *update item master* (dated edit, never silent), *flag to accountant*. Red-line mapping keeps the supplier's raw text visible with type-ahead or pre-filled Create-item; differing units get a one-time "CTN = how many PCS? [24]" stored on the alias. Microcopy under every mapping: *"Will be remembered for Vikram Distributors."* That sentence is the product promise. `Ctrl+↵` posts syncs down store applies movements; price-change and label prompts fire on the store machine. Target: a 30-line bill confirmed in under 60 seconds; no bulk-post by design every posting is a human confirmation. Header stat: "This month: 84% of purchase lines via Inbox."
**Exceptions (never a dead end):** unknown supplier GSTIN public lookup pre-fills create, or link-to-existing fuzzy list; unknown items per-line map-or-create (bulk "create all N" gated to purchaser role); exact duplicates can never post, fuzzy ones show side-by-side compare with reason-coded override; total mismatch ranked causes (add-charges one-click, tax-inclusive basis toggle, jump-to-worst-cell), post blocked until resolved or supervisor-overridden; unreadable scans request re-upload (polite catalog template) or manual entry pre-filled with whatever parsed; multi-invoice files auto-split; statements file under Other and feed ledger reconciliation later.
### 3.5 Suppliers
List: name, GSTIN, outstanding, overdue, last purchase, MTD volume. Detail tabs: **Overview** (ageing buckets, terms vs actual days-to-pay, 12-month trend, top items) `L/1` · **Ledger** (running balance, drill into any document, Record-payment with oldest-first allocation, WhatsApp statement) `L/1` · **Price history** (item × date cost grid, trend flags the data behind the last-cost chips) `S/2` · **Pending POs/GRNs** (accrual exposure totaled, one-click remind) `P/3` · **Items & aliases** (the matching memory, editable, CSV-seedable for big distributors) `P/3` · **Documents** (all Inbox docs) `P/3` · **Settings** (GSTIN, terms/limit, tax basis, recognized senders, trusted-auto-post, bank details) `L/1`. Back Room gets read-only ledger lookup the supplier's rep at the back door gets his balance offline.
### 3.6 GSTR-2B reconciliation (Pro, Phase 3)
Pick period pull via GSP (or upload portal JSON fallback always available) summary tiles (*ITC per 2B · per books · Matched % · At-risk ₹*) work buckets top-down; period locks when 3B files. Buckets: **Exact** (auto, collapsed) · **Fuzzy** (side-by-side, `↵` confirms confirmations train the per-supplier invoice-no normalizer) · **Value mismatch** (accept-with-note / raise DN-CN / flag supplier) · **In 2B, not in books** the killer feature: **"Create draft in Purchase Inbox"** from the 2B row (header pre-filled, lines pending until the paper bill surfaces); nothing missed stays missed · **In books, not in 2B** ITC-at-risk list with WhatsApp/email nudge to the supplier, ITC-hold flag surfacing in 3B prep, auto-recheck next period. All actions audit-logged; recon state append-only per period.
### 3.7 Purchase returns / debit notes (MVP, Lite+)
Immutable DNs in their own per-store series (`ST1-DN-00042`), referencing the original where linked. Flow: pick supplier pick original invoice (scan any item to filter) or unlinked mode return qty per line with **mandatory DB-configured reason codes** (damaged/expired/short-supplied/rate difference/scheme not passed/quality), batch defaulting to received batch **rate-difference-only type**: amounts without quantities no stock movement, pure ledger + GST document tax auto-reverses at the **original invoice's rates** (rates are dated config; the original document is the truth) post (above-X supervisor PIN): stock-out movements, ledger debit print/PDF/WhatsApp travels with the goods; e-Way queue picks it up over threshold. Settlement nets against the next payment allocation or a replacement GRN referencing the DN.
### 3.8 Config & metrics
Everything tunable is `setting` rows (`purchases.mode/column_profile/cost_warn_pct/margin_floor_pct/margin_basis/total_tolerance/over_receipt_pct`, `inbox.autopost_trusted_suppliers`, `dn.approval_threshold`, `gstr2b.amount_tolerance`) plus `supplier_item_alias`, `inbox_document/extraction`, `price_change_event`, `label_job` tables. Instrument from day one: keys/seconds per manual line (≤ 5 / 8 s), % lines via Inbox, % docs auto-Ready, median review time (< 60 s), alias hit rate (→ 95% per supplier by the third bill), duplicate and total-mismatch catches, 2B matched-% and ITC-at-risk per tenant.
---
## 4. Owner App & Onboarding
### 4.1 Owner app (React Native, Android-first; Phase 3)
**Stance:** online-first, cache-always every screen renders instantly from a local SQLite snapshot, then refreshes; a permanent freshness ribbon ("As of 12:41 PM · Store synced 3 min ago") because "live" is really "as-synced"; never a spinner over a cached screen. **One write action: approve/deny** everything else is read; new alert types, digest sections, and report tiles are DB config, not app releases. Language per user setting, all text from `message_catalog` (Hindi + English at launch). Auth: phone + OTP, then a 4-digit app PIN (doubles as approval PIN) with biometric; max 3 devices, revocable from Admin. On **Lite**, only Today tiles and the Day-End Digest render; Alerts/Approvals/Reports show as locked cards with one-tap upgrade.
**Pages** (four bottom tabs + header store switcher/profile): **Today** net sales vs same weekday last week, bills count + bills/hour sparkline, payments donut (Cash/UPI/Card/Khata), ABV, returns, khata given, per-counter open/closed status, sync freshness chips, Day-2 checklist card, top 3 alerts; 60 s auto-refresh. **Store switcher** sheet with per-store sales + freshness dots (green 5 min / amber 30 / red > 30 during open hours), "All stores" rollup first; single-store tenants never see it. **Alerts feed** — 30 days cached, day-grouped, filter chips (Stock/Expiry/Khata/Sync/Discounts/System); every alert deep-links; long-press mutes the type (user-scope setting); dismiss state syncs later. **Approvals** — pending cards (type, store+counter, cashier, bill detail, **live countdown to POS timeout**, Approve/Deny behind PIN) + 7-day history showing who decided; **approve/deny never queue offline** — a stale approval is worse than none; offline shows "counter will use supervisor PIN". **Reports Lite** — deliberately five, no builder: sales trend, top-20 items, category performance, khata outstanding (tap → server-side WhatsApp reminder), month-to-date GST snapshot; tile definitions are DB rows, so a sixth report is config. **Day-End Digest** — rendered digest per store + date, last 14 cached (the guaranteed-offline artifact), back-swipe browses days. **Settings** — language, per-event Push/Digest/Off, quiet hours, PIN, devices, plan (read + upgrade CTA), support routed to the dealer first via tenant config.
**Flow — remote approval (the one write):** the invariant rules it — billing never waits on network, so remote approval is a fast path *layered on* the always-available supervisor-PIN gate, never a replacement. Threshold trips → POS overlay offers Supervisor PIN (always) and "Ask owner" (only when online with a live owner push token) → `approval_request` outbox doc → high-priority FCM, body resolved server-side per device language → owner taps, PIN, decides → decision pushes down. Round-trip target < 10 s; POS timeout 45 s (counter setting); on timeout/offline/deny the overlay falls back to PIN or cancel, and the cashier can hold the bill and serve the next customer. Late decisions are discarded and logged. Every override however resolved still emits a post-facto owner alert: remote approval is convenience; the audit trail is the guarantee. Offline owner (train, basement): every tab renders cached with a grey ribbon, zero blank screens; reconnect refreshes silently.
### 4.2 Notification strategy
**The owner's phone is sacred**: default posture is digest; events earn a push; one coalesced push beats twelve. Three priority classes assigned per event in a `notification_rule` table (event class, coalesce window, quiet-hours override, channel team-editable, no release): **P0** interrupt now (bypasses quiet hours) · **P1** push, batched, respects quiet hours (default 22:0007:00) · **P2** digest/feed only. All texts resolve server-side from `message_catalog` keyed `(code, channel, lang)` push is one line, WhatsApp is a structured template. Channels: FCM primary; WhatsApp for the day-end digest to app-less owners; SMS only for license-expiry final notice; no email. Per-user overrides to Push/Digest/Off except P0 sync-down and license expiry, reducible only to Digest.
Defaults: approval requests **P0** (uncoalesced) · store sync-silent > 30 min during open hours **P0** (one per incident + recovery push) · license expiry **P0** T-7/3/1 (+SMS at T-1 if push undelivered) · day-end digest **P1** at close or 21:30 · cash variance > ₹100 **P1** · big discount/void **P1** coalesced in 15-min batches ("3 overrides at Store 1") · khata limit crossed **P1** (max 1/customer/day) · khata ageing, low stock (coalesced counts), near-expiry (30/15/7-day horizons), record-day delight, app updates, backup status **P2** (backup failed 2 days running escalates to P1).
**The Day-End Digest** is the product's daily heartbeat: one push per store (+ chain rollup) — net sales & bills vs last same weekday → payments split → cash variance per counter → khata given/collected → top 5 items → alerts summary → one Day-2-checklist nudge. Each section is a DB-configured digest block, reorderable without release. Ships in **Phase 2 as WhatsApp-only** (needs no app); the Phase 3 app is mostly a rendering client.
### 4.3 Onboarding wizard (Phase 2; back office `/setup`, resumable)
Target: **first real bill within 15 minutes**, measured. Every completed step writes real tenant config immediately (no final submit — abandonment leaves a usable half-configured tenant); every skip is a stored decision that creates its Day-2 checklist item; per-step 30-second Hindi/English videos make both self-serve and dealer-assisted modes work without us on a call (the Phase-2 exit criterion). No step can dead-end: GSTIN API down → manual entry + auto-retry verify; printer undetected → skip; import errors → quarantine.
| # | Step | Highlights | Skippable |
|---|---|---|---|
| 0 | Account & plan | Phone + OTP → tenant; plan picker rendered from `plan`/`price_book` rows; default 14-day full-Pro trial; language sets tenant + user default | Plan deferrable via trial |
| 1 | GSTIN → business | GSP auto-fetch of legal name/address/state/registration type; **"No GSTIN"** path = unregistered mode (Bill of Supply) — the mini-shop reality; adding GSTIN later upgrades the invoice format automatically | Yes |
| 2 | Store & counters | Store 1 pre-filled from GSTIN; each counter auto-assigns a GST-legal series prefix (`ST1C1-`, editable) in `doc_series` | Counters addable later |
| 3 | Tax & pricing defaults | Three pre-answered confirmations: MRP tax-inclusive ON, round to nearest rupee, B2C thermal default; composition preset; GST rate master preloaded system-wide — never owner-entered | Yes |
| 4 | Items | (a) Excel upload with synonym-dictionary column auto-mapping ("MRP/Rate/Price", "विवरण"), 20-row preview, good rows import in background, bad rows quarantined — never block on row 4,812; (b) business-type template catalogs (kirana, pharmacy, apparel… — DB data, dealer-extendable); (c) skip — POS quick-add covers day one | **Yes — the most important skip** |
| 5 | Staff | Cashiers with POS PINs; owner is default supervisor until staff added | Yes |
| 6 | Install & pair POS | Download link SMS'd to the counter PC; POS first-run takes a 6-digit pairing code (15-min TTL) → binds machine, pulls license, starts master sync; import progress visible | Pairing deferrable |
| 7 | Printer setup (on POS) | Auto-detect ESC/POS, paper width, test print (₹ + Hindi line verifies code page), drawer-kick test | **Yes** — "No printer / WhatsApp e-bills only" is a first-class choice |
| 8 | First test bill | Guided overlay on the real billing screen; **TRAINING watermark, separate `doc_type=TRAINING` series** — never pollutes the GST series; the dealer's proof-of-done | No — the "aha" |
| 9 | Go live | Confirm series starts → Start Billing flips training off; confetti once; owner-app link SMS'd | No |
Progress lives in `onboarding_step(tenant, step_code, status, completed_by, at)` rows, feeding the checklist and the dealer hand-off record; every step is reopenable from Settings forever.
### 4.4 Day-2 checklist
One data-driven "Finish setting up" surface: `checklist_item(code, title_msg_code, deep_link, completion_probe, edition_min, priority, snooze_days)` — items added/retired without release; **completion is detected by server-side probes, never self-reported** (UPI item completes the moment the setting row exists). Surfaces: back-office dashboard card (progress ring + next 3, deep-linked), owner-app Today card, one idle POS status-bar chip ("Setup 70%" — never on the billing path), and exactly one line in the day-end digest. **No pushes for checklist items, ever.** Default items in priority order: complete/verify GSTIN → fix quarantined import rows → items missing HSN/GST rate (blocks clean GSTR-1 — chase early) → MRP/cost on template items (auto-completes via purchases) → UPI ID for dynamic QR → WhatsApp e-bills → top-5 suppliers → reorder levels (auto-suggest from 2 weeks of sales) → khata opening balances → staff + discount threshold → shop logo → owner-app install (auto-completes on first login) → barcode labels → store timings (drives sync-health alerts and digest timing) → backup/sync green (surfaces only if red). Done (auto) / Snooze 7 days / Dismiss forever; the card retires at 100% or 60 days and moves to Settings → Setup.
### 4.5 Dealer machinery (Phase 2 minimum; portal is Phase 4)
`dealer(code, name, phone, region, white_label_profile)` rows; every tenant carries `dealer_code` from signup — attribution for margins from day one, before any dealer UI exists. Dealer-assisted signup: dealer enters code + owner's phone; **the owner receives the OTP** and reads it out (consent stays with the owner; a consent SMS names the dealer). The dealer's login gets a scoped `installer` role: wizard steps 18, settings, imports, printer — **no sales data, ledgers, or reports** — auto-expiring 7 days after go-live, owner-re-grantable. The wizard's resumability *is* the dealer workflow: pre-provision steps 15 from the office the evening before, then on-site pairing → printer → training bill with the actual cashier at the keyboard → go-live in under 10 minutes. USB offline installer (shop bandwidth reality; pairing needs one moment of connectivity, initial payload masters-only). Classic customers: the migration tool replaces step 4 and marks steps 15 done from imported data. On go-live a **hand-off summary** generated from `onboarding_step` rows goes to the owner by WhatsApp — disputes settled by data. The training series doubles as the post-go-live staff-training tool; `support.channel` routes the owner's Support button to the dealer first. Deferred to the portal: commission statements, fleet view, white-label UI — the attribution rows above make them pure additions.
---
## 5. Cross-Cutting UX Systems
One meta-rule governs everything here: *nothing on the billing path may steal focus, block on network, or require reading a paragraph.* Each system is one pure-TS package (`@sims/search-core`, `ui-states`, `i18n`, `print-queue`, `gates`, `error-catalog`) consumed by all four surfaces; behavior ships in git once, content is tuned in the DB forever.
### 5.1 Search — one engine, two profiles
The POS scan box (local SQLite, items only, results painted **< 100 ms** headroom inside the 150 ms scan-to-line budget) and the back-office Ctrl+K omnibox (Postgres `pg_trgm` + the same key functions ported, < 400 ms p95, items/parties/bills/actions/settings with `#`/`@`/`>` prefixes) share one normalizer, phonetic function, and ranking — a cashier promoted to back-office work never relearns search.
**Match tiers (short-circuit):** 1 barcode exact (≥ 8 digits or wedge suffix; `2x` scale barcodes parsed *before* search) → 2 item-code exact (Classic codes carry over) → 3 token prefix on every word of every name/alias → 4 phonetic key → 5 fuzzy (edit-distance ≤ 2), running only when tiers 14 return < 8 results. Rank within tier by 90-day sales velocity, then shorter name popularity re-ranking makes 100k SKUs feel small.
**Hinglish, three mechanisms, all DB data:** multi-script names auto-transliterated at save (चावल `chaval`); a shared Indic phonetic key applied to index and query (`aaa, eei, wv, phf, chhch, shs`… so typed `chawal` and indexed `chaval` both collapse to `caval`; dhaniya/dhania/daniya collide correctly); and `item_alias(item_id, alias_text, source)` fed by imports, Purchase Inbox matching memory, and a one-key POS gesture when a cashier's search fails but they find the item another way, POS offers "remember *chawal* for this item? [Enter]". The catalog teaches itself the shop's vocabulary; a Hindi commodity-term seed pack ships as rows.
**Performance at 100k SKUs:** FTS5 with 23-char prefixes + a phonetic `search_key` table; top-5,000-by-velocity hot set in memory at startup (< 300 ms, inside the 5 s cold start) painting in < 20 ms with full-index merge behind it; no debounce (debounce = felt lag) each keystroke cancels the in-flight query on a worker thread; results capped 50, fuzzy candidates bounded 500. p50/p95 latency is telemetry with regression alerts we publish the number.
### 5.2 State & feedback vocabulary
Five tokens, one meaning each, everywhere: `ok` green (done/synced/paid) · `busy` amber (queued/pending) · `alert` red (a human must act **now**) · `idle` grey (offline/disabled *calm*) · `info` blue. **Offline is grey, never red** offline is the architecture, not a failure; red spent on "meh" kills the vocabulary. Every state renders icon + color + text/number via shared components that physically cannot render color alone. Back office uses skeletons (never a bare spinner > 300 ms); the POS billing path has no loading states at all. One document-status chip set spans POS lines, the bill browser, and the owner app: Draft · Held · Closed · Print queued/Printed · e-bill sent · IRN pending/registered · Sync pending/Synced · Cancelled.
**The sync chip (canonical spec, same component on all surfaces):** Synced (grey-green check) · Syncing (amber + `↑ 12`) · Offline (grey slash + quiet count) · Attention (red triangle — sync *errors* retry won't fix). State changes animate the chip only — no toast, no sound, no focus change. Clicking it (or Ctrl+, → Sync) opens a slide-over: last sync, pending ops by type, per-item errors with codes and action buttons, [Sync now], [Copy diagnostics]; Esc returns focus to the scan box.
**The Focus Doctrine:** on the billing screen, no system-initiated toast, dialog, popover, or focus change — not sync, not updates, not license warnings. Ambient information goes to a non-focusable **notice rail** above the status bar (icon + short text + key hint chips, max 3, overflow collapses to a count). The only sanctioned overlays are user-initiated by keystroke, and Esc always returns to the scan box. Back office may toast and dialog normally — it tolerates interruption; the counter does not.
### 5.3 i18n & bilingual printing
The full `message_catalog` (keyed by code, per language, per channel `ui|print|whatsapp|sms`, ICU MessageFormat) syncs into POS SQLite like any master — language never depends on network; per-user language applies instantly at login. Fallback: user → store default → English → (dev builds) the raw key, so gaps surface before rollout; a missing-strings report + machine-draft/human-confirm workflow makes language N a data task. Rendering rules: bundle Noto Sans + Devanagari (+ launch scripts) in app and print renderer — never trust shop-PC fonts; **ASCII digits always**, in every language, wherever money/qty appears; one shared `formatINR()` and FY-aware date formatter; layouts CI-tested against a +40% pseudo-locale (Hindi runs ~25% longer); key hints (`F8`) never localize.
Printing language policy lives in the template, not code: GSTIN/HSN/tax summary/legal footer always English (stable for auditors); header and thank-you in the store print language; item names per `{{item.name:print}}` policy (English-only default for thermal, bilingual default for A4/A5). ESC/POS text mode can't render Indic scripts — non-Latin receipts rasterize in bit-image mode; if raster pushes close-to-print past 1 s, POS suggests English thermal + bilingual A4. Fast by default, pretty on request.
### 5.4 Print system
`print_profile(counter, purpose ∈ receipt|invoice_a4|label|report, printer, paper, template, copies, drawer_kick…)`**routing is a rule, not a question**: B2C → thermal, B2B-with-GSTIN → A4, labels → label printer; the cashier never picks a printer mid-bill (Ctrl+P on a closed bill offers the override list). Profiles are settings rows editable from POS *and* back office — support fixes a store's printing remotely; the back-office template editor can queue a test page to any counter via sync. **The iron rule: the bill is legal the moment it commits to SQLite; printing is a consequence, never a condition.** Drawer-kick fires on payment confirm, not print success — cash must open even with a jammed printer. Failed prints stay `Queued` (billing is already on the next customer), retry every 15 s, auto-drain oldest-first on recovery (prompt-first is a config alternative); a job queued > 60 s with a customer phone offers the WhatsApp e-bill from the notice rail. Reprints: same-day from F10 ungated; every copy prints "DUPLICATE" and is audit-logged; older/> 2 copies → supervisor. Silent printing always — the OS dialog exists only in Settings → Advanced.
### 5.5 Permissions UX — two gates, two looks
**Edition gates lock (and sell); role gates hide — except in-flow actions, which challenge.** Locked features render normally + lock glyph + edition badge (`<GateBadge>` reading license-token flags); activation opens the upsell panel (screenshot/15 s demo, all DB content marketing can tune) — CTA by role: owner → in-place upgrade, staff → "Ask owner" request. Staff hitting paywalls become the sales channel. Never gate mid-bill: edition-gated POS features simply don't bind keys on the billing screen; the lock lives in Settings/menu. Role gates: navigation hidden (the cashier's world contains no Accounting menu — a smaller world trains in 10 minutes), but in-flow sensitive actions stay *visible* and raise the supervisor overlay (§1.7) — hiding them breaks flow and teaches nothing.
### 5.6 Accessibility & the training reality
Design target: a cashier who reads numbers fluently, reads text slowly, may be new to computers, and must be productive in 10 minutes. Defaults: distinct audio for scan-OK / error / attention (cashiers scan without looking — sound *is* the primary feedback channel); totals the largest element on screen, tabular numerals, WCAG AA high-contrast dense theme with dark mode for night shifts; every actionable button shows its key (`भुगतान F11`), with a printed keyboard sticker sheet in the dealer install kit — the F-row is the UI for a non-reader; touch targets ≥ 48 px, quick-keys as image + name + code (recognition beats reading); counter-scope Normal/Large font setting. Back office is semantic HTML + ARIA, keyboard-complete; POS v1's accessibility investment is keyboard + audio, full screen-reader support honestly deferred.
**Practice mode (Phase 2):** a `Practice` button on the login screen, no role needed. Separate SQLite DB seeded from a read-only copy of the store's *real* item master (muscle memory forms on the real catalog) + fake customers/khata, reset nightly. Unmistakable: diagonal `PRACTICE / अभ्यास` watermark, a reserved purple top bar, PRACTICE banner on the customer display. Contained: `TRAIN-####` series (the same mechanism as onboarding step 8's training bills — never touches the GST series), prints blocked by default, no transactional sync ever leaves, drawer-kick disabled. A guided first run coaches a 5-bill script (scan, qty, unknown-barcode recovery, hold/resume, cash close, UPI, one return) with completion recorded per user ("trained ✓" in Admin); script content is catalog rows, dealer-localizable. Practice mode doubles as the dealer's in-shop demo and the upsell demo surface.
### 5.7 Errors a human can act on
Every error, every surface: **[icon] what happened (one plain sentence) · what to do (imperative, one or two steps) · action buttons that perform the fix · a small code** (`E-2101`) — the support-call handshake replacing five minutes of description. Codes: `E-1xxx` billing · `2xxx` print/hardware · `3xxx` sync · `4xxx` payment/UPI · `5xxx` GST · `6xxx` auth/license · `7xxx` master data · `9xxx` internal. `error_catalog(code, severity, title_key, body_key, actions, support_note)` — texts translated and support-editable without release; `support_note` is the internal playbook line; code frequencies feed the tickets-per-store metric.
Presentation: billing-path errors are inline only (attached to the line or beside the scan box — never modal); ambient failures go to the notice rail + chips; **blocking is reserved for exactly three conditions** — shift not open, local DB integrity failure, license hard-expiry after the grace path (grace itself is a notice; never strand a shop with a queue mid-day). No raw exceptions ever: anything unmapped renders as `E-9001` ("Your bills are safe. Restart; if it repeats, call support with this code") and auto-files a crash report. Destructive confirmations exist only off the billing path and name the object — never "Are you sure?". Day-one catalog examples: E-1102 barcode not found ([Enter] quick-add · [Esc] skip) · E-1310 batch expired ([Pick batch] · [Supervisor]) · E-1405 over credit limit ([Take payment] · [Supervisor]) · E-2101 printer not responding ("Bill is saved… prints automatically when it's back" · [Retry] · [Send e-bill]) · E-4203 UPI unconfirmed ([Mark paid] · [Wait] · [Cash instead]) · E-3302 cloud rejected changes ("Nothing is lost" · [Open sync panel]) · E-5110 IRN pending offline (notice only) · E-6201 subscription expiring ([Remind owner]).
### Phase mapping
Search (POS profile), state vocabulary, print system, and errors are MVP-critical — they *are* the counter. i18n ships its mechanism in MVP (from first commit) with Hindi content in Phase 2. The supervisor PIN overlay is MVP; practice mode and the onboarding wizard + WhatsApp digest + Day-2 checklist are Phase 2; owner app, push notifications, remote approvals, Purchase Inbox, PO/GRN, and GSTR-2B recon are Phase 3. Every metric named in this document — keystrokes per flow, scan-to-line, wizard-to-first-bill, Inbox line share, approval round-trip, push opt-out rate — is instrumented from day one: we publish what we measure.