# SCREENS INVENTORY — Accounting (Saaed Books)

> **Verified accurate:** 2026-05-02 — module overview, AcctFlows registry, storage namespace, and globals all hold. **One drift:** `front/accounting/` is now 30 files (not 25) — the +5 are batch additions (`batch1-collect-spend.js`, `batch1-settings.js`, `batch1-stock-adj.js`, `batch3-screens.js`) for Batch 1 + Batch 3 work; net-new surfaces, original 15 tabs and 9 flows still in place as documented.
> **Status:** active source-of-truth doc; the canonical Accounting/Saaed Books catalog (3-part document, all 3 parts in this file).

> **Module:** `front/accounting/`
> **Files:** 25 (16,339 LOC)
> **Tabs:** 15 (`home`, `sell`, `collect`, `spend`, `pay`, `bank`, `journal`, `gl`, `recurring`, `coa`, `opening`, `periods`, `vat`, `reports`, `settings`)
> **Flows:** ~9 registered on `window.AcctFlows` (`invoice.quick`, `invoice.standard`/`invoice.b2b`/`invoice.zatca`, `bill.create`, `bill.pay`, `recv.payment`, `bank.reconcile`, `vat.file`, `period.close`, `onboarding`, `je.manual`)
> **Storage namespace:** `acct.*` in localStorage (`acct.tab`, `acct.tweaks`, `acct.store`, etc.)
> **Globals:** `window.SaaedBooks` (root), `window.AcctTab*` (15 tab components), `window.AcctFlows` (composer registry), `window.AcctData` / `AcctView` / `AcctStore` / `AcctLookup` / `AcctFmt` / `AcctTweaks` / `AcctHooks` / `AcctPillars` / `AcctCOATemplates`
> **Status:** Stage 4 backend coverage is the strongest in the codebase (80 OpenAPI paths under `/api/v1/accounting/*`). Day-1 cut-over is mostly replacing `AcctData.*` accessors with `useApi('accounting.*.list')` calls.

---

## §0 — How to read this document

This file is being delivered in **3 parts** because the module is the largest in the codebase. Each part is appended to this same file:

| Part | Tabs | LOC covered | Status |
|---|---|---|---|
| **Part 1 (this turn)** | overview · `home` · `sell` · `collect` · `spend` · `pay` | ~2,400 | ✅ in this file |
| **Part 2 (next turn)** | `bank` · `journal` · `gl` · `recurring` · `coa` · `opening` · `periods` | ~7,200 | ⏳ pending |
| **Part 3 (turn after)** | `vat` · `reports` · `settings` (16,339-LOC settings file) · cross-cutting flows · cut-over summary · pointer index | ~6,700 | ⏳ pending |

Same conventions as the Pay and Retail inventories: every screen lists **what renders**, **what it reads**, **what it writes**, **API mapping**, **permission gates**, and **production additions** the integration team should expect to need beyond the current FE.

---

## §1 — Module overview

### 1.1 File census

| File | LOC | Role |
|---|---|---|
| `accounting-hub.compiled.js` | 758 | Root shell `SaaedBooks`, tab bar, `HomeTab`, `ZeroDayHome`, `QuietHome`, pillar cards, alerts, revenue trend, cash forecast, P&L snapshot, top partners, fixture-dataset switcher |
| `accounting-sell.compiled.js` | 456 | `AcctTabSell` — invoice list + detail + status filters + ZATCA phase chips |
| `accounting-collect.compiled.js` | 350 | `AcctTabCollect` — receivables ageing, customer balances, payment recording |
| `accounting-spend.compiled.js` | 401 | `AcctTabSpend` — bill list + detail + 3-way match indicators |
| `accounting-pay.compiled.js` | 298 | `AcctTabPay` — vendor-payments list, batch payments, SADAD references |
| `accounting-bank.compiled.js` | 164 | `AcctTabBank` — bank-account list, transactions, reconcile launcher |
| `accounting-journal.compiled.js` | 593 | `AcctTabJournal` — journal-entries list + detail + manual JE button |
| `accounting-gl.compiled.js` | 368 | `AcctTabGL` — general-ledger drilldown (deep-linked from anywhere via `acct:gotoTab`) |
| `accounting-recurring.compiled.js` | 385 | `AcctTabRecurring` — recurring-journal templates, schedules, run history |
| `accounting-coa.compiled.js` | 282 | `AcctTabCOA` — chart-of-accounts tree (PK = `code`, not ulid) |
| `accounting-opening.compiled.js` | 347 | `AcctTabOpening` — opening-balance entry per account, trial-balance check |
| `accounting-periods.compiled.js` | 530 | `AcctTabPeriods` — fiscal periods, lock/unlock, year-end close |
| `accounting-vat.compiled.js` | 466 | `AcctTabVAT` — VAT returns + ZATCA filings + 7-box workpaper |
| `accounting-reports.compiled.js` | 343 | `AcctTabReports` — P&L, balance sheet, cash flow, custom report runner |
| `accounting-settings.compiled.js` | ~3,008 | `AcctTabSettings` — company, COA templates, tax rates, numbering, e-invoice, integrations, fiscal calendar, audit log |
| `accounting-flow-defs.compiled.js` | ~2,475 | All 9 wizard composers (multi-step flow defs registered on `AcctFlows`) |
| `accounting-flows.compiled.js` | ~? | `AcctFlow.open()` host, modal chrome, step navigation, palette `Cmd-K` integration |
| `accounting-data.compiled.js` | ~? | `AcctData` — fixture business, customers, vendors, invoices, bills, bank txns, KPIs, ageing, trends |
| `accounting-store.compiled.js` | ~? | `AcctStore` — write-side; subscribe; persisted to `localStorage` |
| `accounting-view.compiled.js` | ~? | `AcctView` — read-side derivations + `isZeroDay()` |
| `accounting-lookup.compiled.js` | ~? | `AcctLookup` — `customer(id)` / `vendor(id)` / `account(code)` resolvers |
| `accounting-fmt.compiled.js` | ~? | `AcctFmt` — `sar()`, `date()`, currency/locale formatters |
| `accounting-tweaks.compiled.js` | ~? | `AcctTweaks` — `pillarStyle`, `fixtures`, density, demo toggles |
| `accounting-pillars.compiled.js` | ~? | `AcctPillars` — three labelings of the 5 pillars (concept / arabic / business) |
| `accounting-coa-templates.compiled.js` | ~? | `AcctCOATemplates` — 25 Saudi-ready CoA templates (retail / spa / restaurant / healthcare / etc.) |

**Total: ~16,339 LOC across 25 files.** Settings + flow-defs + sell + journal alone are ~7,500 LOC.

### 1.2 Tab routing

The shell stores the active tab in `localStorage['acct.tab']` (default `'home'`). Cross-tab navigation uses a `CustomEvent('acct:gotoTab', { detail:{ tab, ...params } })` — children dispatch, the shell catches it and re-renders. GL deep-links additionally re-emit `acct:gl:filter` after a tick so the destination tab can mount before consuming the filter.

> **Production refactor:** Replace this with the platform-wide router (the same gap flagged in MODULE-MAP). The custom-event pattern is fine internally but won't survive cross-module deep links once Owner+HR or Pay want to drop a user on a specific journal entry.

### 1.3 The 5 pillars

`accounting-pillars.compiled.js` exposes three styling treatments (`concept`, `arabic`, `business`) for the same 5-pillar concept that defines the Home dashboard:

| # | id | Concept | Arabic | Business |
|---|---|---|---|---|
| 1 | `sell` | Sell | بيع | Revenue / Invoicing |
| 2 | `collect` | Collect | حصّل | Accounts Receivable |
| 3 | `spend` | Spend | أنفق | Costs / Expenses |
| 4 | `pay` | Pay | ادفع | Accounts Payable |
| 5 | `know` | Know | اعرف | Reports / P&L |

The 5th pillar (`know`) clicks through to the **Reports** tab, not its own tab. The other four map 1:1 to tabs of the same id.

### 1.4 Atom library (consistent across all 15 tabs)

Inventoried by reading `accounting-sell.compiled.js`, `accounting-hub.compiled.js`, and the audit doc:

- **`<Stat>`** — KPI tile with label / value / accent colour. ~20-line component, repeated inline in each tab file rather than imported (legacy decision; flagged for dedup in INTEGRATION-NOTES).
- **`<StatusChip>`** — pill with `bg`/`fg` from a per-tab `STATUS_COLORS(T)` map. Each tab redefines its own status palette; the schemas are mostly identical.
- **`<PhaseChip>`** — ZATCA phase indicator (`phase1` / `phase2` / `cleared` / `reported`).
- **`<PillarCard>`** — Home-only, the big 5-tile strip.
- **`<Pending>`** — fallback shown when a tab component hasn't loaded yet (`window.AcctTab*` undefined).
- **`<FixturePill>`** — top-right dataset switcher (Dalseen Retail vs Al-Nakheel Spa).

> **Cut-over note:** Every tab redefines `Stat`, `StatusChip`, `Tab` button-bar wrappers, etc. inline. There's **no shared atom export** — production should extract these into `acct-atoms.compiled.js` before adding new tabs, or the duplication will keep growing.

