diff --git a/Backend/ERPCore/Controllers/GeneralLedgerController.cs b/Backend/ERPCore/Controllers/GeneralLedgerController.cs
new file mode 100644
index 0000000..2f0bf12
--- /dev/null
+++ b/Backend/ERPCore/Controllers/GeneralLedgerController.cs
@@ -0,0 +1,43 @@
+using ERPCore.Services.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ERPCore.Controllers;
+
+///
+/// Generic reverse proxy into the external General Ledger service — forwards every
+/// method/path/query/body under this prefix verbatim via
+/// and returns GL's response (status, content-type,
+/// body) unchanged. No endpoint-specific shape lives here; see
+/// docs/12-GENERAL-LEDGER-INTEGRATION.md for the full GL contract and what this proxy
+/// does and doesn't do. Gated by the same ERP door policy as every other v1 endpoint
+/// () — the shared GL API key is attached server-side
+/// only and is never exposed to the frontend.
+///
+[Route("api/v1/gl")]
+public sealed class GeneralLedgerController : ApiControllerBase
+{
+ private readonly IGeneralLedgerService _gl;
+
+ public GeneralLedgerController(IGeneralLedgerService gl) => _gl = gl;
+
+ [HttpGet("{**path}")]
+ public Task Get(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Get, path, ct);
+
+ [HttpPost("{**path}")]
+ public Task Post(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Post, path, ct);
+
+ [HttpPut("{**path}")]
+ public Task Put(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Put, path, ct);
+
+ private async Task ForwardAsync(HttpMethod method, string path, CancellationToken ct)
+ {
+ var body = method == HttpMethod.Get ? null : Request.Body;
+ var result = await _gl.ForwardAsync(method, path, Request.QueryString.Value, Request.ContentType, body, ct);
+ return new ContentResult
+ {
+ StatusCode = result.StatusCode,
+ Content = result.Body,
+ ContentType = result.ContentType ?? "application/json"
+ };
+ }
+}
diff --git a/Backend/ERPCore/Infra/Gl/GeneralLedgerClient.cs b/Backend/ERPCore/Infra/Gl/GeneralLedgerClient.cs
new file mode 100644
index 0000000..170879f
--- /dev/null
+++ b/Backend/ERPCore/Infra/Gl/GeneralLedgerClient.cs
@@ -0,0 +1,55 @@
+using ERPCore.System.Errors;
+
+namespace ERPCore.Infra.Gl;
+
+///
+/// HTTP implementation of . Registered as a typed
+/// client (`AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>`) with its
+/// `BaseAddress` bound from `GeneralLedgerService:BaseUrl`. Every call attaches the
+/// shared `GeneralLedgerService:ApiKey` as `X-Api-Key` and streams the request/response
+/// body straight through, unparsed — GL's own response (status, content-type, body) is
+/// returned exactly as received; nothing here reshapes it.
+///
+public sealed class GeneralLedgerClient(HttpClient http, IConfiguration configuration) : IGeneralLedgerClient
+{
+ private readonly HttpClient _http = http;
+ private readonly string _apiKey = configuration["GeneralLedgerService:ApiKey"] ?? string.Empty;
+
+ public async Task SendAsync(
+ HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
+ {
+ var relativeUri = path.TrimStart('/') + queryString;
+ using var request = new HttpRequestMessage(method, relativeUri);
+ request.Headers.TryAddWithoutValidation("X-Api-Key", _apiKey);
+
+ if (body is not null && method != HttpMethod.Get)
+ {
+ var content = new StreamContent(body);
+ if (!string.IsNullOrEmpty(contentType))
+ content.Headers.TryAddWithoutValidation("Content-Type", contentType);
+ request.Content = content;
+ }
+
+ HttpResponseMessage response;
+ try
+ {
+ response = await _http.SendAsync(request, ct);
+ }
+ catch (HttpRequestException)
+ {
+ throw new DomainException(ErrorCodes.GlServiceUnavailable, "The General Ledger service is unreachable.", 503);
+ }
+ catch (TaskCanceledException) when (!ct.IsCancellationRequested)
+ {
+ throw new DomainException(ErrorCodes.GlServiceUnavailable, "The General Ledger service timed out.", 503);
+ }
+
+ var responseBody = await response.Content.ReadAsStringAsync(ct);
+ return new GeneralLedgerResponse
+ {
+ StatusCode = (int)response.StatusCode,
+ ContentType = response.Content.Headers.ContentType?.ToString(),
+ Body = responseBody
+ };
+ }
+}
diff --git a/Backend/ERPCore/Infra/Gl/GeneralLedgerResponse.cs b/Backend/ERPCore/Infra/Gl/GeneralLedgerResponse.cs
new file mode 100644
index 0000000..bfaec0c
--- /dev/null
+++ b/Backend/ERPCore/Infra/Gl/GeneralLedgerResponse.cs
@@ -0,0 +1,15 @@
+namespace ERPCore.Infra.Gl;
+
+///
+/// Raw HTTP result from the external General Ledger service — status code, content
+/// type, and body exactly as GL returned them. Deliberately un-reshaped: GL's own
+/// envelope (see the GL service's own API reference) is passed through byte-for-byte
+/// so its camelCase-success/PascalCase-error inconsistency and full decimal precision
+/// survive the hop unchanged (docs/12-GENERAL-LEDGER-INTEGRATION.md).
+///
+public sealed class GeneralLedgerResponse
+{
+ public int StatusCode { get; init; }
+ public string? ContentType { get; init; }
+ public string Body { get; init; } = string.Empty;
+}
diff --git a/Backend/ERPCore/Infra/Gl/IGeneralLedgerClient.cs b/Backend/ERPCore/Infra/Gl/IGeneralLedgerClient.cs
new file mode 100644
index 0000000..962c0eb
--- /dev/null
+++ b/Backend/ERPCore/Infra/Gl/IGeneralLedgerClient.cs
@@ -0,0 +1,13 @@
+namespace ERPCore.Infra.Gl;
+
+///
+/// Typed HTTP transport to the external General Ledger service. Injects the shared
+/// `X-Api-Key` secret and forwards method/path/query/body/content-type verbatim —
+/// see docs/12-GENERAL-LEDGER-INTEGRATION.md. Internal: only
+/// consumes this.
+///
+public interface IGeneralLedgerClient
+{
+ Task SendAsync(
+ HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct);
+}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/NavItemConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/NavItemConfiguration.cs
index 0a89d0a..4db1434 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/NavItemConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/NavItemConfiguration.cs
@@ -36,7 +36,9 @@ public sealed class NavItemConfiguration : IEntityTypeConfiguration
new NavItem { NavItemId = 7, Code = "warehouses", Label = "Warehouses", Href = "/dashboard/warehouse", SortOrder = 7 },
new NavItem { NavItemId = 8, Code = "orders", Label = "Orders", Href = "/dashboard/orders", SortOrder = 8 },
new NavItem { NavItemId = 9, Code = "settings", Label = "Settings", Href = "/dashboard/settings", SortOrder = 9 },
- new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 }
+ new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 },
+ new NavItem { NavItemId = 11, Code = "ledgers", Label = "Ledgers", Href = "/dashboard/ledgers", SortOrder = 11 },
+ new NavItem { NavItemId = 12, Code = "accounts", Label = "Accounts", Href = "/dashboard/accounts", SortOrder = 12 }
);
}
}
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
index 065f5b8..47bec28 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
@@ -42,10 +42,26 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration();
builder.Services.AddScoped();
builder.Services.AddScoped();
+// General Ledger service proxy → external GL microservice (docs/12-GENERAL-LEDGER-INTEGRATION.md)
+builder.Services.AddHttpClient(c =>
+{
+ var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"]
+ ?? throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured.");
+ c.BaseAddress = new Uri(baseUrl);
+});
+builder.Services.AddScoped();
+
// Current-user (audit actor). AuthHex has no sub/nameid → a claims transformation
// JIT-provisions a local shadow user and injects the local `int` id as `nameid`.
builder.Services.AddHttpContextAccessor();
diff --git a/Backend/ERPCore/Services/GeneralLedgerService.cs b/Backend/ERPCore/Services/GeneralLedgerService.cs
new file mode 100644
index 0000000..4b70e23
--- /dev/null
+++ b/Backend/ERPCore/Services/GeneralLedgerService.cs
@@ -0,0 +1,16 @@
+using ERPCore.Infra.Gl;
+using ERPCore.Services.Interfaces;
+
+namespace ERPCore.Services;
+
+///
+public sealed class GeneralLedgerService : IGeneralLedgerService
+{
+ private readonly IGeneralLedgerClient _client;
+
+ public GeneralLedgerService(IGeneralLedgerClient client) => _client = client;
+
+ public Task ForwardAsync(
+ HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
+ => _client.SendAsync(method, path, queryString, contentType, body, ct);
+}
diff --git a/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs b/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs
new file mode 100644
index 0000000..eeff07b
--- /dev/null
+++ b/Backend/ERPCore/Services/Interfaces/IGeneralLedgerService.cs
@@ -0,0 +1,16 @@
+using ERPCore.Infra.Gl;
+
+namespace ERPCore.Services.Interfaces;
+
+///
+/// Single entry point into the external General Ledger service — the one function
+/// used both by (frontend
+/// requests, forwarded verbatim) and, once wired, other ERPCore services that need to
+/// post directly to GL (e.g. GRN confirm, adjustments — docs/12-GENERAL-LEDGER-INTEGRATION.md).
+/// No business logic lives here yet; this pass only connects the transport.
+///
+public interface IGeneralLedgerService
+{
+ Task ForwardAsync(
+ HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct);
+}
diff --git a/Backend/ERPCore/System/Errors/ErrorCodes.cs b/Backend/ERPCore/System/Errors/ErrorCodes.cs
index 903af82..bc439f3 100644
--- a/Backend/ERPCore/System/Errors/ErrorCodes.cs
+++ b/Backend/ERPCore/System/Errors/ErrorCodes.cs
@@ -66,4 +66,7 @@ public static class ErrorCodes
public const string LeftoverExceedsConsumed = "LEFTOVER_EXCEEDS_CONSUMED";
public const string RunCostClosed = "RUN_COST_CLOSED";
public const string RunNotCancellable = "RUN_NOT_CANCELLABLE";
+
+ // General Ledger service proxy (GeneralLedgerController → external GL service, docs/12)
+ public const string GlServiceUnavailable = "GL_SERVICE_UNAVAILABLE";
}
diff --git a/Backend/ERPCore/appsettings.Development.json b/Backend/ERPCore/appsettings.Development.json
index 28ac658..6d40007 100644
--- a/Backend/ERPCore/appsettings.Development.json
+++ b/Backend/ERPCore/appsettings.Development.json
@@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
- "DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=root"
+ "DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=dbuser"
},
"AuthHex": {
"BaseUrl": "http://localhost:5011"
diff --git a/Backend/ERPCore/appsettings.json b/Backend/ERPCore/appsettings.json
index ee6e372..9d07a93 100644
--- a/Backend/ERPCore/appsettings.json
+++ b/Backend/ERPCore/appsettings.json
@@ -22,5 +22,9 @@
"RootPath": "App_Data/hr-documents",
"MaxSizeBytes": 10485760
},
+ "GeneralLedgerService": {
+ "BaseUrl": "https://localhost:7024/api/v1/",
+ "ApiKey": "1A5FE0E389C029B1FBCD2A94650CEBF0656083BF06FF48E5E11987040ABA262D"
+ },
"AllowedHosts": "*"
}
diff --git a/Backend/PROGRESS.md b/Backend/PROGRESS.md
index f9431cf..4e6884f 100644
--- a/Backend/PROGRESS.md
+++ b/Backend/PROGRESS.md
@@ -105,6 +105,31 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
- [~] FEFO picking for perishables; block expired / on-hold issue — **issue-block done + verified** (`ONHOLD_NOT_ISSUABLE`, `EXPIRED_BATCH_BLOCKED`; on-hold/expired layers excluded from consume). FEFO *pick ordering* (oldest-expiry first) not yet built.
- [x] Reason codes (FR-X-04) — `ReasonCode` entity + `GET/POST /reason-codes`; standard set (docs/10 §B.8.3) seeded idempotently at startup (`DataSeeder`). Verified.
+## 7. External Integrations
+> **General Ledger service** (separate microservice, own repo/DB) — connected 2026-07-20 as a generic reverse-proxy only; no ERPCore business logic posts to it yet. Full contract + progress detail: `docs/12-GENERAL-LEDGER-INTEGRATION.md`.
+- [~] Generic proxy `GET|POST|PUT /api/v1/gl/{**path}` (`GeneralLedgerController` → `IGeneralLedgerService` → `IGeneralLedgerClient`) — forwards method/path/query/body/content-type verbatim to the GL service with a server-attached `X-Api-Key`; GL's response (status + body) returned unchanged. ErpAccess-door-policy-gated like every other v1 endpoint. Config: `GeneralLedgerService:BaseUrl`/`ApiKey` in `appsettings.json`. Build verified clean; **not yet live-smoke-tested** (no running GL instance this pass).
+- [ ] Internal wiring — ERPCore services (GRN confirm, adjustments, etc.) calling `IGeneralLedgerService` directly to post real journal entries. Deliberately deferred.
+
+> ### 2026-07-20 — RBAC nav seed for the frontend's new "Ledgers" section
+> The `Frontend/PROGRESS.md` §8 "Ledgers" sidebar section (docs/21-GENERAL-LEDGER-FRONTEND.md) needs a matching `NavItem`/`SubNavItem`/`Permission` row for every entry, or the sidebar filters it out for every role regardless of the frontend change (docs/10 C.8, `GET /auth/me`'s `navCodes`). Added via `NavItemConfiguration.cs`/`SubNavItemConfiguration.cs`/`PermissionConfiguration.cs` `HasData`: `NavItem` `ledgers` (id 11), 7 `SubNavItem` rows (ids 9–15, `ledgers.trial-balance` … `ledgers.bank-accounts`), 8 `Permission` rows (ids 19–26) — same one-`Permission`-per-nav-entry convention as every existing nav row. Migration `AddLedgersNavSeed`.
+> **Build note:** a locally running `ERPCore.exe` (PID 29692) held the default `bin/Debug` output locked for the whole session, so `dotnet ef migrations add` twice produced an empty no-op migration off a stale assembly (`--no-build` silently reused pre-edit code) before the real cause was found. Fixed by building to a scratch output directory (unaffected by the lock), copying the fresh `ERPCore.dll` over the locked `bin/Debug` copy (the running process only locks the `.exe`, not the `.dll`), then re-scaffolding — the resulting migration's `Up`/`Down` were verified by inspection against the identical, already-applied `AddRolesNavPermissions` migration's `InsertData`/`DeleteData` shape. The stray process was left running rather than killed, since it wasn't started by this work and may be in active use elsewhere.
+> **Not yet applied to a live database** — no Postgres instance was available in this pass to run `dotnet ef database update` against. `dotnet build` is clean (0 warnings/0 errors).
+> **Operational step still needed post-deploy (not code):** a new `NavItem`/`SubNavItem` carries no `RolePermission` grants by default — an administrator must check the new Ledgers permissions for the relevant role(s) via **Settings → Roles** before anyone sees the sidebar entry, same as every previous nav addition.
+
+> ### 2026-07-30 — RBAC nav seed: 8th sub-item for the new "Tax Report" screen
+> The frontend's GL-revision pass (`docs/21-GENERAL-LEDGER-FRONTEND.md`, Frontend/PROGRESS.md §8) added a Tax Report screen to the Ledgers sidebar section — needs the same nav-seed treatment as every other entry (docs/10 C.8). Added `SubNavItem` id 16 (`ledgers.tax-report`, `/dashboard/ledgers/tax-report`, sort order 7) and `Permission` id 27 (`NAV:ledgers.tax-report`); re-sequenced the existing `ledgers.bank-accounts` row's `SortOrder` from 7→8 so Tax Report sits before it, matching the sidebar array's actual order. Migration `AddTaxReportNavSeed` — no locked-process issue this time (confirmed no stray `ERPCore.exe` running before scaffolding), generated cleanly on the first attempt with real `InsertData`/`UpdateData`/`DeleteData` (`Down()` correctly restores `bank-accounts`' `SortOrder` to 7). `dotnet build` clean (0 warnings/0 errors). **Not yet applied to a live database** — same open item as the original `AddLedgersNavSeed` migration; both are still pending `dotnet ef database update` against a real Postgres instance.
+
+> ### 2026-07-30 (2) — Fixed a real `SubNavItemId`/`PermissionId` collision between Procurement and Ledgers seed data
+> **Root cause:** when the 2026-07-20 `AddLedgersNavSeed` migration was authored, its `SubNavItem`/`Permission` IDs were picked by looking at the *actual DB row count*, not the config source — but `SubNavItemConfiguration.cs`/`PermissionConfiguration.cs` already had `HasData` entries for Procurement's 4 sub-items (`procurement.requisitions`/`.rfqs`/`.purchase-orders`/`.purchase-returns`, ids 9–12/19–22) that **had never actually been migrated into any database** (no migration `Up()` anywhere ever inserts them — confirmed by grep across every migration file). Ledgers then claimed the same ids (9–12 sub-nav, 19–22 permission) for its own rows, so the config ended up with two `HasData` entries sharing the same primary key per table. `ErpDbContextModelSnapshot.cs` had silently absorbed both (`dotnet ef migrations add` doesn't hard-fail on this at scaffold time), but **EF's runtime model validator does** — `dotnet ef migrations add` for anything touching these tables, and by extension normal app startup/first `DbContext` use, throws `InvalidOperationException: A seed entity ... has the same key value as another seed entity mapped to the same table`. This is very likely the crash the user was hitting.
+> **Fix:** moved Procurement's 4 sub-nav rows off the colliding ids onto **17–20** (`SubNavItemId`) and **28–31** (`PermissionId`), past every id already claimed by Ledgers/Tax-Report (max 16/27). Removed the phantom duplicate Procurement entries from `ErpDbContextModelSnapshot.cs` (they never reflected real DB state) so the differ could compute a clean diff, then generated migration **`FixProcurementNavIdCollision`** — pure `InsertData` for the 4 sub-nav rows + 4 permission rows at their new ids (this is also the *first* migration that actually creates Procurement's sub-nav-item/permission rows in the database at all). `Down()` is a clean `DeleteData` reversal.
+> **Verified:** running `dotnet ef migrations add` against the pre-fix config reproduced the exact `InvalidOperationException` above (scaffold failed outright, no migration file produced), confirming this was a real, reproducible crash and not a false alarm; after the fix, the same command succeeded and `dotnet ef migrations list` builds the full model with no error, listing all 7 migrations (the last 2 — `AddTaxReportNavSeed`, `FixProcurementNavIdCollision` — still `(Pending)`, no Postgres instance available this session); `dotnet build` clean (0 warnings/0 errors). **Not yet applied to a live database** — same standing blocker as the two prior nav-seed migrations.
+> Also fixed, same pass: `Frontend/erp-system/components/Layouts/AppSidebar.tsx`'s auto-expand-active-parent logic tripped `react-hooks/set-state-in-effect` (`setExpanded` called synchronously inside a `useEffect`) — converted to the same "adjust state during render" pattern used for the Ledgers report pages, keyed on a `pathname + item-codes` composite key (tracked via a `lastAutoExpandKey` state var) so it still re-fires once `items` populates after the RBAC `navCodes` fetch resolves. `npx eslint components/Layouts/AppSidebar.tsx` clean.
+
+> ### 2026-07-31 — RBAC nav seed: new "Accounts" nav item (Cheque Management screens + Cash/Bank Accounts moved off Ledgers)
+> The frontend added a new "Accounts" sidebar section (`Frontend/PROGRESS.md` §8) for the new Cheque Management screens and to hold Cash/Bank Accounts, which moved out of Ledgers into it (user-requested — Cheque Books/Received Cheques/Cash-Bank Accounts are all the same kind of operational account bookkeeping, not a statutory report). Migration **`AddAccountsNavSeed`**: `InsertData` for `NavItem` `accounts` (id 12) and two new `SubNavItem`/`Permission` pairs (`accounts.cheque-books` id 21/33, `accounts.received-cheques` id 22/34); **`UpdateData`, not delete-and-recreate**, for the existing Cash/Bank Accounts `SubNavItem`/`Permission` (ids 15/26) — same ids, just new `Code`/`Href`/`NavItemId` — so a role that had already been granted this permission under its old `ledgers.bank-accounts` code doesn't silently lose it just because the section changed. `Down()` correctly reverses both the inserts and the renamed-row update back to its Ledgers-era values.
+> **No locked-process issue avoided this time** — `ERPCore.exe` was found running twice during this pass (the user had restarted it between turns to test the Tax Report fix); confirmed with the user before killing it each time, per this session's standing caution around stopping their dev server. `dotnet build` clean (0 warnings/0 errors); `dotnet ef migrations list` shows all 8 migrations with none pending. **Applied to the live database this session** (`dotnet ef database update`) — unlike every prior nav-seed migration this session, this one did not have to wait for a live Postgres instance to become available.
+> **Operational step still needed post-deploy (not code):** same as every previous nav addition — an administrator must grant the new `NAV:accounts`/`NAV:accounts.cheque-books`/`NAV:accounts.received-cheques` permissions to the relevant role(s) via **Settings → Roles** before anyone sees the new sidebar entries (the re-homed `NAV:accounts.bank-accounts` keeps whatever grants it already had).
+
## Deferred (Phase 2+ — do NOT build now, hooks only)
- [ ] Vendor invoice + three-way match
- [ ] Reservation/allocation fulfilment
diff --git a/Frontend/PROGRESS.md b/Frontend/PROGRESS.md
index 90d3a13..b6cb645 100644
--- a/Frontend/PROGRESS.md
+++ b/Frontend/PROGRESS.md
@@ -83,6 +83,46 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
- [ ] `412` conflict → prompt refetch before retry — `apiRequestWithETag` surfaces the ETag but no screen edits a GRN yet (GRN has no PUT), so untested in practice
- [x] No client-side gating on stock/availability/status (server-authoritative) — GRN create always submits to the server and surfaces `OVER_RECEIPT_TOLERANCE`/etc. via `error-map.ts` rather than pre-blocking
+## 8. General Ledger (Ledgers + Accounts sections)
+> Two sidebar sections (`app/dashboard/ledgers/*`, `app/dashboard/accounts/*`), sourced entirely from the external General Ledger service via ERPCore's generic proxy (`docs/12-GENERAL-LEDGER-INTEGRATION.md`). Full detail, decisions, and known gaps: `docs/21-GENERAL-LEDGER-FRONTEND.md`.
+- [x] Ledgers: reports hub + 7 report screens (Trial Balance, Balance Sheet, General Ledger, Profit & Loss, Cash Flow, Budget vs Actual, Tax Report) — statutory-format header/table, PDF **and CSV** download via the same endpoint with `outputFormat=Pdf`/`Csv`
+- [x] Accounts: hub + Cash/Bank Accounts — unified list (GL's own server-side `accountType` union + client-side text search) + create (Cash/Bank toggle; GL account is now auto-created server-side, **no picker** — see 2026-07-31 (6) below). **Moved here from Ledgers (2026-07-31 (5))**
+- [ ] Cash/Bank Accounts — edit: **not built**, GL has no `GET`/`PUT` by id for either table to build it against (list shows a disabled Edit affordance with an explanatory tooltip instead of a broken form)
+- [x] Accounts: Cheque Books — list/filter, create (auto-generates every leaf), drill-down to a book's own pages list, per-page details/issue/status-update in a modal
+- [x] Accounts: Received Cheques — list/filter, create, per-row details/status-update in a modal
+- [x] Sidebar "Ledgers" (7 sub-items) + new "Accounts" (3 sub-items) nav items (`components/Layouts/AppSidebar.tsx`) + header title mappings (`components/Layouts/Header.tsx`)
+- [x] Dedicated GL fetch client (`lib/api/general-ledger.ts`) — GL's envelope differs from ERPCore's own `ProblemDetails`, so this does not reuse `lib/api-client.ts`; now also covers Cheque Management (`chequeBooksApi`/`chequePagesApi`/`receivedChequesApi`)
+
+> **2026-07-30 — GL's 2026-07-22 backend revision built out (large pass).** Five reports restructured (Trial Balance flattened, Profit & Loss → nested named sections with a Gross Profit subtotal, Cash Flow → a real structured statement replacing the four `StatCard`s), a new CSV export on all seven reports (`components/reports/DownloadCsvButton.tsx`), a brand-new **Tax Report** screen (Income Tax Computation, collapsible optional-adjustments panel, payable/refundable sign-dependent final row), and Cash/Bank accounts split into two real GL endpoints (`POST /bank-accounts` vs `POST /cash-accounts`, unified `GET /bank-accounts?accountType=`) with a two-choice create-form toggle and a Cash Account Type picker that can create a new type on the fly. Extracted `components/reports/{ReportSection,ReportSubtotal}.tsx` — shared by Profit & Loss and Cash Flow rather than duplicating the "bordered section + bold subtotal" markup twice. Three response shapes (`ProfitAndLossResponse`/`CashFlowResponse`/`TaxSummaryResponse`) are **inferred** where GL's own reference doesn't spell out every field verbatim — flagged in `types/general-ledger.ts`'s own comments and `docs/21-GENERAL-LEDGER-FRONTEND.md`, same posture as the original inferred `BankAccount` shape. Backend: migration `AddTaxReportNavSeed` adds the 8th sidebar sub-item + its permission row. Verified: `tsc --noEmit` clean, `eslint` clean across every touched file, `npm run build` succeeds with all 9 `/dashboard/ledgers/*` routes (incl. `/tax-report`), `dotnet build` clean. **Not done:** live smoke test against a running GL instance (still no instance available this session) — the three inferred response shapes are the highest-value thing to verify first.
+
+> **2026-07-20, same-day fixes (user-reported):** (1) General Ledger report was wrongly calling `GET /accounts` to populate an account picker — GL documents `accountId` on this report as a raw id, not a code-lookup value, so the picker is gone; the screen now only ever calls `/reports`, entering `accountId` directly and reading the account's code/name for display off the report's own returned rows instead. (2) `ReportType`/`ReportOutputFormat`/`GlAccountTypeId` converted from string/numeric literal unions to real TS enums. (3) Fixed a UI-only bug where a selected `