Skip to content

Accounting fiscal-period UX — global period selector + RBAC-gated Fiscal-Year config tab

Why

The Accounting "Layered view" makes the user pick a period/date-range separately per tab (P&L has its own range; TB/BS/AR are as-of-now; CFS/SOCIE/WPP/Bank/Tax each carry their own picker). This caused a real misread (owner thought the Related-Party report under-counted — it was just the per-tab period). Owner wants: one global period selector above all tabs (+ "All time") and an RBAC-gated tab to view/edit the fiscal calendar.

Build branch

nightly. accounting:config (needed by Phase 3's config tab) was cherry-picked to origin/nightly (0a604417); nightly also has the Q2/WPP work (e9ae445a). Build Phase 1→3 on nightly, commit per phase. (Pacing: owner chose phase-by-phase, commit each.)

Current state (file anchors, from read-only audit)

  • Container: AccountingContent (components/accounting/AccountingApp.tsx:3485). LayeredView.tsx is presentational (no period state).
  • Existing page-level shared controls: Subsidiary + Basis (AccountingApp.tsx:~4272-4303) — add the period selector beside these.
  • Fragmented period handling: P&L parent pnlDateRange + inline RangePicker (:4088); TB/BS/AR as-of-now; CFS/SOCIE/WPP/Bank/Tax each own a picker.
  • Fiscal calendar: EntityCalendarSettings (lib/accounting/types.ts:1701), computed by listBasisPeriods() (lib/taxHK/basisPeriod.ts:88), ERL_CALENDAR fallback. Editable today but buried as regex text inputs in the Settings modal (AccountingApp.tsx:~3341-3369), no validation/preview.
  • RBAC: page gated allowedRoles: ['auditing_accountant','bookkeeper'] (lib/rbac/pageRules.ts:59). accounting:config permission now exists (granted super_admin/admin/auditing_accountant).

Plan (phase-by-phase, commit each)

  • Phase 1 — global period state + selector. New PeriodContext provider holding { start, end, label } or all-time; a header selector above the tabs (beside Subsidiary/Basis), options from listBasisPeriods() (P1, stub, each FY) + "All time".
  • Phase 2 — migrate tabs to consume it. P&L drops its inline RangePicker; CFS/SOCIE/WPP follow; TB/BS/AR use the period END as the as-of date. Keep optional LOCAL override only where finer granularity is genuinely needed (Bank Transactions, Tax, Month-end).
  • Phase 3 — Fiscal-Year config tab (RBAC-gated). Editor for EntityCalendarSettings (replacing the buried regex inputs), live period-preview table, validation (contiguous, one year-end/year, can't redraw a closed period), gated by accounting:config (client + the server PATCH — note: settings PATCH server gate landed in T-057).

Risks

Tabs needing sub-period granularity; closed-vs-open period interplay; "All time" query cost; ensure the period selector and the WPP launcher's own period stay consistent.

Log

  • 2026-06-17 created. Prep done: accounting:config brought to nightly (0a604417). Phase 1 next.
  • 2026-06-17 Phase 1 DONE (origin/nightly 8d2df025): new components/accounting/PeriodContext.tsx (PeriodProvider + useAccountingPeriod() + PeriodSelector); options = listBasisPeriods(entityCalendar) newest-first + "All time", default = latest period; provider self-fetches via useAccountingSettings. Wired into AccountingApp (wrap AccountingContent + selector beside Basis). tsc+lint clean. Tabs NOT yet consuming it (Phase 2). Browser-verify on nightly preview (page is auth-gated, couldn't preview locally). Phase 2 next: migrate P&L/CFS/SOCIE/WPP to consume the context; TB/BS/AR use period end as as-of; keep local overrides on Bank/Tax/Month-end.
  • 2026-06-17 Phase 2a DONE (origin/nightly 08f051cc): P&L + Related Party now consume useAccountingPeriod() — both per-tab RangePickers removed, replaced with a "set via the Period selector" hint. (RP was the motivating case — the $2k "under-count" was just the local picker's narrow period.) tsc+lint clean.
  • 2026-06-17 Phase 2b DONE (owner folded the as-of follow-up INTO 2b; guiding rule: single source of truth).
  • 2b(1) as-of (origin/nightly f117f556): TB/BS/AR now compute AS OF the selected period end. Threaded asOf through useReports hooks + query keys; AccountingApp passes period.endIso (All time → undefined = now); the reports API parses a date-only asOf as HK end-of-day to match the WPP boundary byte-for-byte (one derivation). TB/BS CSV "As of"/filename now use the real as-of. NO server report-logic change — TB/BS already derive from getDerivedJournalEntriesServer bounded by asOf; AR was as-of since T-043. (Server + reports API already supported asOf — the gap was purely the client never sending it.)
  • 2b(2) CFS/SOCIE/WPP (origin/nightly 5c1d2eac): all three consume useAccountingPeriod(); pickers removed. WPP dropped its parallel ERL_CALENDAR computation; Download disabled on All-time (route needs bounded dates); stale "JSON mirror" blurb fixed. tsc+lint clean throughout. Couldn't browser-verify (auth-gated) — eyeball on nightly preview. Phase 3 next: RBAC-gated Fiscal-Year config tab (edit entityCalendar; reuse accounting:config; live period-preview + contiguity / one-year-end-per-year / closed-period validation).
  • 2026-06-17 Phase 3 DONE → T-058 COMPLETE (origin/nightly fcc56eb4): new Admin-layer "Fiscal Year" tab (components/accounting/FiscalYearConfig.tsx) — validated entityCalendar editor (incorporation / year-end MM-DD / explicit period-ends) with a LIVE listBasisPeriods preview; RBAC-gated on accounting:config (read-only otherwise; server PATCH enforces via T-057); save behind a Popconfirm re: closed/filed months. Full suite 325/325, tsc+lint clean. All phases on origin/nightly, NOT main. Open follow-ups (not blocking): (a) the Settings modal still has the old flat entityCalendar fields — redundant UI writing the same store; dedup to the new tab for true single-source. (b) Closed-period EDIT is a soft Popconfirm warning, not a hard block (a hard guard needs the closed-periods list). (c) Owner Q: the global selector currently governs BOTH the Layered AND Tabs views (the report components are shared) — owner only intended Layered; pending their call on whether to keep it unified (recommended) or split.