### 1.5 Permission map

| Permission | Gates |
|---|---|
| `accounting.invoices.view` / `.create` / `.update` / `.send` / `.void` | Sell tab + invoice composer |
| `accounting.payments.view` / `.create` (customer-side) | Collect tab + receive-payment flow |
| `accounting.bills.view` / `.create` / `.update` / `.approve` | Spend tab + bill composer |
| `accounting.vendor-payments.view` / `.create` | Pay tab + bill-pay flow |
| `accounting.bank-accounts.view` / `.bank-transactions.view` / `.reconciliations.create` | Bank tab + reconcile flow |
| `accounting.journal-entries.view` / `.create` / `.post` | Journal tab + manual JE flow |
| `accounting.gl.view` | GL tab (read-only by definition) |
| `accounting.recurring.view` / `.create` / `.run` | Recurring tab |
| `accounting.coa.view` / `.manage` | COA tab + Settings → Chart of Accounts |
| `accounting.opening-balances.manage` | Opening tab |
| `accounting.periods.view` / `.lock` / `.close` | Periods tab + close-period flow |
| `accounting.vat-returns.view` / `.create` / `.file` | VAT tab + file-VAT flow |
| `accounting.reports.view` | Reports tab |
| `accounting.settings.manage` (and ~12 sub-perms) | Settings tab |

Roles: **owner** has all; **manager** has all view + create/update on transactions but not `.lock` / `.close` / `.settings.manage`; **accountant** has view+create on transactions and `.coa.view` but not `.manage`; **auditor** has view-only across the board.

### 1.6 Storage keys

| Key | Type | Notes |
|---|---|---|
| `acct.tab` | string | Last active tab — survives refresh |
| `acct.tweaks` | JSON | `{pillarStyle, fixtures, showDemo, density, ...}` |
| `acct.store` | JSON | Full mutable store (`AcctStore.get()`) — invoices, bills, JEs, settings.company, onboarded flag, vatFilings, periodCloses, openingBalances, billPayments. Shimmed off API in production. |
| `acct.onb.dismissed` | bool | Suppresses the auto-onboarding popup once dismissed |

**Cut-over:** `acct.store` is the **mock backend**. Stage 4 has full coverage; the integration team's task is replacing `AcctStore.get()` reads with `useApi(...)` calls and `AcctStore.update*()` writes with mutations. The `acct.tab` and `acct.tweaks` keys stay client-side.

### 1.7 Cross-tab event bus

Three custom events on `window`:

- `acct:gotoTab` — payload `{ tab, ...params }`. Shell-level navigation.
- `acct:gl:filter` — payload `{ accountCode?, periodId?, customerId?, vendorId?, dateFrom?, dateTo? }`. GL drill-down.
- `acct:flow:opened` / `acct:flow:closed` — emitted by `AcctFlow.open()` and the modal-host close. Tabs listen so they can refresh after a write.

Plus the `AcctHooks.on(cb)` subscriber pattern for data changes (invoices added, bills paid, etc.). Every tab subscribes; many use a `useReducer(x=>x+1, 0)` bump trick to force re-render on hook fire.

> **Production:** Replace `AcctHooks` with TanStack Query invalidation. The `acct:gl:filter` event is the only one that needs to survive — it's a deep-link contract.

### 1.8 Zero-day branching

The Home tab has **three states** based on `AcctView.isZeroDay()` and `AcctStore.onboarded`:

1. **Pre-onboarding** (`isZeroDay && !onboarded`): `<ZeroDayHome>` — welcome card + 4 Day-One actions. Auto-opens the `onboarding` flow once (`window.__acctOnbPrompted` guard).
2. **Post-onboarding-quiet** (`isZeroDay && onboarded`): `<QuietHome>` — real pillar grid with zero values + "Create first X" CTAs.
3. **Active**: full dashboard with KPI strip, alerts, revenue trend, cash forecast, P&L snapshot, top partners.

**This three-state pattern is unique to Home.** Every other tab shows an "empty list / no data" inline empty state instead.

### 1.9 Cross-references

- **Boot sequence & namespaces** → see `MODULE-MAP.md` §3
- **Design tokens, atoms, type scale** → see `DESIGN-SYSTEM.md`
- **API endpoint catalogue** → see `API-USAGE-MAP.md` (every `useApi()` consumer mapped to backend route)
- **Pay tab in this module** is **distinct from** the `front/pay/` module — Acct's Pay tab handles AP (vendor payments); the Pay module handles AR settlements + payment-rail compliance. They share zero code but are sometimes confused. Flagged for INTEGRATION-NOTES.

---

## §2 — `home` tab · `HomeTab` (lines 358–660 of `accounting-hub.compiled.js`)

### 2.1 Layout

Three-state branching as documented in §1.8. Below covers the **active** state — the dashboard a tenant with real data sees.

