Files
ERP-core/docs/21-GENERAL-LEDGER-FRONTEND.md
T
HarithaRandunu 22657f0910 feat: Implement new General Ledger frontend section with comprehensive report screens and cash/bank account management
- Added a new Ledgers sidebar section for statutory-format financial reports and cash/bank-account management.
- Introduced dedicated GL client for API interactions, handling response envelopes and error management.
- Developed report screens for Trial Balance, Balance Sheet, General Ledger, Profit & Loss, Cash Flow, Budget vs Actual, and a new Tax Report.
- Implemented CSV download functionality alongside existing PDF downloads for all report screens.
- Separated Cash and Bank accounts into distinct tables/endpoints, with updated create forms and unified list view.
- Created a new Accounts section for Cheque Management, moving Cash/Bank Accounts from the Ledgers section.
- Updated RBAC navigation to include new permissions and sub-navigation items for the added features.
- Ensured compliance with GL's updated API contract, including renaming fields and adjusting response shapes.
- Addressed various bugs and presentation issues, enhancing user experience across the new module.
2026-07-31 18:02:03 +05:30

41 KiB
Raw Blame History

21 · GENERAL LEDGER — Frontend (Ledgers section)

Navigation: you arrived from 00-CORE.md. The transport this section calls through is docs/12-GENERAL-LEDGER-INTEGRATION.md (ERPCore's /api/v1/gl/* proxy into the external General Ledger service — that doc's new §7 has a curated endpoint reference for everything this page uses). General frontend architecture/validation rules are 20-FRONTEND.md — this doc only adds what's specific to the Ledgers screens. Record work in Frontend/PROGRESS.md. GL's own endpoint contract (exact request/response shapes, error cases) lives in the General Ledger service's own repo (04_API_Reference_And_Scenarios.md) — not duplicated here in full, per 01-DOC-GUIDE.md §5's single-source-of-truth rule; docs/12 §7 is a summary for quick reference, that doc is the canonical detail.


0. Major update, 2026-07-22 — the GL backend changed substantially since this section was last built

