Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 36 additions & 44 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,60 +23,24 @@
- [x] **PR #631** — **hotfix**: temporarily disable the Profile currency switcher with a tooltip and force-reset any stored non-UAH user preference to UAH on next login. Prevents the mixed-label regression where `/expenses` showed `1000.00 USD` rows alongside `1 000,00 грн` totals *(safety net; blocks #632 until it is solved)*
- [x] **PR #634** — currency conversion v2: rewrite `useFormatCurrency` with the signature `(uahValue, date?)` and proper math (`uah / rateToUah`), null-safe rendering (`—`), warn-once fallback when the rate table is empty; introduce the single-source `<Money/>` component; lock all monetary input addons to hardcoded `₴` (Variant B) and show a "Сума зберігається в гривнях" helper text; unit tests (7 cases) + Playwright regression test for the mixed-label bug. Switcher re-enabled as the last commit. *(TZ 8.2, conversion layer)*
- [x] **PR #616** *(parallel design-system track)* — design-system foundation: TypeScript token source-of-truth, `scripts/build-tokens.ts`, `frontend/src/design-system/tokens/*`, `lightTheme.ts` as deadcode ThemeConfig. Zero breaking changes to existing CSS variable names.
- [x] **PR #614** — Super-admin core: impersonation + global users + audit log *(TZ 14 remainder, core)* — Impersonation engine (`POST /api/admin/impersonate` with 60min TTL, rate-limited 3/24h per admin-target pair, index-backed query on SuperAdminAuditLog), forbidden-action filter (403 + `impersonate.forbidden_attempt` audit), in-app notification to target (severity warning, title "Сесія імперсонації", Ukrainian body with timestamp), `/admin/users` global search + impersonate modal, `/admin/audit-log` filtered view, red persistent non-closable z-index-9999 banner during session. 6 integration tests required / all pass. Merged as PR #640 (commit b029b0c).

---

## In progress

- [ ] **PR #614 — Super-admin core: impersonation + global users + audit log** *(TZ 14 remainder, core)*
- Impersonation engine: `POST /api/admin/impersonate` with mandatory `reason` (min 10 chars), 60min TTL, not renewable, returns short-lived JWT carrying `impersonating_user_id` + `impersonated_by_user_id` + `original_tenant_id` + `reason` claims
- `POST /api/admin/impersonate/end` returns the super-admin's restored token and writes `impersonate.end` audit
- Rate limit: 3 sessions per (admin, target) per 24h, enforced by query against `SuperAdminAuditLog` filtered by `Action = 'impersonate.start'`. Backed by a **partial composite index** on `(admin_user_id, target_id, occurred_at DESC) WHERE action = 'impersonate.start'`
- Forbidden-action filter in impersonation: blocks password change, email change, API keys write scope, billing, full data export. Returns **403 AND** writes a separate audit entry of type `impersonate.forbidden_attempt` with the attempted route
- Audit: every mutation while impersonating tagged `impersonated_by` + `acted_as` (extension to existing `ISuperAdminAuditService`)
- **In-app notification** to target user (via `Notification` entity, severity `warning`, title "Сесія імперсонації", body "Адміністратор {full_name} увійшов під вашим акаунтом {timestamp_kyiv}. Причина: {reason}."), independent of SMTP availability
- Best-effort `IEmailService.SendAsync` call as well (silently no-ops if SMTP unconfigured)
- `/admin/users` page — global search across tenants, impersonate button → reason modal → token swap → redirect to `/`
- `/admin/audit-log` page — global view with filters (tenant, user, action type, period); CSV export deferred to #614a
- **Red persistent banner** during impersonation: not closable, not dismissable, z-index 9999, full viewport width, shows target user + tenant + remaining TTL countdown + "Вийти з режиму" button. No localStorage opt-out
- 6 integration tests required (non-super-admin → 403, no-MFA → 403+header, valid start → 200+audit+notification, reason<10 → 400, rate-limit 4th → 429, forbidden action → 403+forbidden-attempt audit). `TestAuthHandler` extended in this PR if needed
- [ ] **PR #615 — Warehouse: grain receipt + inventory** *(TZ 9, TZ 10)*
- `/grain-storages` "Прийняти зерно" button: full form (date, crop, warehouse, batch, qty, moisture/trash %, source, driver/TTN), validation (qty>0, warehouse capacity, field in season if source=Field), atomically creates GrainReceipt + GrainBatch + GrainBatchPlacement + StockLedgerEntry, recalculates StockBalance
- Verify `TransferGrainHandler` updates `GrainBatchPlacement` (known bug)
- `/inventory` full cycle: Draft → InProgress → Completed/Cancelled, inline Counted editing with auto Difference, color-coded (red/amber/neutral), comment dropdown (Недостача/Списання/etc), progress bar "Підраховано X з Y", completion creates InventoryAdjustment ledger entries, invalidates StockBalance cache, cancellation without ledger changes
- Integration tests (receipt creates 4 entities, capacity validation, inventory completion ledger, inventory cancellation no-ledger)
- Playwright e2e (grain receipt form fill+submit)

---

## Upcoming (in order — do not reorder without approval)

- [ ] **PR #614a — Super-admin: system health page** *(TZ 14 follow-up)*
- `/admin/system` page (read-only dashboard)
- Backend: `GET /api/admin/system/health` aggregates Hangfire queue depth + failed-jobs count, DB connection pool usage, storage volume usage, active SignalR connections (when notification hub lands), background-job last-run-at per recurring job
- Auto-refresh every 30s, severity colours (green/amber/red) per metric
- CSV export added to `/admin/audit-log` here as well