```
┌─────────────────────────────────────────────────────────────┐
│ Sticky tab bar (15 tabs · badges · fixture-pill on right)  │
├─────────────────────────────────────────────────────────────┤
│ HERO                                                        │
│ ┌─────────────────────────────┐  ┌──────────────────────┐  │
│ │ "SAAED Books" · biz name    │  │ Pillar style chooser │  │
│ │ VAT · Q2 2026 · Apr 22      │  │ [Concept|Arabic|Biz] │  │
│ └─────────────────────────────┘  └──────────────────────┘  │
│                                                             │
│ ┌──────────────────────────────────────────────────────┐   │
│ │ 5-PILLAR STRIP — 5 large cards, equal columns       │   │
│ │ ① Sell   ② Collect   ③ Spend   ④ Pay   ⑤ Know     │   │
│ │ each: number badge, name, sub, metric value, delta │   │
│ └──────────────────────────────────────────────────────┘   │
│                                                             │
│ ┌──────────────────────────────────────────────────────┐   │
│ │ NEEDS ATTENTION (alerts) — only if any alerts       │   │
│ │ • N overdue invoices · SAR 12,400              →    │   │
│ │ • N overdue bills · SAR 4,200                  →    │   │
│ │ • N bank txns to reconcile                     →    │   │
│ │ • VAT return due in 9 days · SAR 23,000         →   │   │
│ └──────────────────────────────────────────────────────┘   │
│                                                             │
│ ┌─────────────────────────┐  ┌─────────────────────────┐   │
│ │ Revenue trend (12mo)    │  │ Cash forecast (90d)     │   │
│ │ — sparkline + delta     │  │ — runway calc           │   │
│ └─────────────────────────┘  └─────────────────────────┘   │
│                                                             │
│ ┌─────────────────────────┐  ┌─────────────────────────┐   │
│ │ P&L snapshot            │  │ Top customers/vendors   │   │
│ │ — revenue/COGS/opex/net │  │ — top 5 by AR or AP     │   │
│ └─────────────────────────┘  └─────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 Pillar metrics (active state)

Computed from `AcctData.kpis()`:

| Pillar | Metric | Sub-label | Delta |
|---|---|---|---|
| Sell | `K.revenue` | "Revenue · YTD" | `+12.4%` (hard-coded — flagged) |
| Collect | `K.ar` | "Owed to you" | `N overdue` from ageing rows |
| Spend | `K.cogs + K.opex` | "Costs · YTD" | `% of revenue` |
| Pay | `K.ap` | "You owe" | `N overdue` from AP ageing |
| Know | `K.net` | "Net profit" | `% margin` |

Each card colours its number-badge from a 5-stop palette: `[ok, accent, warn, danger, ink]`. Hover state: `translateY(-2px) + 0 4px 16px shadow + borderColor=color+'44'`. Click: `setTab(p.id === 'know' ? 'reports' : p.id)`.

### 2.3 Alerts row

Built inline at lines 410–432 of hub. Up to 4 alert kinds:

| Kind | Source | Threshold | Click destination |
|---|---|---|---|
| `ar` | `aging.rows.filter(r=>r.daysLate>0)` | `length > 0` | `tab=collect` |
| `ap` | `apAging.rows.filter(r=>r.daysLate>0)` | `length > 0` | `tab=pay` |
| `bk` | `bankTxns.filter(t=>!t.matched)` | `length > 0` | `tab=bank` |
| `vat` | `vatPeriods.find(p=>p.status==='open')` | `period exists` | `tab=vat` |

> **Production additions:**
> - **Snooze** — accountants want to dismiss for 24h.
> - **Severity ordering** — current order is creation order, not urgency. Sort by `daysLate desc` within each kind.
> - **VAT due-days calculation** uses a hard-coded `new Date(2026,3,22)` reference. **Bug** — replace with `Date.now()`.

### 2.4 Revenue trend / Cash forecast / P&L snapshot / Top partners

Four cards in a 2-column grid (revenue + cash on top, P&L + partners below). Each is its own component — `RevenueTrendCard`, `CashForecastCard`, `PnlSnapshotCard`, `TopPartnersCard` — defined later in the same file (lines 580+).

| Card | Renders | Reads |
|---|---|---|
| Revenue trend | 12-month bar/line sparkline · YTD total · vs-prior-year delta | `AcctData.revenueTrend()` → `{labels, arLabels, values, prior}` |
| Cash forecast | 90-day projected cash position · runway days · "lowest point" marker | `AcctData.cashForecast()` → `{days, projected, ar_ins, ap_outs}` |
| P&L snapshot | Revenue · COGS · Gross · Opex · Net — 5 rows with bars; "View full P&L" → reports | `AcctData.kpis()` |
| Top partners | Top 5 customers by AR + top 5 vendors by AP, side by side | `aging.rows`, `apAging.rows` |

### 2.5 API mapping (cut-over)

| FE source | Production endpoint | Status |
|---|---|---|
| `AcctData.kpis()` | `GET /api/v1/accounting/dashboard/kpis?period=ytd` | ✅ verified Stage 4 |
| `AcctData.aging()` | `GET /api/v1/accounting/reports/ar-aging` | ✅ |
| `AcctData.apAging()` | `GET /api/v1/accounting/reports/ap-aging` | ✅ |
| `AcctData.revenueTrend()` | `GET /api/v1/accounting/reports/revenue-trend?months=12` | ✅ |
| `AcctData.cashForecast()` | `GET /api/v1/accounting/reports/cash-forecast?days=90` | ⚠️ verify — name may be `cash-flow-forecast` in OpenAPI |
| `AcctView.isZeroDay()` | derived client-side from above (`invoices.length===0 && bills.length===0 && journalEntries.length===0`) — keep client-side | n/a |
| `AcctStore.onboarded` | `GET /api/v1/companies/{id}` → `setup_completed_at` boolean | ✅ |

### 2.6 Production additions

| Item | Priority | Notes |
|---|---|---|
| Period picker (currently hard-coded "Q2 2026 · Apr 22") | P0 | Header pill, persist to `acct.tweaks.period` |
| Configurable pillar metric (revenue → gross? net?) per role | P2 | Owner sees revenue, accountant sees net |
| Drill-down on revenue-trend bars | P1 | Click month → GL with date filter |
| "Compare vs last year" toggle on P&L snapshot | P2 | |
| Multi-company switcher in header | P0 | Currently single-tenant assumption |
| Forecast-confidence band (high/medium/low) | P1 | Backend already returns `confidence` per day |

---

## §3 — `sell` tab · `AcctTabSell` (456 LOC)

### 3.1 Layout

```
┌─ Header ────────────────────────────────────────────────┐
│ "Sell" · "Invoices" · subtitle           [+ New invoice]│
├─ 4-tile stat strip ─────────────────────────────────────┤
│ [YTD invoiced] [Outstanding] [Overdue] [Collected]      │
├─ Filter + search ───────────────────────────────────────┤
│ [All|Draft|Sent|Partial|Paid|Overdue]    🔎 search…    │
├─ Two-column body ───────────────────────────────────────┤
│ ┌──────────────────────────────┐ ┌────────────────────┐ │
│ │ INVOICE LIST                 │ │ INVOICE DETAIL     │ │
│ │ 7-col grid:                  │ │ (when selected)    │ │
│ │ Number · Customer · Date ·  │ │ — invoice face     │ │
│ │ Due · Total · ZATCA · Status │ │ — JE preview       │ │
│ │ — sticky header              │ │ — ZATCA XML viewer │ │
│ │ — virtualized at >100 rows   │ │ — actions menu     │ │
│ │ — overdue rows: red Due cell │ │                    │ │
│ │ — partial: warn sub-total    │ │ [Close]            │ │
│ └──────────────────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```

### 3.2 List columns

`gridTemplateColumns: '1fr 2fr 110px 100px 120px 110px 100px'`

| Col | Header | Renders | Notes |
|---|---|---|---|
| 1 | Number | `inv.no` in Fraunces serif | |
| 2 | Customer | `c.en`/`c.ar` + truncated `VAT 30012345…` if present | resolved via `AcctLookup.customer(id)` |
| 3 | Date | `F.date(inv.date, lang)` | locale-aware |
| 4 | Due | `F.date(inv.due, lang)`, **red** if `status==='overdue'` | |
| 5 | Total | SAR no-cents, **bold serif**; if partial: `SAR X left` sub-line in warn color | |
| 6 | ZATCA | `<PhaseChip>` (phase1 / phase2 / cleared / reported) or `—` | |
| 7 | Status | `<StatusChip>` from `STATUS_COLORS(T)` | 6 statuses |

**Active row:** `borderLeft: 3px solid T.ink` + `background: T.surface3`. Empty state: "No invoices match" centred at 40px padding.

### 3.3 Status palette

```js
{
  draft:   { bg:T.surface3,  fg:T.muted,   en:'Draft',   ar:'مسودة' },
  sent:    { bg:T.accent+'18', fg:T.accent, en:'Sent',    ar:'مرسلة' },
  partial: { bg:T.warn+'20',   fg:T.warn,   en:'Partial', ar:'جزئي'  },
  paid:    { bg:T.ok+'18',     fg:T.ok,     en:'Paid',    ar:'مدفوعة'},
  overdue: { bg:T.danger+'18', fg:T.danger, en:'Overdue', ar:'متأخرة'},
  void:    { bg:T.border,      fg:T.muted,  en:'Void',    ar:'ملغاة' },
}
```

### 3.4 Stat tiles

| Label | Calc |
|---|---|
| YTD invoiced | `sum(invoices.total)` |
| Outstanding | `sum((status≠paid,void,draft).total - .paid)` |
| Overdue | `sum(status===overdue).(total-paid)` |
| Collected | `sum(status===paid).total` |

Tile colours: ink / accent / **danger** / **ok** — matches the alert-card colour language.

### 3.5 Detail drawer (right column when selected)

Right pane is `460px` fixed. Contains (component `<InvoiceDetail>`, lines 220+ of file):

- **Invoice face** — bilingual header, customer block, line items, VAT breakdown, totals
- **ZATCA QR + phase indicator** — Phase-2 invoices show the cleared/reported badge with timestamp
- **Journal entry preview** — DR Accounts Receivable / CR Sales / CR Output VAT
- **Action menu** — Send · Send reminder · Record payment · Duplicate · Void · Download PDF · Download ZATCA XML
- **History** — sent at, viewed at (if email-tracked), paid at

### 3.6 Composer entry points

- **Header `+ New invoice`** → `window.AcctFlow.open('invoice.standard')` → routes to `invoiceB2B` flow (5 steps: type → who → lines → terms → review)
- **`Cmd-K` / global FAB → "Quick invoice"** → `window.AcctFlow.open('invoice.quick')` → routes to `invoiceB2BQuick` flow (single step)
- **`Cmd-K` → "ZATCA invoice"** → same `invoiceB2B` flow but pre-selects Phase-2 in step 1

The `composerOpen` local state at line 25 is **dead code** — the new flow system uses `AcctFlow.open()` instead. Flagged for cleanup.

### 3.7 What it reads / writes

**Reads:**
- `window.AcctData.invoices` (active, NOT `AcctView` — flagged)
- `window.AcctLookup.customer(id)` — for each row's customer name + VAT
- `window.AcctFmt.sar()`, `window.AcctFmt.date()`

**Writes:** none directly — all writes go through the flow composer (`AcctStore.addInvoice`, `AcctStore.markSent`, `AcctStore.recordPayment`, `AcctStore.voidInvoice`).

**Re-renders on:** `AcctHooks.on(...)` (data bus).

### 3.8 API mapping

| FE | Production endpoint | Verified |
|---|---|---|
| List | `GET /api/v1/accounting/invoices?status=&q=&from=&to=&page=` | ✅ |
| Detail | `GET /api/v1/accounting/invoices/{ulid}` | ✅ |
| Create draft | `POST /api/v1/accounting/invoices` | ✅ |
| Send | `POST /api/v1/accounting/invoices/{ulid}/send` | ✅ |
| Record payment | `POST /api/v1/accounting/invoices/{ulid}/payments` | ✅ |
| Void | `POST /api/v1/accounting/invoices/{ulid}/void` | ✅ |
| ZATCA submit (Phase 2) | `POST /api/v1/accounting/invoices/{ulid}/zatca/submit` | ✅ |
| ZATCA XML download | `GET /api/v1/accounting/invoices/{ulid}/zatca/xml` | ✅ |
| Download PDF | `GET /api/v1/accounting/invoices/{ulid}/pdf` | ✅ |

### 3.9 Production additions

| Item | Priority | Notes |
|---|---|---|
| Server-side filter+search (currently client-side over fixture) | P0 | List fetcher must accept the filter params |
| Pagination / virtualization for >500 invoices | P0 | Current scroll container caps at 560px max-height — add `react-window` |
| Bulk actions (select rows → mark sent / send reminder / void) | P1 | Checkbox column + sticky bulk-bar |
| Date range filter | P1 | "Last 30 days" / "Q2" / custom |
| Saved views | P2 | "Overdue 30+" / "VIP customers" |
| Export to Excel/CSV | P1 | Standard accountant ask |
| Send-reminder action with template | P1 | Currently no email-template UI |
| Inline credit-note creation from a paid invoice | P1 | Today only via Cmd-K → manual JE |
| Multi-currency totals strip (when company has FX invoices) | P2 | Stat tiles assume SAR only |

### 3.10 Hard-coded values flagged

- Line 44: `invoices.filter` does not pass `q` through `AcctView` — bypasses zero-day shim. Use `window.AcctView?.invoices?.() ?? AcctData.invoices`.
- Line 25: dead `composerOpen` state — remove.
- Line 60: `dispatchEvent(new CustomEvent('acct:flow:opened', ...))` not emitted before `AcctFlow.open` — small bug, no observable effect because flow host emits its own event.

---

## §4 — `collect` tab · `AcctTabCollect` (350 LOC)

### 4.1 Layout

```
┌─ Header ───────────────────────────────────────────────┐
│ "Collect" · "Receivables"      [+ Record payment]     │
├─ 4-tile stat strip ────────────────────────────────────┤
│ [Total AR] [Overdue] [Avg DSO] [In dispute]           │
├─ AGEING BUCKETS (5-tile mini-grid) ───────────────────┤
│ Current | 1-30 | 31-60 | 61-90 | 90+                  │
├─ Filter + search ──────────────────────────────────────┤
│ [All|Current|Overdue|In dispute]   🔎                 │
├─ Customer balance list ────────────────────────────────┤
│ Customer · Open invoices · Oldest · Total balance · ...│
└────────────────────────────────────────────────────────┘
```

### 4.2 Stat tiles

| Label | Calc |
|---|---|
| Total AR | `K.ar` |
| Overdue | `sum(aging.rows where daysLate>0).balance` |
| Avg DSO | `K.dso` (days sales outstanding) |
| In dispute | `customers.where(disputed).balance` |

### 4.3 Ageing buckets

Five mini-tiles in a single horizontal grid. Each click filters the list below to that bucket:

| Bucket | daysLate range | Colour |
|---|---|---|
| Current | `≤ 0` | ok |
| 1-30 | 1-30 | accent |
| 31-60 | 31-60 | warn |
| 61-90 | 61-90 | danger lighter |
| 90+ | `> 90` | danger |

### 4.4 Customer-balance list

Per customer, not per invoice. Columns:

| Col | Header | Renders |
|---|---|---|
| 1 | Customer | name + VAT no. |
| 2 | Open invoices | count badge |
| 3 | Oldest | days since oldest unpaid invoice |
| 4 | Current / 1-30 / 31-60 / 61-90 / 90+ | mini ageing bars (5 numbers) |
| 5 | Balance | total open SAR |
| 6 | Action | `[Record payment]` button |

Click row → expand to show **per-invoice breakdown** of that customer's open balances. Each invoice row has its own `[Record payment]` and `[Send reminder]` action.

### 4.5 Composer entry points

- **Header `+ Record payment`** → `AcctFlow.open('recv.payment')` → 4-step flow: pick invoice → method → amount & date → review.
- **Per-row `[Record payment]`** → same flow, pre-fills the customer + invoice.

### 4.6 What it reads / writes

**Reads:** `AcctData.aging()`, `AcctData.invoices`, `AcctData.customers()`, `AcctLookup.customer`.
**Writes:** via `AcctStore.recordPayment(invoiceId, {amount, method, date, ref})` — emits `acct:flow:closed` on completion.

### 4.7 API mapping

| FE | Production endpoint | Verified |
|---|---|---|
| AR ageing | `GET /api/v1/accounting/reports/ar-aging` | ✅ |
| Customer balances | `GET /api/v1/accounting/customers?include=balance` | ✅ |
| Open invoices for customer | `GET /api/v1/accounting/invoices?customer_id=&status=open` | ✅ |
| Record payment | `POST /api/v1/accounting/invoices/{ulid}/payments` | ✅ |
| Send reminder | `POST /api/v1/accounting/invoices/{ulid}/remind` | ⚠️ verify — endpoint name |

### 4.8 Production additions

- **Statement of account** — PDF per customer summarizing all open invoices. P1.
- **Bulk reminder send** — select customers/invoices → email all. P1.
- **Auto-reminder cadence** — already exists in Settings, but UI to override per customer. P2.
- **Disputes workflow** — currently a flag; needs a notes thread + resolution states. P1.
- **Promised-pay-date** field — sales/AR tool. P2.

---

## §5 — `spend` tab · `AcctTabSpend` (401 LOC)

Mirrors Sell — same shell, different entity (bills instead of invoices, vendors instead of customers).

### 5.1 Layout

Header (`+ New bill`) → 4-tile stat strip (YTD spent · Open bills · Overdue · Paid this month) → Filter+search (All|Draft|Approved|Partial|Paid|Overdue) → 2-col body (bill list | bill detail).

### 5.2 List columns

`gridTemplateColumns: '1fr 2fr 110px 100px 120px 100px 100px'`

| Col | Header | Renders |
|---|---|---|
| 1 | Bill # | `bill.vendor_invoice_no` (vendor's number, **not** our internal) |
| 2 | Vendor | name + tax# |
| 3 | Date | invoice date |
| 4 | Due | red if overdue |
| 5 | Total | SAR; partial sub-total in warn |
| 6 | Match | **3-way-match indicator** — ◯ unmatched · ⚠ partial · ✓ full (PO + GRN + bill) |
| 7 | Status | StatusChip |

The **3-way-match column** is unique to Spend — it's the strongest pre-built integration point with the Retail purchasing module. When the FE wires up to the live API, this column should show match state from the `purchasing.bills` join.

### 5.3 Stat palette + status palette

Statuses: `draft / pending_approval / approved / partial / paid / overdue / void` — 7 not 6 (added `pending_approval` because bills route through manager approval where invoices don't).

### 5.4 Detail drawer

Lines 200+ of file. Contains:

- **Bill face** — vendor block, line items, VAT breakdown, totals
- **3-way match panel** — links to PO and GRN (Retail module), shows variances
- **Approval timeline** — submitted by · approved by · paid by
- **JE preview** — DR Expense / DR Input VAT / CR AP
- **Action menu** — Approve · Reject · Pay · Void · Download · Match to PO

### 5.5 Composer entry points

- **`+ New bill`** → `AcctFlow.open('bill.create')` → 3 steps: vendor → detail → review.
- **`Match to PO`** action → opens a **Match drawer** (not a flow) — list of open POs from Retail filtered by vendor, drag-to-match line items.

### 5.6 API mapping

| FE | Production endpoint | Verified |
|---|---|---|
| List | `GET /api/v1/accounting/bills?status=&vendor_id=&q=&from=&to=` | ✅ |
| Detail | `GET /api/v1/accounting/bills/{ulid}` | ✅ |
| Create | `POST /api/v1/accounting/bills` | ✅ |
| Approve | `POST /api/v1/accounting/bills/{ulid}/approve` | ✅ |
| Match to PO | `POST /api/v1/accounting/bills/{ulid}/match` body `{po_id, lines:[…]}` | ⚠️ verify — cross-module endpoint |
| Void | `POST /api/v1/accounting/bills/{ulid}/void` | ✅ |

### 5.7 Production additions

- **OCR upload** — drag PDF → autofill bill fields. P1, large feature.
- **Approval routing rules** by amount/category/vendor. P1 — Settings has the rule editor; this tab consumes.
- **Recurring bill from this** — promote a paid bill to a recurring template. P2.
- **Vendor-portal link** — vendor can submit bills directly. P3.

---

## §6 — `pay` tab · `AcctTabPay` (298 LOC)

### 6.1 Distinction from `front/pay/`

This tab is **AP outflow** — paying vendors. The separate `front/pay/` module is **AR inflow** — settling customer card transactions and managing the payment-rail. **Different domain entirely.** Linkable confusion is high; both surfaces should keep distinct titles in production (Saaed Books "Vendor payments" vs Pay "Settlements").

### 6.2 Layout

```
┌─ Header ───────────────────────────────────────────────┐
│ "Pay" · "Vendor payments"          [+ Pay bills]      │
├─ 4-tile stat strip ────────────────────────────────────┤
│ [Total AP] [Due this week] [Overdue] [Paid YTD]       │
├─ Tabs: [Upcoming|History] ─────────────────────────────┤
├─ Upcoming view ────────────────────────────────────────┤
│ Vendor · Bill · Due · Amount · Method preset · ...    │
│ — checkboxes for batch                                 │
├─ History view ─────────────────────────────────────────┤
│ Date · Vendor · Bills paid (count) · Method · Ref ·   │
│ Amount · Status                                        │
└────────────────────────────────────────────────────────┘
```

### 6.3 Upcoming list

Columns: vendor · bill # · due · amount · default method (from vendor record) · checkbox.

A sticky bottom bar appears when ≥1 row checked: **`Pay N bills · Total SAR XXX [Continue]`** → triggers `AcctFlow.open('bill.pay')` with the batch pre-filled.

### 6.4 History list

Columns: date · vendor · bills count · method · reference (IBAN last-4 / SADAD ref / cheque #) · amount · status (`pending / sent / cleared / failed`).

### 6.5 Method palette

Maps to the bill-pay flow's method tiles:

| Method | Icon | Code | Sub |
|---|---|---|---|
| Bank transfer (IBAN) | 🏦 | 1101 | Same-day · no fee |
| SADAD | 🧾 | 1101 | For utility/govt bills |
| Cheque | 📝 | 1101 | Reduces cash on cleared |
| Cash | 💵 | 1010 | From cash on hand |

### 6.6 Composer entry points

- **`+ Pay bills`** (header) → `AcctFlow.open('bill.pay')` with no batch.
- **Batch bar** → same flow with `{batch: [billIds]}` pre-fill.
- **Per-row "Pay now"** → same flow with `{billId}`.

### 6.7 API mapping

| FE | Production endpoint | Verified |
|---|---|---|
| Upcoming | `GET /api/v1/accounting/bills?status=approved,partial&due_before=...` | ✅ (re-uses bills endpoint) |
| History | `GET /api/v1/accounting/vendor-payments?from=&to=` | ✅ |
| Detail | `GET /api/v1/accounting/vendor-payments/{ulid}` | ✅ |
| Create payment | `POST /api/v1/accounting/vendor-payments` | ✅ |
| Cancel pending | `POST /api/v1/accounting/vendor-payments/{ulid}/cancel` | ⚠️ verify |

### 6.8 Production additions

- **Cash-impact preview** before confirming a batch (current bank balance − batch total = projected). P0 — accountants always check.
- **Multi-bank chooser** if company has >1 bank account. P0.
- **WPS-style batch file export** for cross-vendor bank uploads (similar to HR/payroll). P1.
- **Approval gate** — high-value payments need owner sign-off. P1.
- **Failed payment reason capture + retry**. P1.

---

## §7 — `bank` tab · `AcctTabBank` (164 LOC)

### 7.1 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ BANKING                                            [✦ Auto-reconcile]│
│ Bank feeds & reconciliation                                         │
│ Synced 2h ago · 27 transactions                                     │
├─ Account tabs ──────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐        │
│ │ Riyad·Op   │ │ Al Rajhi   │ │ ANB·Payroll│ │ Mada CC    │        │
│ │  142,830   │ │  88,210    │ │   12,400   │ │  5,231 owe │        │
│ │ 14/27 recon│ │ No activity│ │ No activity│ │ No activity│        │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘        │
├─ Reconciliation banner ─────────────────────────────────────────────┤
│ ⊙ 52% │ 13 transactions need matching        [Unmatched|Matched|All]│
│       │ 8,420 SAR net unmatched                                     │
├─ Transaction list ──────────────────────────────────────────────────┤
│ Date     Description         Amount    Match              Action    │
│ 22 Apr   POS sweep · Olaya  +12,830   ✓ INV-1208 PMT-...         — │
│ 22 Apr   Customer transfer  +1,500     ✦ INV-1243 balance  [Match] │
│ 21 Apr   Bank fee           -45        ✦ Bank fee · 6600   [Match] │
│ ...                                                                 │
└─────────────────────────────────────────────────────────────────────┘
```