Nothing in this pass has been implemented against the frontend codebase yet. The 2026-07-20 build (§6's history) reflects an earlier, now-superseded GL contract. This revision describes the target design against the confirmed, live-verified-on-GL's-side contract as of 2026-07-22 — every section below should be read as "what to build," not "what exists," until §6 says otherwise.

Three things changed on GL's side, in order of how much they affect this doc:

  1. Five reports got real structural rework (not just field renames) — Trial Balance flattened, Profit & Loss restructured into named sections, Cash Flow's display simplified, Tax Report completely redesigned into an Income Tax Computation (previously a VAT/WHT/NBT summary), plus a new outputFormat=Csv on all seven reports.
  2. Cash and Bank accounts are two separate GL tables/endpoints now, not one table with a type field — POST /bank-accounts (unchanged) and a new POST /cash-accounts, unified for reading via GET /bank-accounts?accountType=Cash|Bank|Both.
  3. CLAUDE.md Rule 8.2 renamed numeric ID fields to business codes across most of GL's API — the one that reaches this page: GeneralLedger's accountId param is now accountCode (still optional, same two-mode behavior as before), and the Bank/Cash create forms' glAccountId is now glAccountCode.

1. What this is

A new Ledgers sidebar section (app/dashboard/ledgers/*) giving the frontend statutory-format financial reports and cash/bank-account management, sourced entirely from the external General Ledger service via ERPCore's generic proxy (docs/12-GENERAL-LEDGER-INTEGRATION.md). Every screen calls GET /api/v1/gl/reports or /gl/bank-accounts//gl/cash-accounts — there is no ERPCore business logic behind any of it yet (that internal wiring, e.g. posting real journal entries from GRN/adjustments, is tracked separately and deliberately deferred, see docs/12 §6).

Sidebar structure (components/Layouts/AppSidebar.tsx, mirrored server-side in Backend/ERPCore/Infra/Persistence/Configurations/{NavItem,SubNavItem,Permission}Configuration.cs per the existing RBAC-nav convention, docs/10 C.8/C.9) — gains one entry, Tax Report:

Ledgers
├── Trial Balance          /dashboard/ledgers/trial-balance
├── Balance Sheet          /dashboard/ledgers/balance-sheet
├── General Ledger         /dashboard/ledgers/general-ledger
├── Profit & Loss          /dashboard/ledgers/profit-and-loss
├── Cash Flow              /dashboard/ledgers/cash-flow
├── Budget vs Actual       /dashboard/ledgers/budget-vs-actual
├── Tax Report             /dashboard/ledgers/tax-report          (new)
└── Cash / Bank Accounts   /dashboard/ledgers/bank-accounts (+ /new)

/dashboard/ledgers itself is a card-grid hub, same pattern as /dashboard/stock — gains an 8th card.


2. Transport: a dedicated GL client, not lib/api-client.ts

lib/api/general-ledger.ts is a separate fetch client from the one every other screen in this app uses (lib/api-client.ts's apiRequest/apiRequestWithETag). Reason: those assume ERPCore's own RFC 7807 ProblemDetails error shape and a bare-DTO success body. The GL service wraps every response — success and error alike — in its own { statusCode, success, message, data } envelope, and (a documented GL quirk, carried through the proxy unchanged per docs/12 §4) success bodies are camelCase while error bodies are PascalCase. glRequest() unwraps both casings itself and throws a GlApiError shaped { status, detail } — duck-type compatible with lib/error-map.ts's ApiErrorLike, so errorMessage()/toasts work unchanged across both API surfaces.

All calls are relative to /api/v1/gl/* — same-origin through the existing Next rewrites() proxy (next.config.ts) → ERPCore → the GL service. No new proxy config was needed, and still isn't for any of this revision's changes — ERPCore's GeneralLedgerController (docs/12) is a byte-for-byte passthrough that never inspects GL's own field names or report contents, so every rename/restructure described in this doc requires zero ERPCore-side change, only frontend-side (docs/12 §4's new note on this).

No ETag/If-Match anywhere in this module — the GL service's own reference documents no concurrency tokens on any of the endpoints this UI calls.


3. Reports: "Sri Lankan Standard" report UI

Every report screen (app/dashboard/ledgers/{trial-balance,balance-sheet,general-ledger,profit-and-loss,cash-flow,budget-vs-actual,tax-report}/page.tsx) shares three building blocks:

  • components/reports/ReportHeader.tsx — a centered statutory header block: small-caps "General Ledger" eyebrow, the report's LKAS-aligned statement name (not always GL's own reportType value — see the mapping below), the as-at/for-period line, and a currency note. This is the "Sri Lankan Standard report UI" requested: the on-screen equivalent of the formal company-financial-statement layout (title block, period line, right-aligned money columns, bold/indented subtotal rows), not a bespoke ad-hoc table per screen. The Tax Report screen does not use this component — see its own entry below, it needs a fuller identity block GL's own PDF gives that report alone.
  • components/reports/DownloadPdfButton.tsx — re-issues the exact same report call with outputFormat=Pdf instead of Json, base64-decodes data.contentBase64 client-side into a Blob, and triggers a browser download.
  • components/reports/DownloadCsvButton.tsx (new) — identical mechanism, outputFormat=Csv instead, Blob typed text/csv, triggers a .csv download. Sits next to the PDF button on every screen — two download buttons now, not one. No proxy/transport concern either (docs/12 §7) — same passthrough as Pdf always was.
Screen reportType On-screen title Required params Response shape
Trial Balance TrialBalance Trial Balance asOfDate Flat array {accountCode, accountName, debit, credit} — no depth anymore
Balance Sheet BalanceSheet Statement of Financial Position asOfDate Unchanged — hierarchical array {depth, lineItem, accountType, balance}, always ends with a synthetic "Current Year Earnings" row under Equity
General Ledger GeneralLedger General Ledger periodStart, periodEnd (accountCode optional — renamed from accountId, see below) Unchanged shape
Profit & Loss ProfitAndLoss Statement of Profit or Loss periodStart, periodEnd Nested object, not a flat array — named sections, see below
Cash Flow CashFlow Statement of Cash Flows periodStart, periodEnd Nested object; workingCapitalChanges[] gained direction; openingCashBalance/closingCashBalance still returned, not displayed
Budget vs Actual BudgetVsActual Budget vs Actual budgetId Unchanged
Tax Report (new) TaxSummary Income Tax Computation periodStart, periodEnd; optional allowableDeductions, otherTaxableIncome, qualifyingPaymentsReliefs, surchargeAmount, taxRateOverride Single flat object, see below

Defaults, so a screen is never blank on first load: asOfDate/periodEnd default to today, periodStart to the 1st of the current month (lib/format.ts's todayIso/startOfMonthIso) — these params are required by GL (400 if missing), so the UI always sends something sensible rather than erroring on mount. The Tax Report's five optional parameters get no forced client-side default — an untouched field sends nothing, letting GL's own server-side defaulting (0 for four of them, system_config-driven for qualifyingPaymentsReliefs/the tax rate) be the single source of truth for what "not supplied" means.

reportType/outputFormat are TS enums (types/general-ledger.ts's ReportType, ReportOutputFormat) — ReportOutputFormat gains a Csv member. GlAccount.accountTypeId stays a GlAccountTypeId enum (Asset=1Expense=5).

General Ledger: accountId renamed to accountCode (GL's CLAUDE.md Rule 8.2), behavior unchanged from the 2026-07-20 correction. GL's own reference still documents two modes: accountCode supplied → "Account Ledger" (one account + its descendants, one running balance); accountCode omitted → the true General Ledger, every is_postable account's own transactions together, each with its own running balance that resets whenever the account changes, sorted by accountCode then entryDate. This screen still always calls the second mode — reportsApi.generalLedger(periodStart, periodEnd) never sends accountCode — and has no account picker/input, same as before; only the underlying param name the client would use if a single-account mode were ever added changed, not this screen's own behavior.

Amount formatting (lib/format.ts): comma-grouped thousands + fixed 2 decimals + parentheses for negatives (formatAmount) — standard financial-statement convention. CSV export does not reuse this formatter — GL's own CSV cells are plain decimals with a leading minus sign, no thousands separator, no parentheses (spreadsheet-numeric-parsing convention, not human-display convention). DownloadCsvButton downloads GL's bytes unmodified, same "don't reshape what GL sent" posture as the PDF button.

Hierarchical rows: Balance Sheet still carries depth, rendered with depth-proportional left padding. Trial Balance dropped this entirely — now a plain flat list, same row-component style as Budget vs Actual.

Profit & Loss is a nested object, not a flat array. GL's response has named sections — sales, costOfSales, otherIncome, distributionExpenses, administrationExpenses, otherExpenses, financialExpenses, unclassified (only present with lines if the COA has untagged Income/Expense accounts — GL flags this as worth watching for during setup) — each { lines[], total }, plus top-level grossProfit/netProfitForPeriod. The screen renders one bordered section per non-empty group, in this fixed order: Sales, Cost of Sales, Gross Profit (its own bold subtotal row, not a section), Other Income, the four expense groups, unclassified last if present, then Net Profit for the Period.

Cash Flow is a structured statement layout, not four StatCards. Net Earnings as its own line, an "Additions to Cash" bordered section and a "Subtractions From Cash" bordered section — client-side bucketed by sign from the combined nonCashAdjustments[] + workingCapitalChanges[] (a working-capital line's label uses its direction field, e.g. direction: "Decrease" + accountName: "Trade Receivables""Decrease in Trade Receivables"; a non-cash-adjustment line just prints its plain description, e.g. "Depreciation", no Increase/Decrease prefix) — then Net Cash From Operations as a subtotal, Investing/Financing sections (their lines[], no subtotal row when a section has only one line), ending in the final combined total. openingCashBalance/closingCashBalance are fetched (still in the response) but not rendered anywhere on screen — same "computed but not displayed" posture GL's own PDF/CSV takes.

Tax Report (app/dashboard/ledgers/tax-report/page.tsx) is an entirely new screen. Layout:

  • A periodStart/periodEnd date-range picker, same component as every other period-based report.
  • A collapsible "Adjustments" panel, collapsed by default, with five optional numeric inputs — Allowable Deductions, Other Taxable Income, Qualifying Payments / Reliefs, Surcharge / Education Levy, and Tax Rate Override (%). Each maps directly to the report call's optional query params.
  • Result: a two-column Description/Amount table in GL's own fixed order (profitBeforeTax through balanceTaxPayable) — Add:/Less: prefixes are literal row labels the component renders per a fixed lookup, not derived from the amount's sign. Bold rows for the reconciliation checkpoints (Adjusted Business Profit, Assessable Income, Taxable Income, Gross Tax Liability, the final total).
  • The final row's label depends on the sign of balanceTaxPayable — GL's own PDF renders a negative value (credits/payments exceeded the gross liability) as "BALANCE TAX REFUNDABLE", not a negative "payable" figure (confirmed live on GL's own seeded loss-making test data, 03_Progress_Tracker.md). The on-screen table should match this exactly: balanceTaxPayable >= 0 → label "BALANCE TAX PAYABLE", amount as-is; balanceTaxPayable < 0 → label "BALANCE TAX REFUNDABLE", amount shown as its absolute value (not in parentheses — GL's own PDF changes the label, not the sign display, for this one row specifically).
  • Uses its own header block, not the shared ReportHeader — GL's own PDF gives this one report a fuller identity block (company name, address, TIN, BRN) via Company:Address/Tin/Brn config values (05_LKAS_Report_PDF_Templates.md §0/§8, confirmed implemented server-side). Open question, genuinely unresolved: GL exposes no endpoint returning these four values as data — they're appsettings.json entries, read only by the PDF renderer. The on-screen React version needs the same values from somewhere, and there's no way to fetch them from GL today. Two options, neither picked here: (a) GL adds a small GET /company-info-style endpoint; (b) the frontend duplicates these four values in its own config/env, accepting the risk of drifting out of sync with GL's appsettings.json whenever one side changes without the other. Flag this for a decision before building this screen's header — don't silently pick one.

4. Cash / Bank Accounts

app/dashboard/ledgers/bank-accounts/page.tsx (list) + /new/page.tsx (create). No delete — GL's own reference has no delete endpoint for either concept, consistent with every other master in this system (FR-MD-08's deactivate-not-delete posture, though GL doesn't even expose deactivate here).

Rework — Cash and Bank are separate GL tables/endpoints, not one table with a type field. An earlier design (drafted, never shipped on GL's side) proposed a single bank_account.accountType discriminator; GL's team judged a cash account a peer concept to a bank account, not a kind of bank account, and built two separate tables instead (Accounting_System_Design_LKAS.md §5.8). This still satisfies the original requirement — the create page only ever offers exactly two choices, Cash or Bank, nothing else — the two-choice constraint now lives in "which endpoint does the form submit to," not "which value of a discriminator field."

The three endpoints this page now uses

  • POST /bank-accounts — unchanged in shape from the original 2026-07-18 design, except glAccountIdglAccountCode (Rule 8.2). Fields: accountName (required), bankName/ accountNumber (both optional/nullable — a Bank account can genuinely be created with these blank and filled in later, unlike Cash's stricter rule below), glAccountCode (required), currencyCode (optional, defaults "LKR").
  • POST /cash-accounts (new)accountName (required), cashAccountTypeName (required — matched case-insensitively against GL's cash_account_type reference table; a name with no existing match creates a new type on the fly, so a frontend "Other, please specify" free-text option becomes a permanent selectable choice for every future cash account created after it), accountNumber (optional — if omitted, GL auto-generates a sequential CASH-000001-style reference; if supplied, used as entered), glAccountCode (required), currencyCode (optional, defaults "LKR").
  • GET /cash-account-types (new) — flat, unfiltered list, seeded with Petty Cash / Till Cash / Safe Cash / Cash in Transit, growing over time. Feeds the create form's Cash Account Type dropdown.

Create form

A two-choice toggle at the top — Cash or Bank, backed by a CashBankAccountType TS enum (Cash/Bank) — decides which of the two fieldsets below shows and which endpoint the form submits to:

  • Bank selected: Bank Name + Account Number fields (both optional on this side — GL doesn't enforce them as required for a Bank row), submits to POST /bank-accounts.
  • Cash selected: a Cash Account Type <Select> fed by GET /cash-account-types, with an "Other, please specify" option that reveals a free-text input — its value is sent as cashAccountTypeName regardless of whether it matched an existing type or not, letting GL's own on-the-fly-creation handle the rest (no separate "create type first, then create account" round trip needed on the frontend's part). An optional Account Number field, with placeholder text noting it'll be auto-generated if left blank. Submits to POST /cash-accounts.
  • glAccountCode (required regardless of type, references GL's own Chart of Accounts) stays a code — name picker fed by glAccountsApi.list() (GET /accounts) either way — both kinds still need a linked GL account, nothing about that requirement differs by type.

List page — now a unified, filterable view

GET /bank-accounts?accountType=Cash|Bank|Both (default Both) replaces the old "flat, unfiltered, unpaginated bank-accounts-only" list — it's GL's own server-side union of both tables now, not a client-side-only filter over one table. Response per row: { accountType, accountId, accountName, bankName, cashAccountTypeName, accountNumber, glAccountId, currencyCode, createdAt }bankName is null on Cash rows, cashAccountTypeName is null on Bank rows. The list table should show an accountType badge/column per row (a small "Cash"/"Bank" pill next to the account name) so the two kinds are visually distinguishable at a glance. Client-side filters (name/bank-or-type/account-number/currency/linked-GL-code) still layer on top of whichever server-side accountType filter is active — GL's own filter narrows which table(s) contribute rows; everything else about the existing client-side filtering approach is unchanged.

Known gap — edit, still unresolved

The user-facing requirement was list → create → edit (no delete). Edit still cannot be built. GL's own reference documents no GET/PUT by id for either bank_account or cash_account — nothing to edit against, for either kind. The list page shows a disabled Edit button with a tooltip explaining the gap, same posture as before this rework. This needs a GL service change (add the missing endpoints, for both tables now, not just one) before a real edit screen can exist.

Known gap — response shape, now resolved for Bank, still open for the unified list's exact field names

GL's reference previously never showed a full bank-account response body (only the create request fields), so types/general-ledger.ts's old BankAccount interface was an inferred shape. This is now resolved for the unified list — GET /bank-accounts's response fields are explicitly documented (above) — but double-check the exact TS interface against a live call before trusting it blindly; inferred-shape mistakes have been a real, repeated source of drift in this module (§6's history).

Fixed — selected GL account showed as a raw number, not its name (2026-07-20, unaffected by this rework)

The <Select> picker (bank account form, and Budget vs Actual's budget picker) displayed the selected item's numeric value after picking it, even though the correct id/code was genuinely being sent to the server. Cause: this app's <Select> wraps @base-ui/react/select, whose Select.Value resolves its displayed text from each Select.Item's label prop, a separate field from children. Fixed by passing label={...} explicitly on each <SelectItem>. Not fixed at the shared components/ui/select.tsx level — flagged for whoever next touches a similar picker elsewhere in the app. The new Cash Account Type picker (this rework) should apply the same label={...} pattern from the start, not reintroduce the bug.


4a. Accounts section (2026-07-31) — Cheque Management (new module) + Cash/Bank Accounts moved here

A new top-level sidebar item, "Accounts" (app/dashboard/accounts/*), sitting alongside "Ledgers" rather than under it — Cash/Bank Accounts and cheques are operational account bookkeeping, not a statutory report, so they don't belong under the Ledgers report hub. Cash / Bank Accounts moved here verbatim from ledgers/bank-accounts (§4 above) — same code, same behavior, just relocated; the Ledgers hub's card grid dropped its Cash/Bank Accounts card accordingly.

Cheque Management is a new GL module (04_API_Reference_And_Scenarios.md, Module: Cheque Management — added to GL 2026-07-30, beyond its original plan), purely operational tracking: no endpoint in it ever creates or touches a journal entry itself. Two independent sub-areas, per GL's own spec:

  • Cheque Books/Pages (app/dashboard/accounts/cheque-books/*) — cheques issued from this company's own cheque book supply. POST /cheque-books auto-generates every leaf (ChequePage, all Unused) in one call. List page filters by status; create form restricts the bank-account picker to Bank-type accounts only (GL's own module note: cheque books are bank-account-only, never cash-account). Clicking a book navigates to [chequeBookNo]/page.tsx, which fetches GET /cheque-books/{chequeBookNo}?expand=pages and lists every leaf — clicking a leaf opens components/accounts/ChequePageDialog.tsx.
  • Received Cheques (app/dashboard/accounts/received-cheques/*) — cheques received from customers/suppliers/others, deliberately not linked to any ChequeBook. Same list-then-modal shape via components/accounts/ReceivedChequeDialog.tsx.

Modal chosen over a second-level page for leaf/row details (left open by the request) — working through several cheque pages in one book, or several received cheques in the list, without losing the list's scroll position/filter state each time. Each dialog shows read-only details plus only the actions valid from the item's current status (per GL's own status-transition tables), and each action reveals only the extra fields that specific transition needs — e.g. Clear asks for clearedDate, Cancel asks for cancelReason, Bounce/Void/Deposit(+bank picker) need nothing else beyond an optional performedBy/note. This mirrors GL's own validation exactly rather than showing one giant always-visible form.

branchId/companyId/payeeId/voucherId/referenceId are plain numeric inputs, not dropdowns — GL's reference explicitly documents these as "loose references" (no Branch/Company/ Customer/Supplier table exists in that service for them to point at), so building a picker against non-existent master data would be worse than a labeled numeric field.

Types built from request-body fields GL does document, not guessed wholesaleChequeBook/ ChequePage/ReceivedCheque in types/general-ledger.ts are assembled from every field named in the POST/PUT request bodies plus the status fields GL's prose confirms (issueStatus/ printedAt/clearedDate/clearedByBank/cancelReason, etc.) — not a shape invented from nothing. Two things genuinely are guesses, flagged in the type comments: ChequeBook/ChequePage's internal numeric PK column names (GL never states them — chequeBookNo/chequeNo, both explicitly documented as the identifying route values, are used for every key/URL instead, sidestepping the guess), and ReceivedCheque's JSON id field name (receivedChequeId, inferred from this project's own <entity>Id convention — GL's reference only shows a numeric {id} in the URL, not the field name).

Backend: migration AddAccountsNavSeed — new NavItem accounts (id 12) and two new SubNavItem/Permission pairs (accounts.cheque-books, accounts.received-cheques); the existing Cash/Bank Accounts SubNavItem/Permission (ids 15/26) were updated in place, not deleted and recreated, so a role already granted that permission under its old ledgers.bank-accounts code keeps it. Applied to the live database this session.

Not done — live smoke test. This entire module (both sub-areas) is unverified against real Cheque Management data — no running GL instance with cheque data was available this session. Same "verify before trusting the inferred pieces" posture as the rest of §6's history.


5. RBAC nav seed (backend, small, additive)

The sidebar's role-based visibility (docs/10 C.8, GET /auth/me's navCodes) requires every AppSidebar.tsx entry's code to have a matching backend NavItem/SubNavItem row, or the item is invisible to every role regardless of the frontend change. Migration AddLedgersNavSeed, Backend/ERPCore/Infra/Persistence/Migrations:

  • NavItem ledgers (id 11) + 8 SubNavItem rows (ledgers.trial-balanceledgers.tax-report (new, id 16) … ledgers.bank-accounts, ids 916) — NavItemConfiguration.cs/SubNavItemConfiguration.cs.
  • 9 matching Permission rows (NAV:ledgers, NAV:ledgers.*, ids 1927, NAV:ledgers.tax-report new at id 27) — PermissionConfiguration.cs, following the existing one-Permission-per-nav-entry convention exactly.

Operational step still needed, not code: a brand-new NavItem/SubNavItem carries no RolePermission grants by default. An administrator must open Settings → Roles, edit the relevant role(s), and check the new Ledgers permissions (including the new Tax Report one) before anyone with that role sees the sidebar entries — normal onboarding, not a bug.


6. Progress

  • Reports, original 6 screens (2026-07-20): all 6 report screens + hub, all calling the live GL proxy, all with a working PDF download. Verified tsc --noEmit/eslint clean.
  • Cash/Bank Accounts — list + create, original single-table design (2026-07-20): client-side-filtered list, create form with a GL-account picker.
  • RBAC nav seed, original 7-entry version (2026-07-20): migration AddLedgersNavSeed, not yet applied to a live database.
  • Corrections (2026-07-20): General Ledger dropped accountId entirely (later renamed to accountCode, see below), ReportType/ReportOutputFormat/GlAccountTypeId became TS enums, fixed the Select-shows-raw-number bug on two pickers.
  • This revision, built (2026-07-30). Every item in §3/§4's rework is now implemented against the frontend codebase:
    • components/reports/DownloadCsvButton.tsx — new, added to all seven report screens (alongside the existing DownloadPdfButton).
    • components/reports/ReportSection.tsx/ReportSubtotal.tsx — new shared components, extracted because Profit & Loss and Cash Flow both needed the identical "bordered section of lines + a bold subtotal row" shape; not built as one-off per-screen markup.
    • Trial Balance screen — flat-list renderer, depth/indentedCode dropped.
    • Profit & Loss screen — full rework to the nested-sections object, fixed section order (Sales → Cost of Sales → Gross Profit → Other Income → the four expense groups → Unclassified if present → Net Profit for the Period), empty sections hidden.
    • Cash Flow screen — replaced the four StatCards with the structured statement layout: Net Earnings, Additions/Subtractions to Cash (sign-bucketed from the combined nonCashAdjustments[]/workingCapitalChanges[], working-capital lines labeled via their direction), Net Cash From Operations, Investing/Financing sections (subtotal row suppressed when a section has exactly one line, per spec), final combined total. openingCashBalance/closingCashBalance are fetched but deliberately not rendered.
    • New Tax Report screen (app/dashboard/ledgers/tax-report/page.tsx) — period picker, collapsible Adjustments panel (5 optional numeric inputs, no client-side default), fixed-order two-column result table with Add:/Less: row labels from a static lookup, bold reconciliation checkpoints, and the payable/refundable label+sign logic on the final row. Uses its own header, not the shared ReportHeader — the open question about Company:Address/Tin/Brn (§3, end) is still unresolved; the header explicitly states the company-identity fields aren't shown rather than fabricating placeholder values, and a hub card was added.
    • types/general-ledger.tsReportOutputFormat gained Csv; ReportType gained TaxSummary; GeneralLedger's (unused-by-this-screen) param renamed accountCode; new CashBankAccountType enum; TrialBalanceRow flattened; ProfitAndLossResponse/CashFlowResponse/TaxSummaryResponse added (all three inferred in places GL's own reference doesn't spell out every field verbatim — flagged in the type file's own doc comments, same posture as the original inferred BankAccount shape); BankAccount replaced by the unified CashAndBankAccountDto plus separate CreateBankAccountRequest/CreateCashAccountRequest types.
    • Cash/Bank create form — Bank/Cash toggle (two buttons, not a dropdown), conditional fieldsets, Cash Account Type <Select> (with label={...} set correctly from the start, avoiding the raw-number-display bug from §4's earlier fix) fed by GET /cash-account-types, an "Other, please specify" free-text option, glAccountCode picker now keyed by the account's code string instead of its numeric id.
    • Cash/Bank list page — server-side accountType filter (Both/Bank/Cash, a <Select> next to the existing search box) driving GET /bank-accounts?accountType=..., client-side text search still layered on top, a Cash/Bank badge column, Edit still disabled with its explanatory tooltip (now type-aware: "...for cash accounts"/"...for bank accounts").
    • RBAC nav seed — migration AddTaxReportNavSeed adds SubNavItem id 16 (ledgers.tax-report) and Permission id 27, and re-sequences ledgers.bank-accounts's SortOrder from 7→8 so Tax Report sits before it, matching the sidebar's array order.
    • Verified: tsc --noEmit clean, npx eslint clean across every touched file, npm run build succeeds with all 9 /dashboard/ledgers/* routes present (including /tax-report), dotnet build clean (0 warnings/0 errors) after the migration.
  • Live smoke test, partial (2026-07-31) — Cash Flow crashed, now fixed. A GL instance became reachable and a user hit the exact risk flagged above: Cash Flow threw TypeError: Cannot read properties of undefined (reading 'map') on report.nonCashAdjustments.map. Confirmed root cause — GL's serializer omits an empty/zero list or section field from the JSON entirely, rather than sending [] or { lines: [], total: 0 }. Fixed in cash-flow/page.tsx (?? [] defaults, an activitySectionLines() helper, optional chaining on investingActivities/financingActivities/their .total) and proactively in profit-and-loss/page.tsx (same defensive treatment for every section, since ProfitAndLossResponse shares the identical nested-section shape and hadn't crashed yet only because no period tested so far happened to have an empty section). types/general-ledger.ts's CashFlowResponse and ProfitAndLossResponse fields changed from required to optional to match. TaxSummaryResponse is still unverified against this same risk — it wasn't touched this pass; treat it as equally likely to have optional/omittable fields until proven otherwise. The unified CashAndBankAccountDto also remains unverified live.
  • Corrected against GL's own API reference (2026-07-31 (2)) — Cash Flow's shape was wrong, not just fragile; Tax Report was missing five fields. The user supplied GL's own 04_API_Reference_And_Scenarios.md, letting every report be checked against a confirmed contract instead of inference for the first time. Trial Balance, Balance Sheet, General Ledger, Profit & Loss, and Budget vs Actual all match it exactly. Two didn't:
    • CashFlowResponse — everything nests under operatingActivities. There is no top-level netEarnings/nonCashAdjustments/workingCapitalChanges/netCashFromOperations at all; the real shape is operatingActivities: { profitForPeriod, nonCashAdjustments[], workingCapitalChanges[], netCashFromOperatingActivities }, and investingActivities/ financingActivities each have their own differently-named total (netCashFromInvestingActivities/netCashFromFinancingActivities), not a shared total. This was the actual cause of the crash in the entry above — the previous fix's ?? [] guards were aimed at fields that never existed at that nesting level, so the page would have kept silently rendering an empty Operating Activities section forever, crash or not. Also: workingCapitalChanges[] entries use changeAmount, not amount. Rewrote cash-flow/page.tsx and types/general-ledger.ts (CashFlowResponse plus new CashFlowOperatingActivities/ CashFlowInvestingActivities/CashFlowFinancingActivities) to match exactly.
    • TaxSummaryResponse was missing nonDeductibleExpenses, corporateIncomeTax, apitCredit, whtCredit, quarterlyTaxPayments entirely — real GL-computed figures silently never shown, not a wrong guess at a name. Added all five to the type and to tax-report/page.tsx's ROWS table in their correct position in the confirmed order.
    • CashFlowActivityLine's own field names ({description, amount} inside investingActivities/ financingActivities lines[]) and CashAndBankAccountDto remain unverified — GL's reference confirms the containing shapes but not these inner fields verbatim.
    • Verified: tsc --noEmit/eslint clean on every touched file.
  • Balance Sheet regrouped into a proper LKAS Statement of Financial Position layout (2026-07-31 (3), user-reported). BalanceSheetRow's shape is correct (matches the reference above exactly) — this was a presentation bug, not a data bug. The flat one-table rendering made a rollup total visually indistinguishable from the leaf amounts it already sums (e.g. "Cash and Bank"'s balance already includes "Petty Cash"/"Main Operating Bank Account"/"Savings Bank Account" beneath it), only depth===0 did any bolding, and there was no section grouping at all — a "Type" column per row instead of an ASSETS/LIABILITIES/EQUITY structure. Rewrote balance-sheet/page.tsx to group by accountType into sections, each with a bold "Total {Section}" row (summed from that section's depth-0 rows only, since a depth-0 row's balance already rolls up its descendants), any row followed immediately by a deeper row is bolded as a rollup (not just the top level), and a final "Total Liabilities and Equity" row for the standard balance check. Handles one GL quirk explicitly: the synthetic "Current Year Earnings" balancing row always carries depth: 1 even though it's a peer Equity entry, not a child of whatever precedes it — a new effectiveDepth() helper special-cases it to 0. Verified against the actual reported numbers: Total Assets (5,880,466.50) = Total Liabilities (2,025,000.00) + Total Equity (3,855,466.50), exact match. tsc --noEmit/eslint clean.
  • Superseded by a real GL retrofit (2026-07-31 (4)) — BalanceSheet is now a pre-classified nested shape, not a flat array to regroup client-side. GL's own API reference documents a 2026-07-31 backend change: the flat {depth, lineItem, accountType, balance} recursive-rollup array (what the entry above regrouped client-side) is replaced by { asOfDate, nonCurrentAssets: {lines[], total}, currentAssets: {lines[], total}, unclassifiedAssets: {lines[], total}, totalAssets, equity: {lines[], total}, nonCurrentLiabilities: {lines[], total}, currentLiabilities: {lines[], total}, unclassifiedLiabilities: {lines[], total}, totalEquityAndLiabilities }, driven by GL's new accounts.balance_sheet_classification tag. GL now does the Non-Current/Current classification itself — the previous entry's client-side effectiveDepth/sectionTotal/depth-based rollup logic is entirely obsolete. Replaced BalanceSheetRow with BalanceSheetLine/ BalanceSheetSection/BalanceSheetResponse (every section optional, same defensive posture as CashFlowResponse/ProfitAndLossResponse after the Cash Flow crash, since this shape isn't live-verified against this frontend yet) and rewrote balance-sheet/page.tsx from scratch. Also restyled to match a user-supplied reference Statement of Financial Position image — a real classified SOFP, Non-Current/Current Assets each their own subtotaled block, then Equity and Liabilities the same way, ending in a Total Assets vs Total Equity-and-Liabilities check — by reusing the same ReportSection/ReportSubtotal components Profit & Loss and Cash Flow already use, rather than one-off markup; line items show plain account names only, no codes, matching the reference. Verified: tsc --noEmit/eslint clean; confirmed no remaining references to the removed BalanceSheetRow.
  • New "Accounts" nav section built (2026-07-31 (5)) — Cheque Management (Cheque Books/Pages + Received Cheques) plus Cash/Bank Accounts moved here from Ledgers. Full detail: §4a above. Six new screens (cheque-books list/create/detail, received-cheques list/create) plus two new modal components (ChequePageDialog/ReceivedChequeDialog), new types/API client functions for the whole Cheque Management module, migration AddAccountsNavSeed (applied live). Verified: tsc --noEmit/eslint clean, npm run build compiles successfully. Not done — live smoke test, this module's inferred id-field-name gaps are flagged in §4a.
  • glAccountCode removed from Cash/Bank Account creation (2026-07-31 (6)), matching a further GL retrofit. GL's own reference now documents that POST /bank-accounts/POST /cash-accounts no longer accept a glAccountCode at all — the backing GL account (and, for Cash, its type-header node) is always auto-created server-side, never caller-selected. Removed the field from CreateBankAccountRequest/CreateCashAccountRequest, deleted the "GL account" Select and its glAccountsApi.list() fetch from bank-accounts/new/page.tsx entirely, and dropped the matching check from validateBankAccountForm. The create response is typed as the new CreateCashOrBankAccountResponse (glAccount nested, confirmed from the doc; other fields inferred) so the success toast can show the auto-generated GL account code back to the user. The Cash/Bank list page is unaffected — GL's unified list endpoint still returns a flat glAccountId per row, still resolved against glAccountsApi.list() there, same as before.
  • Create-form layout widened to fill the page (2026-07-31 (6)). The three GL create forms under Accounts (bank-accounts/new, cheque-books/new, received-cheques/new) each wrapped their fields in a max-w-lg card, leaving roughly half the page blank on any normal desktop width. Dropped the max-w-lg cap (now full-width, matching the report pages' own rounded-2xl bg-white p-6 ... card convention, which was never capped) and replaced the vertical one-field-per-row FieldGroup stacking (plus occasional ad-hoc grid grid-cols-2 pairs) with one consistent grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 wrapper per form, so fields actually spread across the available width instead of stretching a single narrow column. Modals (ChequePageDialog/ReceivedChequeDialog) were deliberately left at their existing fixed width — a dialog is supposed to stay narrow, this complaint was about full-page create forms only.
  • Not done — internal ERPCore→GL wiring. Unrelated to this revision, still deferred (docs/12 §6).
  • Deferred — bank/cash account edit. Needs GL service changes first (§4); now needs them for both tables, not just one.
  • Open — Tax Report's company-identity header fields. Still genuinely unresolved (§3, end); not picked in this pass either. The screen ships without them rather than guessing.

End of 21-GENERAL-LEDGER-FRONTEND.md. Transport: docs/12-GENERAL-LEDGER-INTEGRATION.md. General frontend rules: 20-FRONTEND.md. Record work: Frontend/PROGRESS.md.