- [ ] **PR #614b — Super-admin: global catalogs CRUD** *(TZ 14 follow-up)*
- `/admin/catalogs` page with tabs: Crops, Equipment types, Units, Document types
- Global reference data is shared across all tenants — soft-delete only (existing tenant data must keep referencing the row)
- Backend: `/api/admin/catalogs/{type}` CRUD with audit on every mutation (`catalog.crop.create`, etc.), validation that a row in use cannot be hard-deleted
- Bulk import CSV (deferred until concrete request)

- [ ] **PR #614c — Super-admin: broadcast notifications** *(TZ 14 follow-up)*
- `/admin/broadcast` page: composer (title, body, severity) + audience picker (all tenants / selected tenants / by feature flag)
- Backend: `POST /api/admin/broadcast` fans out to `Notification` rows per target tenant; rate-limited to 1 broadcast per minute per super-admin
- History view of past broadcasts with reach count per broadcast
- Depends on Notifications fixes in PR #617 (per-user routing) — order: ship #614a, then #617, then #614b/c

- [ ] **PR #617 — Export currency header** *(TZ 8.2 follow-up)*
- Add NBU rate on export date to CSV/PDF export headers where currency amounts appear (costs, revenue, grain)
- Tied to existing export helpers; deferred from PR #613 to keep that PR reviewable

- [ ] **PR #615 — Warehouse: grain receipt + inventory** *(TZ 9, TZ 10)*
- `/grain-storages` "Прийняти зерно" button: full form, creates GrainReceipt + GrainBatch + GrainBatchPlacement + StockLedgerEntry, recalculates StockBalance
- Verify `TransferGrainHandler` updates `GrainBatchPlacement` (known bug)
- `/inventory` full cycle: Draft → InProgress → Completed/Cancelled
- Inline editing of Counted column, auto-calculated Difference with color coding
- Progress indicator: "Підраховано X з Y (Z%)"
- Completion creates `InventoryAdjustment` ledger entries per diff, invalidates `StockBalance` cache
- Session history with read-only view after completion

- [ ] **PR #617 — Notifications center** *(TZ 12)*
- [ ] **PR #616 — Notifications center** *(TZ 12)*
- Dropdown layout fix (min-width 400px, title inline)
- dayjs relativeTime with `uk` locale, proper thresholds (`X хв тому`, `X дн. тому`, `12 берез.`, etc.)
- Mark-all-read, clear-read working
Expand All @@ -86,6 +50,10 @@
- Backend fix: `NotificationService` uses empty Identity Roles tables — migrate to enum role on `AppUser`
- Triggers verified: overdue op, tech repair, low fuel, low/over storage, sale completed, job failure

- [ ] **PR #617 — Export currency header** *(TZ 8.2 follow-up)*
- Add NBU rate on export date to CSV/PDF export headers where currency amounts appear (costs, revenue, grain)
- Tied to existing export helpers; deferred from PR #613 to keep that PR reviewable

- [ ] **PR #618 — Demo seeder + Mobile + PWA** *(TZ 11, TZ 13)*
- `Tools/DemoSeeder` idempotent: fills all core modules with connected Ukrainian-realistic data (6–12 months history)
- Auto-enables all optional feature flags for demo tenant after seed
Expand All @@ -100,6 +68,30 @@

---

## Deferred (post-core, before Phase B)

These are super-admin follow-ups that depend on PR #614 core being stable. They will ship after warehouse/notifications/demo work.

- [ ] **PR #614a — Super-admin: system health page** *(TZ 14 follow-up)*
- `/admin/system` page (read-only dashboard)
- Backend: `GET /api/admin/system/health` aggregates Hangfire queue depth + failed-jobs count, DB connection pool usage, storage volume usage, active SignalR connections (when notification hub lands), background-job last-run-at per recurring job
- Auto-refresh every 30s, severity colours (green/amber/red) per metric
- CSV export added to `/admin/audit-log` here as well

- [ ] **PR #614b — Super-admin: global catalogs CRUD** *(TZ 14 follow-up)*
- `/admin/catalogs` page with tabs: Crops, Equipment types, Units, Document types
- Global reference data is shared across all tenants — soft-delete only (existing tenant data must keep referencing the row)
- Backend: `/api/admin/catalogs/{type}` CRUD with audit on every mutation (`catalog.crop.create`, etc.), validation that a row in use cannot be hard-deleted
- Bulk import CSV (deferred until concrete request)

- [ ] **PR #614c — Super-admin: broadcast notifications** *(TZ 14 follow-up)*
- `/admin/broadcast` page: composer (title, body, severity) + audience picker (all tenants / selected tenants / by feature flag)
- Backend: `POST /api/admin/broadcast` fans out to `Notification` rows per target tenant; rate-limited to 1 broadcast per minute per super-admin
- History view of past broadcasts with reach count per broadcast
- Depends on Notifications fixes in PR #617 (per-user routing) — order: ship #614a, then #617, then #614b/c

---

## Decisions locked (do not re-discuss, do not override silently — even in deadcode or scaffolding)

**Currency**
Expand Down
4 changes: 2 additions & 2 deletions docs/TZ.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@

---

## ПУНКТ 9 — Кнопка "Прийняти зерно" на /grain-storages `[PLANNED for PR #615]`
## ПУНКТ 9 — Кнопка "Прийняти зерно" на /grain-storages `[IN PROGRESS in PR #615]`

**Problem:** "Прийняти зерно" button does nothing on click.

Expand All @@ -123,7 +123,7 @@

---

## ПУНКТ 10 — Фікс модуля /inventory (Інвентаризація) `[PLANNED for PR #615]`
## ПУНКТ 10 — Фікс модуля /inventory (Інвентаризація) `[IN PROGRESS in PR #615]`

**Problem:** Inventory module doesn't work end-to-end. Counted/Difference columns empty, progress shows garbage, session can't be completed.

Expand Down
Loading