### 7.2 Account tabs (lines 49–77)

Hard-coded list of four bank accounts pulled by code: `1101` (Riyad Operating), `1102` (Al Rajhi savings), `1103` (ANB payroll), `2210` (Mada credit card). Active account is highlighted with `border: 2px solid T.ink`. Only `1101` has bank-txn data in the seed; the others render "No new activity."

**Production gap.** Account list is hard-coded — should come from `GET /accounting/bank-accounts` (or a derived `acct.bankAccounts` endpoint). Tenants with more than four bank accounts cannot be supported until this is dynamic.

### 7.3 Reconciliation banner (lines 80–101)

Conic-gradient ring shows reconciled %. Filter chips toggle the list. **The "Auto-reconcile" CTA in the header is wired to nothing in the FE** — clicking it does nothing today. Recommend hooking to `POST /accounting/bank-reconcile/auto` or whatever the analogous endpoint is.

### 7.4 Match suggestions (lines 137–158)

`MatchSuggestion` component is a **hard-coded lookup keyed by transaction ID** (`bk285`–`bk289`). This is purely demo polish — no real ML/heuristic matching. Production needs:
- A real backend matching engine (or call to one) that returns ranked suggestions per unmatched txn
- UI for accepting/rejecting/editing a suggestion (today the "Match" button takes a hard-coded `'Manual'` string)

