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.
This commit is contained in:
+5
-1
@@ -45,7 +45,9 @@ erp-monorepo/
|
||||
├── 02-SECURITY.md # accepted-risks register + per-feature security checklist
|
||||
├── 10-BACKEND-PHASE1.md # backend spec: SRS + ER/entities + tech + architecture
|
||||
├── 11-BACKEND-PHASE1.md # backend API reference (complete req/res)
|
||||
└── 20-FRONTEND.md # frontend user-flows + architecture rules + validation posture
|
||||
├── 12-GENERAL-LEDGER-INTEGRATION.md # ERPCore ↔ external General Ledger service (transport only)
|
||||
├── 20-FRONTEND.md # frontend user-flows + architecture rules + validation posture
|
||||
└── 21-GENERAL-LEDGER-FRONTEND.md # Ledgers section: reports UI + cash/bank accounts
|
||||
```
|
||||
|
||||
The `Backend/ERPCore/` internal layout is created in §5.3.
|
||||
@@ -340,6 +342,8 @@ All frontend work is governed by `20-FRONTEND.md`.
|
||||
| API endpoints, request/response shapes, error catalog, enums (Phase 1) | **`11-BACKEND-PHASE1.md`** |
|
||||
| HRM requirements, business rules, entities, ER model (Phase 2) | **`12-BACKEND-HRM.md`** |
|
||||
| HRM API endpoints, request/response shapes, error catalog (Phase 2) | **`13-BACKEND-HRM-API.md`** |
|
||||
| Connecting to the external General Ledger service (proxy, config, API key) | **`12-GENERAL-LEDGER-INTEGRATION.md`** |
|
||||
| The Ledgers frontend section (reports, cash/bank accounts) | **`21-GENERAL-LEDGER-FRONTEND.md`** |
|
||||
| Frontend user-flows, screen flow, architecture rules, validation posture (Phase 1) | **`20-FRONTEND.md`** |
|
||||
| HRM frontend user-flows (Phase 2) | **`21-FRONTEND-HRM.md`** |
|
||||
| Manufacturing requirements, business rules, entities, stock/costing integration, API (Phase 2) | **`30-BACKEND-PHASE2.md`** |
|
||||
|
||||
@@ -38,10 +38,12 @@
|
||||
| `11-BACKEND-PHASE1.md` | High | Backend **API reference**: every endpoint with complete request/response, error catalog, enums. | Any API contract / controller / client work (Phase 1). | Claude + humans |
|
||||
| `12-BACKEND-HRM.md` | High | HRM backend spec: SRS, ER model, HRM-specific architecture notes. Schema is **authoritative** here. | Any HRM model / business-rule / requirement work (Phase 2). | Claude + humans |
|
||||
| `13-BACKEND-HRM-API.md` | High | HRM **API reference**: every endpoint, error catalog additions, enums. | Any HRM API contract / controller work. | Claude + humans |
|
||||
| `12-GENERAL-LEDGER-INTEGRATION.md` | High | ERPCore ↔ external **General Ledger service**: connection/proxy contract, config, progress. GL's own endpoint contract lives in the GL service's own repo, not here. | Any work touching the GL proxy or a future internal GL caller. | Claude + humans |
|
||||
| `20-FRONTEND.md` | High | Frontend **user-flows**, architecture rules to follow, **validation posture**. | Any frontend work (Phase 1). | Claude + humans |
|
||||
| `21-FRONTEND-HRM.md` | High | HRM frontend **user-flows**, screens, validation specifics. | Any HRM frontend work. | Claude + humans |
|
||||
| `30-BACKEND-PHASE2.md` | High | Manufacturing (Production Lines) backend: SRS, ER model, status machines, stock/costing integration, **and** the full API contract — model and API in one doc, unlike Phase 1. Schema is **authoritative** here. | Any manufacturing model / rule / API work. | Claude + humans |
|
||||
| `21-FRONTEND-PHASE2.md` | High | Manufacturing frontend **user-flows**: template canvas builder, run board, run execution screens. | Any manufacturing frontend work. | Claude + humans |
|
||||
| `21-GENERAL-LEDGER-FRONTEND.md` | High | **Ledgers** sidebar section: statutory-format report screens + PDF download, Cash/Bank Accounts (list/create; edit gap explained). Extends `20-FRONTEND.md` rather than duplicating it. | Any work on `app/dashboard/ledgers/*`. | Claude + humans |
|
||||
| `Backend/PROGRESS.md` | Low | Backend **change checklist**, git-shared. | After making backend changes. | **Claude** |
|
||||
| `Frontend/PROGRESS.md` | Low | Frontend **change checklist**, git-shared. | After making frontend changes. | **Claude** |
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# 12 · GENERAL LEDGER INTEGRATION — ERPCore ↔ External GL Service
|
||||
|
||||
> **Navigation:** you arrived from `00-CORE.md`. This doc is the **connection layer only** — the transport between ERPCore and the external General Ledger service. It is not the GL service's own API contract: every endpoint, request/response shape, and error code the GL service exposes is documented in that service's own repo (`04_API_Reference_And_Scenarios.md`, its `01_Full_Development_Plan.md`/`02_Phase_By_Phase_Development_Plan.md`/`03_Progress_Tracker.md` companions, and its Postman collection) — ERPCore does not duplicate that content here (`01-DOC-GUIDE.md` §5, single source of truth per topic).
|
||||
> **Record work:** `Backend/PROGRESS.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this is
|
||||
|
||||
The General Ledger service is a **separate ASP.NET Core microservice** — its own repo, own database, no UI, no end-user login. It is called server-to-server, authenticated with a single shared secret (`X-Api-Key`), by "the core backend" — which, from this repo's side, is ERPCore.
|
||||
|
||||
This pass connects the two systems at the **transport level only**:
|
||||
|
||||
- A generic reverse-proxy endpoint on ERPCore (`/api/v1/gl/*`) that the frontend can call, which forwards the request to the GL service and returns its response unchanged.
|
||||
- A service-layer function (`IGeneralLedgerService.ForwardAsync`) that does the actual forwarding — usable today by the controller, and **intended to be called directly by other ERPCore services later** (GRN confirm, adjustments, etc. posting real journal entries into GL instead of — or alongside — the local `JournalEntryStub`). That internal wiring is **explicitly deferred**; nothing in ERPCore calls GL yet except the proxy itself.
|
||||
|
||||
There is **no GL-specific business logic, no typed DTOs, and no per-endpoint validation** in ERPCore for this integration. Every request under `/api/v1/gl/*` is forwarded byte-for-byte — method, path, query string, request body, and `Content-Type` — and GL's response (status code, content type, body) comes back exactly as GL sent it, un-reshaped.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture
|
||||
|
||||
```
|
||||
Frontend ──► ERPCore /api/v1/gl/{**path} (ErpAccess door policy, same as every other v1 endpoint)
|
||||
│
|
||||
▼
|
||||
GeneralLedgerController (Controllers/GeneralLedgerController.cs)
|
||||
│ Request.Method, path, QueryString, ContentType, Body — untouched
|
||||
▼
|
||||
IGeneralLedgerService.ForwardAsync (Services/GeneralLedgerService.cs)
|
||||
│ the one function — controller and future internal callers both use this
|
||||
▼
|
||||
IGeneralLedgerClient.SendAsync (Infra/Gl/GeneralLedgerClient.cs)
|
||||
│ attaches X-Api-Key, streams body through, no parsing
|
||||
▼
|
||||
External GL service (GeneralLedgerService:BaseUrl)
|
||||
```
|
||||
|
||||
**Why a generic proxy and not typed endpoints:** the GL service's own reference (`04_API_Reference_And_Scenarios.md`) documents ~15 modules and 40+ endpoint variations (Accounts, Fiscal Calendar, Currency, Cost Centers, Journal Entries, Recurring Entries, Tax, Fixed Assets, Bank, Budget, Reporting, System Config, Audit Log). Modelling all of that as ERPCore DTOs before any of it is actually consumed would be premature — this pass connects the pipe; the next pass (deferred, see §6) picks specific GL calls to wire into specific ERPCore workflows and can build typed request/response DTOs for exactly those calls at that point.
|
||||
|
||||
**Layering matches the existing AuthHex integration** (`Infra/Auth/AuthHex/IAuthHexClient` → `Services/Auth/*` → `AuthController`), except AuthHex's client unwraps a known envelope into typed results, while GL's client deliberately does not — see §4.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration
|
||||
|
||||
`Backend/ERPCore/appsettings.json`:
|
||||
|
||||
```json
|
||||
"GeneralLedgerService": {
|
||||
"BaseUrl": "https://localhost:7024/api/v1/",
|
||||
"ApiKey": ""
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Purpose |
|
||||
|---|---|
|
||||
| `GeneralLedgerService:BaseUrl` | The GL service's base URL, **including** `/api/v1/` and a trailing slash (so relative paths combine correctly against `HttpClient.BaseAddress`). GL's own docs note it calls `UseHttpsRedirection()` before its API-key check runs — use the `https://` URL directly, per GL's own integration guide §3, to avoid every call taking a `307` round-trip. |
|
||||
| `GeneralLedgerService:ApiKey` | The shared secret sent as `X-Api-Key` on every forwarded call. **Ships blank** — same pattern as this project's own `ApiKey`-style secrets (02-SECURITY AR-06): get a value from the GL service's own `POST /api/v1/dev-tools/generate-api-key` (Development only) and paste it in here *and* into the GL service's own `ApiKey:Value` config — it is one shared string on both sides, not a per-caller credential. |
|
||||
|
||||
Not touched by this pass: `appsettings.Development.json` / `appsettings.Production.json` — fill in a real key locally the same way `AuthHex:BaseUrl` and other per-environment values are handled today (edit the file directly for local dev; environment-variable override for prod, per `00-CORE.md §5.4`'s secrets note).
|
||||
|
||||
---
|
||||
|
||||
## 4. The proxy contract
|
||||
|
||||
### `{GET|POST|PUT} /api/v1/gl/{**path}`
|
||||
|
||||
- **Auth:** requires a valid ERPCore session (AuthHex-issued token, `erp_at` cookie or Bearer header) satisfying the `ErpAccess` door policy — identical to every other `/api/v1/*` endpoint (`ApiControllerBase`). The GL `X-Api-Key` itself is attached **server-side only**; the frontend never sees it and cannot set it.
|
||||
- **Method:** only `GET`/`POST`/`PUT` are exposed — the GL service's own API reference has no `PATCH` or `DELETE` endpoint anywhere in its surface, so those verbs aren't wired.
|
||||
- **`{**path}`:** everything after `/api/v1/gl/` is forwarded as-is to the GL service's own `/api/v1/{path}` — e.g. a frontend call to `POST /api/v1/gl/journal-entries` reaches GL's `POST /api/v1/journal-entries`. Query string is forwarded unchanged.
|
||||
- **Body:** for `POST`/`PUT`, the raw request body (and its `Content-Type` header) is streamed straight through — this is what makes the one `multipart/form-data` endpoint (bank statement import) work through the proxy without ERPCore needing to understand multipart at all.
|
||||
- **Response:** GL's status code, `Content-Type`, and body are returned to the frontend **unchanged**. This deliberately preserves two documented GL quirks rather than papering over them:
|
||||
- GL's success bodies are camelCase (`{"statusCode":200,...}`), its error bodies (from its own exception/API-key middleware) are PascalCase (`{"StatusCode":400,...}`) — GL's own docs flag this as a known inconsistency, not fixed on their side yet. ERPCore does not normalize it; the frontend must handle both shapes (case-insensitive deserialization), same as GL's own integration guide recommends to *its* callers.
|
||||
- `outputFormat=Json` report amounts keep full decimal precision (no rounding) — since ERPCore never deserializes the body into its own model, nothing here can accidentally truncate it.
|
||||
- **This proxy needs zero code changes whenever GL's own domain contract evolves — confirmed again as of GL's 2026-07-22 revision (§7).** Five reports were restructured, `outputFormat` gained a `Csv` value, Cash/Bank accounts split into two endpoints, and numerous fields were renamed from numeric IDs to business codes (`CLAUDE.md` Rule 8.2 on GL's side) — none of it touched this file. That's the entire point of a byte-for-byte proxy over typed per-endpoint DTOs (§2's rationale): a generic passthrough only ever needs a code change when the *transport* contract changes (new HTTP verb, new content type, new auth mechanism), never when GL's own request/response payloads gain, lose, or rename fields. §7 is a curated reference of what's actually being called today, kept for the frontend team's convenience — it is documentation, not something this proxy depends on or needs updated in lockstep with.
|
||||
|
||||
### Failure modes added by ERPCore itself
|
||||
|
||||
| Case | Status | `code` |
|
||||
|---|---|---|
|
||||
| GL service unreachable (connection refused/DNS/etc.) | 503 | `GL_SERVICE_UNAVAILABLE` |
|
||||
| GL service timed out | 503 | `GL_SERVICE_UNAVAILABLE` |
|
||||
| No/invalid ERPCore session | 401/403 | (standard ERPCore door-policy response — request never reaches the proxy) |
|
||||
|
||||
Everything else — `401` for a bad `X-Api-Key` (can't happen here since ERPCore always attaches the configured key, but a misconfigured/blank key on either side would surface as this), every domain `400`/`404`/`409`/`422` GL itself returns — passes straight through with GL's own body and message, untouched.
|
||||
|
||||
---
|
||||
|
||||
## 5. What is *not* done in this pass
|
||||
|
||||
- **No internal service calls into GL.** `IGeneralLedgerService.ForwardAsync` exists and is registered in DI, but no ERPCore service (GRN, Adjustment, Transfer, PO, etc.) calls it yet. The local `JournalEntryStub` (docs/10 §C.7, FR-STK-13) is untouched and remains the only GL-adjacent artifact those flows produce today.
|
||||
- **No typed request/response DTOs** for any GL endpoint — the proxy is byte-for-byte, so nothing in ERPCore currently understands GL's schema.
|
||||
- **No path allowlist.** The proxy forwards *any* path under `/api/v1/gl/` to GL — it trusts GL's own routing/auth to reject anything invalid. Acceptable for now because the proxy is still gated by ERPCore's own door policy (only ERP-admitted users reach it at all) and GL requires its own valid `X-Api-Key` regardless, but worth revisiting alongside per-endpoint RBAC (02-SECURITY Part D) if GL exposes anything sufficiently destructive to warrant narrowing.
|
||||
- **No `appsettings.Development.json`/`appsettings.Production.json` changes** — only the base `appsettings.json` shape was added, per this pass's scope (connect the pipe, not configure every environment).
|
||||
- **No CORS/rate-limiting review specific to this proxy** — it inherits whatever ERPCore already has (currently none, same accepted gap as `AuthController`, 02-SECURITY AR-08).
|
||||
|
||||
---
|
||||
|
||||
## 6. Progress
|
||||
|
||||
- [x] **Transport wired (2026-07-20).** `Infra/Gl/{IGeneralLedgerClient,GeneralLedgerClient,GeneralLedgerResponse}` — typed `HttpClient` (`GeneralLedgerService:BaseUrl`), attaches `X-Api-Key` (`GeneralLedgerService:ApiKey`), streams request/response bodies through unparsed. `Services/{Interfaces/IGeneralLedgerService,GeneralLedgerService}` — the shared forwarding function. `Controllers/GeneralLedgerController` — `GET|POST|PUT /api/v1/gl/{**path}`, `ErpAccess`-gated like every other v1 controller. Config keys added to `appsettings.json`. Build verified clean (`dotnet build`, 0 warnings/0 errors).
|
||||
- [ ] **Live smoke test against a running GL instance** — not yet run in this pass (no GL instance available in this session). Before relying on this in anger: generate a dev API key from GL's `POST /api/v1/dev-tools/generate-api-key`, put it in both sides' config, and round-trip at least one call per HTTP verb (e.g. `GET /api/v1/gl/accounts`, `POST /api/v1/gl/accounts`, `PUT /api/v1/gl/accounts/{code}`) plus the multipart import call, to confirm the byte-for-byte passthrough holds in practice (headers, streaming, status codes).
|
||||
- [ ] **Internal wiring** — pick the first real ERPCore→GL use case (most likely candidate: posting `FifoCostingService`/`StockMutator`-driven movements as real GL journal entries instead of `JournalEntryStub` rows) and build the typed request/response DTOs + service calls for that specific flow through `IGeneralLedgerService`. Deliberately deferred per this pass's scope.
|
||||
- [ ] **Security pass once real traffic flows** — revisit the "no path allowlist" note in §5, decide whether `GeneralLedgerService:ApiKey` needs to move to a secret store before any shared/staging use (same trigger as `02-SECURITY.md` AR-06), and whether this proxy needs its own accepted-risk entry there.
|
||||
|
||||
---
|
||||
|
||||
## 7. Endpoint reference — GL endpoints currently used (or planned) by the frontend
|
||||
|
||||
**Purpose of this section:** a quick-reference summary of exactly which GL endpoints the frontend calls, for connecting new screens without re-reading GL's full `04_API_Reference_And_Scenarios.md` from scratch each time. **This is a curated summary, not the canonical contract** — for exact request/response shapes, every field, and every error case, GL's own reference stays authoritative (`01-DOC-GUIDE.md §5`). Every call below goes through this proxy at `/api/v1/gl/{path}` (drop the leading `/api/v1` from GL's own documented paths, prefix `/api/v1/gl` instead — e.g. GL's `GET /api/v1/reports` is called here as `GET /api/v1/gl/reports`).
|
||||
|
||||
As of GL's 2026-07-22 revision. Full detail for anything marked "see GL §X" is in GL's `04_API_Reference_And_Scenarios.md` at that section.
|
||||
|
||||
### Reporting (`docs/21` §3 — all seven Ledgers report screens)
|
||||
|
||||
| Method | Path | Used for | Key params |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/reports` | All seven report screens, one shared endpoint | `reportType` (`GeneralLedger`\|`TrialBalance`\|`BalanceSheet`\|`ProfitAndLoss`\|`CashFlow`\|`BudgetVsActual`\|`TaxSummary`), `outputFormat` (`Json`\|`Pdf`\|`Csv`), plus report-specific params — see GL §"GET /reports" for the full per-`reportType` table |
|
||||
|
||||
One endpoint, one row — every report screen and both download buttons (`DownloadPdfButton`/`DownloadCsvButton`) call this same path with different query parameters. Nothing else in this module hits a second reporting endpoint.
|
||||
|
||||
### Accounts (`docs/21` §3/§4 — GL-account pickers only, never a screen of its own here)
|
||||
|
||||
| Method | Path | Used for | Key params |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/accounts` | Populates the `glAccountCode` picker on the Bank/Cash create forms | none required — full list |
|
||||
|
||||
This module never creates, edits, or has a dedicated screen for GL's own Chart of Accounts — `GET /accounts` is called purely to feed a `code — name` dropdown elsewhere.
|
||||
|
||||
### Bank & Cash Accounts (`docs/21` §4)
|
||||
|
||||
| Method | Path | Used for | Key params |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/bank-accounts` | Unified Cash+Bank list page | `accountType` (optional — `Cash`\|`Bank`\|`Both`, default `Both`) |
|
||||
| `POST` | `/bank-accounts` | Create form, "Bank" selected | `accountName`, `bankName`/`accountNumber` (optional), `glAccountCode`, `currencyCode` (optional) |
|
||||
| `POST` | `/cash-accounts` | Create form, "Cash" selected | `accountName`, `cashAccountTypeName`, `accountNumber` (optional, auto-generated if omitted), `glAccountCode`, `currencyCode` (optional) |
|
||||
| `GET` | `/cash-account-types` | Populates the Cash Account Type picker on the create form | none — full list |
|
||||
| `POST` | `/bank-accounts/{id}/statement-lines/import` | Not yet built on the frontend — bank statement `.xlsx` import, `multipart/form-data` | field name `file` |
|
||||
| `PUT` | `/bank-statement-lines/{id}/reconcile` | Not yet built on the frontend — reconcile a statement line | `matchedJournalNo` |
|
||||
|
||||
No `GET`/`PUT` by id exists on GL's side for either `bank_account` or `cash_account` — this is the entire reason edit can't be built yet (`docs/21` §4's known gap).
|
||||
|
||||
### Budgets (`docs/21` §3 — picker only)
|
||||
|
||||
| Method | Path | Used for | Key params |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/budgets` | Populates the `budgetId` picker on the Budget vs Actual report screen | none — full list |
|
||||
|
||||
### Not currently called by this frontend, listed for awareness
|
||||
|
||||
GL exposes a substantially larger surface than this module touches — Journal Entries, Fiscal Calendar, Cost Centers, Currency, Tax Codes, Fixed Assets, Recurring Templates, System Config, Audit Log. None of these have a screen in `docs/21` today. If a future pass adds one, add its row to the relevant table above rather than leaving this reference to drift out of sync with what's actually called — that's the whole value of keeping this section current.
|
||||
|
||||
---
|
||||
|
||||
*End of 12-GENERAL-LEDGER-INTEGRATION.md. Hub: `00-CORE.md`. Record work: `Backend/PROGRESS.md`.*
|
||||
@@ -0,0 +1,532 @@
|
||||
# 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=1`…`Expense=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 `StatCard`s.** `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
|
||||
`glAccountId` → `glAccountCode` (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 wholesale** — `ChequeBook`/
|
||||
`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-balance` … `ledgers.tax-report`
|
||||
(new, id 16) … `ledgers.bank-accounts`, ids 9–16) — `NavItemConfiguration.cs`/`SubNavItemConfiguration.cs`.
|
||||
- **9** matching `Permission` rows (`NAV:ledgers`, `NAV:ledgers.*`, ids 19–27, `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
|
||||
|
||||
- [x] **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.
|
||||
- [x] **Cash/Bank Accounts — list + create, original single-table design (2026-07-20):**
|
||||
client-side-filtered list, create form with a GL-account picker.
|
||||
- [x] **RBAC nav seed, original 7-entry version (2026-07-20):** migration `AddLedgersNavSeed`, not yet
|
||||
applied to a live database.
|
||||
- [x] **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.
|
||||
- [x] **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 `StatCard`s 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.ts` — `ReportOutputFormat` 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.
|
||||
- [x] **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.
|
||||
- [x] **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.
|
||||
- [x] **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.
|
||||
- [x] **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`.
|
||||
- [x] **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.
|
||||
- [x] **`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.
|
||||
- [x] **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`.*
|
||||
Reference in New Issue
Block a user