### 7.5 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Account tabs (line 49) | `GET /accounting/bank-accounts` | **MISSING — currently hard-coded by code** |
| Bank txns list (line 19) | `GET /accounting/bank-transactions?account_code=&status=` | Verified — `acct.bankTxns` seed exists |
| `MatchSuggestion` (line 137) | `GET /accounting/bank-transactions/{id}/suggestions` | **MISSING — hard-coded fixture** |
| "Match" button (line 130) | `POST /accounting/bank-transactions/{id}/match` body `{matchRef, mode}` | **VERIFY** — currently calls `AcctHooks.matchBankTxn` (in-memory) |
| "Auto-reconcile" CTA (line 41) | `POST /accounting/bank-reconcile/auto?account_code=` | **MISSING — no-op today** |
| Sync banner (line 30) | `GET /accounting/bank-feeds/sync-status` | **MISSING — hard-coded "2h ago"** |

### 7.6 Production additions for the integration team

- **Reconcile wizard.** `bank.reconcile` is referenced by Home but not yet implemented as a flow. The wizard should: (1) load all unmatched txns, (2) show statement balance vs. ledger balance, (3) allow split/combine matching, (4) produce a reconciliation report. Spec'd in INTEGRATION-NOTES.
- **Statement upload.** Drop a `.csv`/`.ofx`/`.mt940` file → POST to `/accounting/bank-statements/import` → parsed into transactions for matching. Today Saudi banks rarely offer feeds; manual upload is the norm.
- **Per-account view.** Clicking an account tab should change `account_code` filter on the txn list — currently the list is global.
- **Statement-balance cell.** Missing today. Must show statement balance, ledger balance, and the difference at the top of the txn list — that's the whole point of bank rec.
- **Date-range filter** on txns list.
- **Bulk-match selection** with checkboxes and a sticky action bar.

---

## §8 — `journal` tab · `AcctTabJournal` (594 LOC)

### 8.1 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ JOURNAL                              [⇩ Export CSV] [+ New manual]  │
│ 1,247 entries                                                       │
│ Every journal the books post — sales, bills, payments, POS, ...    │
├─ 4 stat tiles ──────────────────────────────────────────────────────┤
│  Entries in view │ Total debits │ Total credits │ Status           │
│      1,247       │   2,148,300  │   2,148,300   │  Balanced 0.00   │
├─ Filter bar ────────────────────────────────────────────────────────┤
│ Period: [Any|Today|7d|30d|Month|Quarter|Year]                       │
│ │ Kind: [All 1247] [Sale 421] [Bill 198] [Payment 142] ...          │
│ │                                              [🔍 Search…        ] │
├─ List (left) ──────────┬─ Detail (right, sticky) ──────────────────┤
│ DATE  KIND  REF·MEMO   │ [Sale] [Balanced]                          │
│ 22Apr [Sale] INV-1208  │ Office sale · INV-1208                     │
│ 22Apr [Bill] BILL-44   │ JE #: JE-04221  Ref: INV-1208  Date: 22Apr │
│ 21Apr [Pay]  PAY-101   │                                             │
│ ...                    │ Acct  Description  Debit       Credit      │
│                        │ 1201  AR · Acme    1,150.00       —        │
│                        │ 4100  Sales rev       —          1,000.00  │
│                        │ 2401  VAT output      —            150.00  │
│                        │ ────────────────── 1,150.00      1,150.00  │
│                        │                                             │
│                        │ [↗Open source] [⎙Print] [⎘Copy] [✎Adjust]  │
│                        │ [↺ Reverse entry]                          │
└────────────────────────┴────────────────────────────────────────────┘
```

### 8.2 The 11 JE kinds (lines 17–31)

`KIND_META(T)` defines colour, icon, and label for every JE source. **Every JE in the books carries a `kind` set by its poster** — this is the single most important taxonomy in the module:

| Kind | Source | Colour | When emitted |
|---|---|---|---|
| `sale` | Sell tab / `invoice.b2b` flow | `T.ok` | On invoice issue |
| `bill` | Spend tab / `bill.record` | `T.warn` | On bill record |
| `payment` | `recv.payment` | `T.accent` | Customer payment received |
| `bill_pay` | `bill.pay` / `pay` tab | `T.accent` | Vendor payment sent |
| `pos` | POS sweep (cross-module from Retail) | `T.ink2` | End-of-shift sweep |
| `stock` | Stock adjustments (cross-module from Retail) | `T.ink2` | Adjustment, write-off, stocktake |
| `vat` | VAT filing | `T.ink` | Period file/payment |
| `adjust` | `je.adjust` flow | `T.muted` | Manual adjustment |
| `opening` | Opening balances import | `T.muted` | One-time at onboarding |
| `close` | Period/year close | `T.ink2` | Close ceremony |
| `reverse` | `je.reverse` flow | `T.danger` | Reversal entries |
| `general` | Manual entry / fallback | `T.ink` | Default |

The integration team must ensure **every server-side JE poster sets `kind`**. A JE without `kind` falls back to "Manual" which is misleading for engine-posted entries.

### 8.3 Period + kind filter (lines 53–115)

Period chips: `any | today | 7d | 30d | mtd | qtd | ytd`. Kind chips: dynamically built from observed kinds, sorted by frequency. Search matches `id`, `ref`, `memo`, line `code`, line `note`. **`'2026-04-22'` is the hard-coded reference date** — same demo-fixture issue as Home.

### 8.4 Detail panel (lines 280–420)

Shows JE header (kind pill, balanced pill, memo, IDs, date), full lines table with COA name resolution via `window.AcctData.coa.find(a => a.code === line.code)`, totals row, and an actions row.

**Five actions** in the detail panel:
1. **Open source** (line 519, `openSource()`) — pattern-matches on `je.ref`: `INV*` → opens `invoice.b2b`, `BILL*` → opens `bill.record`, `PAY*` → opens `payment.receive`, else narrator toast. **Brittle pattern matching** — the ref convention is convention not contract; production needs `je.sourceType + je.sourceId`.
2. **Print** (line 528) — opens a new window with hand-rolled HTML and calls `window.print()`. The print template is bilingual via `dir` attribute. RTL working.
3. **Copy** (line 564) — clipboard text dump.
4. **Adjust** (line 393, `adjustJE()`) — opens `je.adjust` flow. **Permission-gated** by `accounting.je.adjust`.
5. **Reverse** (line 580, `reverseJE()`) — opens `je.reverse` flow. **Permission-gated** by `accounting.je.reverse`. Disabled if already reversed (checks for an existing `kind: 'reverse'` JE with matching `ref: 'REV-' + je.id`) — opening, reverse, period-close JEs are not reversible.

### 8.5 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Period filter (line 53) | `GET /accounting/journal-entries?from=&to=&kind=&q=&page=` | Verified (`acct.journals`) |
| Detail panel (line 280) | `GET /accounting/journal-entries/{id}` | Verified |
| New manual entry (line 156) | `POST /accounting/journal-entries` (via `je.manual` flow) | **VERIFY** |
| Adjust (line 393) | `POST /accounting/journal-entries/{id}/adjust` | **VERIFY** — flow opens but not networked |
| Reverse (line 580) | `POST /accounting/journal-entries/{id}/reverse` | **VERIFY** — flow opens but not networked |
| Open source (line 519) | (No endpoint — opens client flow) | n/a |
| Export CSV (line 463) | `GET /accounting/journal-entries/export?format=csv&...` | **MISSING — generated client-side today** |

### 8.6 Production additions

- **Pagination.** 1,247 entries renders fine; 100k will not. Backend endpoint already supports `?page=` per Postman; FE needs to wire it.
- **Server-side CSV export** — the client-side blob construction (line 444) won't scale and ignores the active filter on the backend. Should pass current filter set to `/export`.
- **Detail-pane stickiness** — the right column is `position: sticky, top: 74` (line 279). On long JEs with 20+ lines this overflows the viewport. Move to scrolling pane.
- **Source linkage** — replace the `ref`-prefix sniff with explicit `je.source_type` + `je.source_id` from backend.
- **Filter persistence** — currently lost on tab switch. Persist to `localStorage['acct.journal.filter']`.

---

## §9 — `gl` tab · `AcctTabGL` (368 LOC)

### 9.1 What it is

The General Ledger drill-down — a per-account transaction history with running balance. **This is the deep-link target for every report's "drill into account" affordance**: P&L row → opens GL filtered to that account; balance-sheet row → same; tax workpaper → same; bank rec → same.

### 9.2 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ GENERAL LEDGER                                       [⇩ Export XLSX]│
│ 1101 · Riyad Bank · Operating ▾                                     │
│ 1 Apr 2026 → 22 Apr 2026 · 47 movements                             │
├─ Filter strip ──────────────────────────────────────────────────────┤
│ [MTD|QTD|YTD|Custom]  [Branch ▾All] [Cost ctr ▾All] [Project ▾All]  │
├─ Balance strip (4 cells) ───────────────────────────────────────────┤
│  Opening    │ Total Dr │ Total Cr │ Closing                         │
│  120,000    │  87,400  │  64,570  │  142,830 (bigger, ok colour)    │
├─ GL table ──────────────────────────────────────────────────────────┤
│ DATE      SOURCE     MEMO                Dr      Cr      Balance    │
│ 1 Apr     OB-2026    Opening balance     ...     —      120,000     │
│ 2 Apr     INV-1180   Acme settlement     1,150   —      121,150     │
│ 3 Apr     BILL-22    Wholesale           —       4,200  116,950     │
│ ...                                                                 │
└─────────────────────────────────────────────────────────────────────┘
```

### 9.3 Account picker (lines 138–143, 327–366)

Clicking the page title opens a modal: full-text search across COA by code/name (en/ar), default 60-row fallback, click any account to jump.

### 9.4 Period presets (lines 56–66)

`mtd | qtd | ytd | custom` — same demo-fixture `'2026-04-22'` reference date. Custom uses fixed `2026-04-01` → `2026-04-22` (broken — needs a real date picker).

### 9.5 Dimensions (lines 38, 156–169)

Three dimension chips: `branch_id`, `cost_center`, `project`. Filter applies at the **JE level** not the line level — `if (dim.branch_id !== 'all' && je.branchId && je.branchId !== dim.branch_id) return;`. Production needs:
- Backend to accept `dimensions[branch_id]=`, `dimensions[cost_center]=`, `dimensions[project]=` per Postman §17.7
- Lines themselves to carry dimension tags (today only the JE header carries them, which is wrong for cost-center splits)

### 9.6 Running-balance algorithm (lines 95–117)

1. Compute opening = sum of all postings to this account before `range.from`
2. For each line in range, `delta = isDebitNormal ? (debit - credit) : (credit - debit)`
3. Cumulative running balance per row

`isDebitNormal` is derived from the COA `type` field: assets, expenses, COGS are debit-normal; everything else is credit-normal. **This must match backend computation exactly** — otherwise the running balance disagrees with the trial balance.

### 9.7 Drill-down event (lines 39–49)

Listens for `acct:gl:filter` events with `{ tab: 'gl', account_code, from, to, dimension }`. Reports tab fires this when the user clicks a P&L row. **This is the primary cross-tab integration contract for GL.**

Click any GL row → fires `acct:gotoTab` with `{ tab: 'journal', selectId: jeId }` → Journal tab opens with that JE selected.

### 9.8 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Lines (line 75) | `GET /accounting/gl?account_code=&from=&to=&dimensions[branch_id]=` | Verified per Postman §17.7 |
| Account picker | `GET /accounting/coa?postable=true` | Verified |
| Export (line 156) | `GET /accounting/gl/export?account_code=&from=&to=&format=xlsx` | Verified per Postman §17.7 |
| Drill from reports | `POST /accounting/reports/drill` | Verified per Postman §17.7 |

### 9.9 Production additions

- **Date picker for `custom`** — currently hard-coded.
- **Per-line dimensions** — JE-level filtering is wrong for cost-center splits.
- **Running balance must match backend trial balance** — write an integration test.
- **Pagination on the table** — for accounts with many movements (sales revenue, COGS, AR control).
- **Multi-account compare** — analysts often want to see two accounts side-by-side. Spec'd as a future enhancement.

---

## §10 — `recurring` tab · `AcctTabRecurring` (385 LOC)

### 10.1 What it is

Scheduled JEs that auto-post on a cadence (monthly/quarterly/annual). Use cases: rent accruals, prepaid-insurance amortisation, depreciation. Each schedule has lines, cadence, status (`active|paused|stopped`), and run-history.

### 10.2 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ RECURRING ENTRIES                              [+ New schedule]     │
│ Scheduled journal entries                                           │
│ 8 active · 3 due this week                                          │
├─ Filter strip ──────────────────────────────────────────────────────┤
│ [All 12] [Active 8] [Paused 4]                                      │
├─ List (left, sticky) ──────────┬─ Detail (right) ────────────────────┤
│ R-101  ●Active  [Accrual]     │ R-101  ●Active   [Pause][▶Run now] │
│ Office rent                    │ Office rent · monthly               │
│ Monthly · day 1   24,000       │ Posted via prepaid-rent JE on day 1│
│ Next run: 1 May                │                                     │
│                                │ ┌─Schedule──Next──Last──Runs─────┐ │
│ R-102 ●Paused [Adjust]         │ │ Monthly │ 1 May │ 1 Apr │ 4   │ │
│ Depreciation                   │ └────────────────────────────────┘ │
│                                │                                     │
│                                │ Posting lines:                      │
│                                │ Code  Account     Debit    Credit  │
│                                │ 6201  Rent expense 24,000     —    │
│                                │ 1305  Prepaid rent    —    24,000  │
│                                │ ─────────────────  24,000   24,000 │
└────────────────────────────────┴─────────────────────────────────────┘
```

### 10.3 Schedule shapes (lines 21–28, 376–384)

- `STATUS_META`: `active | paused | stopped` (colour-coded)
- `KIND_META`: `accrual | adjust | deferred`
- `scheduleSummary()` formats `s.kind` (`monthly|quarterly|annual`) + `s.dayRule` (`'end'` or `'day:N'`) for display.

### 10.4 Run-now action (lines 199–211)

Optimistic local update: increments `runCount`, sets `lastRun` to today, advances `nextRun` by one period, fires `AcctHooks`. **No network call** — should call `POST /accounting/recurring-je/{id}/run-now` and re-render from response.

### 10.5 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| List (line 35) | `GET /accounting/recurring-je` | Verified per Postman §17.7 |
| New schedule (line 80) | `POST /accounting/recurring-je` | **NOT WIRED — button is a no-op today** |
| Pause/resume (line 192) | `PATCH /accounting/recurring-je/{id}` body `{status}` | **VERIFY — local mutation only** |
| Run now (line 199) | `POST /accounting/recurring-je/{id}/run-now` | **VERIFY — local mutation only** |
| Detail | `GET /accounting/recurring-je/{id}` | Verified |

### 10.6 Production additions

- **Schedule editor** — the "New schedule" button is dead today. Needs a flow: pick name + kind + cadence + lines + start/end dates + memo template.
- **Run-history modal** — see the past 12 runs with their JE IDs and dollar totals. Today only `runCount` and `lastRun` are shown.
- **Skip-next-run** action — accountants need to skip a single period without pausing.
- **Variable-amount schedules** — today amounts are fixed in the lines. For depreciation that decreases each year, the schedule needs an amount formula or a per-run override.
- **Auto-pause on close** — if the target period is closed, the schedule should pause and surface a warning rather than fail silently.

---

## §11 — `coa` tab · `AcctTabCOA` (282 LOC)

### 11.1 What it is

Hierarchical chart of accounts viewer. SOCPA-aligned (Saudi Organization for Chartered & Professional Accountants). **The COA is the most-touched seed by every other module** — POS, invoicing, payroll, VAT all reference COA codes.

### 11.2 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ CHART OF ACCOUNTS                  [Expand all] [Collapse all]      │
│ 87 postable accounts · 14 headers                                   │
│ SOCPA-aligned hierarchy · Retail-group template                     │
├─ 6 group cards ─────────────────────────────────────────────────────┤
│ Assets │ Liabilities │ Equity │ Revenue │ COGS │ Expenses           │
│ 982k   │  321k       │ 480k   │ 2.1M    │ 720k │ 410k               │
│ 12 a/c │ 8 a/c       │ 4 a/c  │ 6 a/c   │ 3 a/c│ 18 a/c             │
├─ Search ────────────────────────────────────────────────────────────┤
│ [🔍 Search name or code …]                       [Clear filter]     │
├─ Tree table ────────────────────────────────────────────────────────┤
│ CODE   ACCOUNT             TYPE        BALANCE      CLASSIFICATION  │
│ 1000   ASSETS                          982,000      Group           │
│ 1100   ▾ Cash & equivalents [Asset]    231,500      Header          │
│ 1101     Riyad·Operating    [Asset]    142,830      Current         │
│ 1102     Al Rajhi·Savings   [Asset]     88,210      Current         │
│ 1200   ▾ Receivables        [Asset]    340,000      Header          │
│ ...                                                                 │
└─────────────────────────────────────────────────────────────────────┘
```

### 11.3 6 SOCPA groups (lines 14–22)

Hard-coded array, each with `key | en | ar | colour | sign`. The `sign` is the natural-balance multiplier used to display revenue/liability/equity as positive magnitudes regardless of internal sign convention.

### 11.4 Tree algorithm (lines 28–58)

`buildTree(accounts)` builds a parent-child tree by `account.parent` reference. Roots are accounts with no parent. `flatten(nodes, collapsed)` produces the visible row list honouring the collapsed-set.

`buildTree` is **O(n²)** — sort uses `localeCompare` which on a 5,000-account COA could be slow. For large tenants the integration team should pre-sort server-side and stream a flat list.

### 11.5 Search/filter logic (lines 75–88)

Query matches `code | en | ar`. **Crucially, matched leaves bring their ancestors with them** — so a search for "rent" finds `6201 · Rent expense` AND its parent `6200 · Operating expenses` AND `6000 · EXPENSES`. This preserves hierarchy context.

### 11.6 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Tree load (line 65) | `GET /accounting/coa` | Verified |
| New account | `POST /accounting/coa` | **MISSING — no UI today** |
| Edit account | `PATCH /accounting/coa/{code}` | **MISSING — read-only viewer today** |
| Archive account | `DELETE /accounting/coa/{code}` (or status flag) | **MISSING** |
| Reorder/reparent | `PATCH /accounting/coa/{code}` body `{parent}` | **MISSING** |

### 11.7 Production additions

- **Edit modal.** Tap a leaf row → modal with name (en+ar) + type + parent + classification + active flag. Today the COA is read-only via UI.
- **New account flow.** Wizard: choose parent → enter code (validate uniqueness, suggest next free) → enter names → choose classification.
- **Archive guard.** Cannot archive an account with non-zero balance or any history.
- **Bulk import.** CSV upload for tenants migrating from another system.
- **SOCPA template selector.** Today the template is "Retail-group" (hard-coded label). Need: Retail / F&B / Services / Manufacturing templates pickable at tenant onboarding.
- **Permission gating.** Edits must require `accounting.coa.edit`.

### 11.8 Cross-module dependencies

Every other Accounting tab and most cross-module postings reference COA codes. **Renaming or re-coding an account is destructive across all journals** — the production edit modal must surface this and require confirmation.

---

## §12 — `opening` tab · `AcctTabOpening` (347 LOC)

### 12.1 What it is

**Mandatory onboarding step for every tenant migrating from another system.** Without opening balances, every report is wrong from day one. The tab supports two paths: manual entry, or import a trial balance from CSV/XLSX.

### 12.2 Layout — three modes

```
view mode (default after first import):
┌─ Header: status pill · as-of date · source system · "Journal JE-OB-2026" link ──┐
│ TRIAL BALANCE BROUGHT FORWARD · 1 Jan 2026                                       │
│  ●Posted  · From QuickBooks · 18 Dec 2025 · Journal JE-OB-2026                   │
│                                  [⇩ Import from file] [✎ Edit manually]          │
├─ Balance strip (always visible) ─────────────────────────────────────────────────┤
│ Total debits │ Total credits │ Difference │ Accounts                              │
│  1,450,300   │   1,450,300   │  0  Balanced │  47 rows                            │
├─ Read-only table ────────────────────────────────────────────────────────────────┤
│ 1101  Riyad·Operating   120,000        —                                         │
│ 1200  Receivables       340,000        —                                         │
│ 2100  Payables                —    180,000                                       │
│ ...                                                                              │
└──────────────────────────────────────────────────────────────────────────────────┘

edit mode (Σdr=Σcr enforced):
  - Inputs are editable
  - Bottom action bar: "Trial balance is balanced — ready to post" or red diff
  - [Post opening JE] disabled until balanced

import mode:
  - Drop / pick CSV/XLSX → "Parsing and matching codes…" → review (which is edit mode)
  - Required columns: code, name_en, name_ar, debit, credit
  - Server matches codes against COA, surfaces unmatched as warnings
```

### 12.3 Status state machine (lines 130–144)

| Status | Colour | Meaning |
|---|---|---|
| `imported` | `T.warn` | File parsed, awaiting balance check + post |
| `balanced` | `T.accent` | Σdr === Σcr, ready to post |
| `posted` | `T.ok` | JE posted, locked |

Once `posted`, opening balances are **immutable** without an `accounting.opening.unlock` permission (not yet implemented).

### 12.4 Save logic (lines 100–119)

```js
if (!totals.balanced) { alert(`Difference: ${F.sar(totals.diff)}`); return; }
seed.rows = rows.slice();
seed.status = 'posted';
seed.postedAt = today;
window.AcctHooks?.fire?.();
alert('Opening balances posted as a single journal entry.');
```

**Local mutation + alert()** — production must:
- `POST /accounting/opening-balances` with the balanced row set
- Backend posts JE with `kind: 'opening'`, `id: 'JE-OB-{fy}'`
- Idempotency: posting twice should fail with a clear "already posted" error

### 12.5 Import flow (lines 270–320)

`drop → parsing → review`. The "parsing" stage is **simulated** with a `setTimeout(1400)` and re-uses `seed.rows` regardless of file content. Production needs:
- `POST /accounting/opening-balances/import` (multipart)
- Backend returns `{ rows: [...], unmatched: [...], warnings: [...] }`
- "Review" stage shows unmatched codes with a "create COA account" inline action or a "skip" option
- Drag-and-drop UI is referenced in the copy ("Or drag a file here") but not implemented — the actual handler is `<input type="file">`

### 12.6 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Initial load (line 28) | `GET /accounting/opening-balances?as_of=` | Verified per Postman §17.3 |
| Save (line 100) | `POST /accounting/opening-balances` | **VERIFY — local-only today** |
| Import (line 296) | `POST /accounting/opening-balances/import` (multipart) | **VERIFY — simulated today** |
| Unlock (future) | `POST /accounting/opening-balances/unlock` (`accounting.opening.unlock` perm) | **MISSING** |

### 12.7 Production additions

- **Real file parsing** — replace the `setTimeout` simulation. Backend must return parsed rows with COA-code matching status.
- **Drag-and-drop** the input to actually accept dropped files.
- **Per-row validation** — flag missing required fields, duplicate codes, codes not in COA.
- **Locale-aware number parsing** — `1,234.56` (en) vs `١٬٢٣٤٫٥٦` (ar) vs `1.234,56` (some EU formats). Today: `parseFloat` only, which silently truncates.
- **Audit log** — every edit/post should log who, when, what changed.
- **Comparison view** — when re-importing (which should rarely happen), show a diff against currently posted balances.
- **Multi-period openings** — for tenants migrating mid-year, opening balances may need to be entered for multiple historical period-ends, not just one as-of date.

---

## §13 — `periods` tab · `AcctTabPeriods` (530 LOC)

### 13.1 What it is

Fiscal calendar management: 12 monthly periods + a year-end ceremony. Each period has status `open | review | closed`, a list of blockers (must resolve before close) and warnings (advisory). Close is a checks-then-confirm flow with a dramatic "Closing period…" pulse animation. Year-end is a separate multi-step modal that requires all 12 periods closed.

### 13.2 Layout

```
┌─ Header ────────────────────────────────────────────────────────────┐
│ FISCAL CALENDAR · FY 2026                  [🔒 Close fiscal year]   │
│ Fiscal year 2026                           [Year-end ceremony]      │
│ 4 of 12 periods closed · started 1 Jan 2026                         │
├─ Year strip · 12 month tiles ───────────────────────────────────────┤
│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐                   │
│ │JAN││FEB││MAR││APR││MAY││JUN││JUL││AUG││SEP││OCT││NOV││DEC│       │
│ │●Cl││●Cl││●Cl││●Op││●Op││ ─ ││ ─ ││ ─ ││ ─ ││ ─ ││ ─ ││ ─ │       │
│ │31J││28F││31M││  ─││  ─││   ││   ││   ││   ││   ││   ││   │       │
│ └──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘                    │
├─ Detail (selected: April) ──────────────────────────────────────────┤
│ ACCOUNTING PERIOD                            [🔒 Close period]      │
│ April 2026  ·  1 Apr → 30 Apr                                       │
├─ Summary strip (4 cells) ───────────────────────────────────────────┤
│ JEs posted │ Postings │ Draft JEs │ Blockers                         │
│   421      │  2,148   │     2 ⚠   │     1 ✗                          │
├─ Blockers + warnings list ──────────────────────────────────────────┤
│ ✗ 1 blocker:                                                        │
│   ⊘ Bank rec incomplete · 13 unmatched txns      [Open]            │
│ ⚠ 2 warnings:                                                       │
│   i Draft JEs exist · review or post              [Open]            │
│   i VAT period not yet filed                       [Open]            │
└─────────────────────────────────────────────────────────────────────┘
```

### 13.3 Period card states (lines 130–170)

`STATUS_META` defines `open | review | closed` with `T.ink2 | T.warn | T.ok` colours. Closed cards show closed-date in serif font.

### 13.4 Period detail (lines 173–232)

Calls `window.AcctData.periodChecks(key)` to load `{ blockers: [], warnings: [], summary: {} }`. **`canClose = !isClosed && blockers.length === 0 && new Date(period.end) <= new Date('2026-04-30')`** — note the hard-coded reference date again. Production needs `today()` from the server.

### 13.5 Close-period modal (lines 312–414)

Three stages: `confirm → running → done`. The "running" stage is a 1.6-second pulse animation (line 408). Real backend close should be:
- `POST /accounting/periods/{key}/close`
- Server posts closing JEs, locks the period, returns the period record
- FE displays "Closing…" until response, not fixed timeout
- Real progress indicator (not just pulse) for closes that touch large data

### 13.6 Year-end modal (lines 417–530)

Two gates: (1) all 12 periods must be closed, (2) review the four steps:
1. Verify all 12 periods closed
2. Sweep P&L → Retained Earnings (one big closing JE)
3. Lock the fiscal year
4. Open FY+1 with carry-forward

**The year-end is the single most consequential operation in Accounting** — it's irreversible by design. Production additions:
- Hard re-confirm with FY name typed by user
- Audit log entry with user + IP + timestamp
- Pre-flight: backup snapshot before running
- Email notification to tenant owner on success
- Reopen-FY permission must be very tightly scoped (root + super-admin only)

### 13.7 Reopen action (lines 92–96)

`if (!confirm('Reopen this period?')) return;` — `window.confirm` dialog. **Should be replaced with a styled modal** and require `accounting.period.reopen` permission (today permission is not checked client-side).

### 13.8 API mapping

| FE call site | Endpoint expected | Status |
|---|---|---|
| Periods load (line 25) | `GET /accounting/periods?fy=2026` | Verified per Postman §17.2 |
| Checks (line 31) | `GET /accounting/periods/{key}/checks` | Verified per Postman §17.2 |
| Close period (line 350) | `POST /accounting/periods/{key}/close` | **VERIFY — local mutation today** |
| Reopen (line 91) | `POST /accounting/periods/{key}/reopen` | **VERIFY** |
| Year-end close | `POST /accounting/year-end/close` | **VERIFY — local-only today** |

### 13.9 Cross-module check sources

The `blockers` and `warnings` returned by `/periods/{key}/checks` come from multiple modules — backend must aggregate from:

| Check source | Origin module | Severity |
|---|---|---|
| Bank rec incomplete | Accounting (this module) | Blocker |
| Draft JEs in period | Accounting | Warning |
| VAT not yet filed | Accounting (vat tab) | Warning (becomes blocker if past due) |
| Open POs / GRNs | Retail (purchasing) | Warning |
| Stocktake variance unposted | Retail (inventory) | Warning |
| Unposted payroll | HR (payroll) | Blocker |
| Open shifts | Retail (POS) | Blocker |
| Unsettled payments | Pay | Warning |

**This is a cross-module aggregation contract** — flagged for INTEGRATION-NOTES.md. The integration team must define each check's owner, severity, and resolution path.

---

## §14 — Part 2 closing notes

### 14.1 What's covered in Part 2

✅ `bank` (account tabs, recon banner, txn list, match suggestions)
✅ `journal` (JE list+detail, 11 kinds, 5 actions including Adjust/Reverse with permission gating)
✅ `gl` (account picker, period presets, dimensions, running-balance, drill-down)
✅ `recurring` (schedules, run-now, pause/resume)
✅ `coa` (tree builder, search-with-ancestors, 6 SOCPA groups)
✅ `opening` (3 modes, balance enforcement, import simulation)
✅ `periods` (12-month strip, blockers/warnings, close ceremony, year-end)

### 14.2 Common patterns surfaced across Part 2

1. **Hard-coded reference date `'2026-04-22'`.** Appears in Home (Part 1), Journal, GL, Periods. **P0** — replace with server `today()` everywhere.
2. **Local mutation instead of API call.** Almost every "action" button in Part 2 mutates seed data and fires `AcctHooks` rather than calling the backend. Confirm/Reverse/Adjust JE; Run-now schedule; Save opening balances; Close period; Year-end. **P0** — wire to real endpoints.
3. **Permission gating exists in two tabs only** (Journal: `accounting.je.adjust|reverse`). All other destructive actions (period close, year-end, opening post, COA edit) lack permission checks today. **P1**.
4. **`alert()` and `confirm()` are still used** in opening and periods. Production must replace with styled modals.
5. **Pulse animations stand in for real progress indicators.** Close-period uses a 1.6s pulse; year-end uses 2.2s. Wire to real responses.

### 14.3 What's coming in Part 3

⏳ `vat` tab — VAT returns, ZATCA filings, 7-box workpaper
⏳ `reports` tab — P&L, balance sheet, cash flow, custom report runner
⏳ `settings` tab — the largest single file in Accounting (~3,008 LOC)
⏳ All 9 wizard composers (`invoice.b2b`, `bill.create`, `bill.pay`, `recv.payment`, `bank.reconcile`, `vat.file`, `period.close`, `onboarding`, `je.manual`)
⏳ Cross-cutting concerns (`AcctFlow` host, `Cmd-K` palette, modal chrome, audit log, multi-tenant gaps)
⏳ Cut-over priority summary (P0/P1/P2 line items across all 15 tabs)
⏳ Pointer index (file → screen → endpoint quick-lookup)

### 14.4 Open uncertainties (need your call before Part 3)

1. **Bank-account list dynamism.** Currently hard-coded to 4 accounts. Confirm this becomes `GET /accounting/bank-accounts` in Stage 4, or document as Stage 5 enhancement.
2. **JE source linkage.** Currently sniffs prefix from `je.ref` (`INV*`/`BILL*`/`PAY*`). Confirm migration to explicit `je.source_type + je.source_id` is in scope, or this stays as a documented sniff.
3. **`/periods/{key}/checks` aggregation.** Per §13.9 this needs cross-module input from Retail/HR/Pay. Is this aggregation backend-side, or does the FE call each module separately and combine? Recommend backend.
4. **Year-end re-confirmation.** Spec'd as "type the FY name" — confirm or relax to a simple modal.
5. **COA edit permissions.** I propose `accounting.coa.edit` (admin-only). Confirm or expand to `manager+`.

Standing by for greenlight on Part 3 or course-correction on Part 2.
