This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Admin/docs/admin-backend-spec.md
kirukib fce0149b1a Ship full Admin console polish and update backend spec.
Add medication Items catalogue, counter POS/QR purchase, profile/login,
detail pages, loyalty ledger, Cmd+K nav, KPI strips/charts, avatars, and
i18n; document the new APIs and open questions for the shared backend team.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 00:03:21 +03:00

426 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gishen Admin — Backend Spec Sheet
**Repo:** Gishen-Admin
**Audience:** Shared backend team (consolidate with Ecom / Mobile / B2B sheets later)
**Base URL (TBD):** `https://api.gishen.example/v1`
**Auth:** Bearer JWT for staff roles
**Mocks:** Admin SPA uses in-memory mocks until `VITE_USE_MOCKS=false` and `VITE_API_BASE_URL` point here.
## Changelog
| Date | Change |
| --- | --- |
| 2026-08-06 | Scaffold: conventions, roles, auth, empty module sections |
| 2026-08-06 | Shell + RBAC + preferredLocale |
| 2026-08-06 | Pharmacist: prescriptions, branch orders |
| 2026-08-06 | Stock + Procurement |
| 2026-08-06 | Finance, orgs create + pending self-reg approval |
| 2026-08-06 | Marketing CRM / loyalty / campaigns |
| 2026-08-06 | Dispatch board + Telegram bot events |
| 2026-08-06 | Super Admin team/audit/settings + FAQ bilingual |
| 2026-08-06 | Data migration import jobs |
| 2026-08-06 | Deployed Admin SPA: https://gishen-admin.vercel.app |
| 2026-08-06 | List UX: filters + create pages for orders, Rx, orgs, stock, team, procurement; `POST` create sketches below |
| 2026-08-06 | Finance: gateway enable/disable moved to `/admin/finance/gateways` UI route `/finance/gateways` |
| 2026-08-06 | Detail pages for orders, Rx, stock, orgs, procurement, team, CRM, FAQ, settlements; `GET /:id` + sub-resources below |
| 2026-08-06 | Dashboard / analytics / marketing / dispatch chart widgets; KPI series endpoints below |
| 2026-08-06 | Removed Service Inbox module (pages, route, nav, permissions, mocks, i18n) |
| 2026-08-06 | Topbar command palette (Cmd+K) + central page directory; register new pages in src/config/navigation.ts |
| 2026-08-06 | Sidebar consolidated into grouped, collapsible domain sections driven by the same navigation registry |
| 2026-08-06 | Staff profile page `/profile` + Yimaru-style login with demo-account picker; `avatarUrl` on staff/customers/riders |
| 2026-08-06 | Medication **Item** master catalogue (`/items`) separate from Stock; create stock picks existing item |
| 2026-08-06 | Counter **POS / scan-to-purchase** (`/pos`): customer QR, invoice association scan, tickets-app integration queue |
| 2026-08-06 | Loyalty ledger + earning activities tabs; Finance/Procurement KPI tab shells; module list KPI strips |
| 2026-08-06 | Detail pages: campaign, segment, rider, migration job, audit event, branch; approve UX (primary + overflow reject) |
| 2026-08-06 | Dashboard charts: popular products, category mix, sales trend, channel, branch, Rx funnel; "today" KPIs date-scoped |
| 2026-08-06 | Table filters: >3 filters open a modal with Apply/Cancel + active chips (`TableToolbar` declarative API) |
| 2026-08-06 | i18n: en/am parity (~666 keys), Noto Sans Ethiopic; locale-aware `formatters`; Gregorian + Amharic month names |
| 2026-08-06 | New `ModuleKey`: `pos` (pharmacist, stock_manager, operations, super_admin). Items gated by existing `stock` |
## Conventions
| Item | Convention |
| --- | --- |
| Content type | `application/json` (multipart for imports / uploads) |
| IDs | UUID v4 |
| Money | Integer ETB (major units for Admin UI demos; align platform-wide later) |
| Errors | `{ "error": { "code": string, "message": string, "details"?: object } }` |
| Pagination | `?page=&limit=` → `{ data, meta: { page, limit, total } }` |
## Staff roles
`pharmacist` · `stock_manager` · `procurement` · `finance` · `marketing_manager` · `operations` · `super_admin`
Pharmacist (and optionally stock) sessions include `branchId`.
---
## Auth & me
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| POST | `/auth/staff/login` | — | Email/password → tokens + user |
| POST | `/auth/refresh` | refresh | Rotate access |
| POST | `/auth/logout` | staff | Revoke |
| GET | `/auth/me` | staff | Current principal, roles, branchId, avatarUrl |
| PATCH | `/users/me` | staff | `{ preferredLocale?: "en"\|"am", name?, phone?, avatarUrl? }` |
| POST | `/users/me/avatar` | staff | Multipart image upload → `{ avatarUrl }` |
**Login response sketch:**
```json
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "uuid",
"name": "Hana Pharmacist",
"email": "pharmacist@gishen.et",
"role": "pharmacist",
"branchId": "uuid",
"preferredLocale": "en",
"avatarUrl": "https://cdn.example/avatars/hana.jpg"
}
}
```
Mock key: `authStore.login` → `/auth/staff/login`
UI: `/profile` (every authenticated role; not permission-gated), topbar “My profile”
---
## Orders (branch queue)
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/pharmacy/orders` | pharmacist, operations, super_admin | `?branchId=` required for pharmacist; filters: `status`, `fulfillment`, `channel`, `q` |
| POST | `/pharmacy/orders` | pharmacist, operations, super_admin | Staff-created order (call centre / walk-in) |
| GET | `/pharmacy/orders/:id` | pharmacist, operations, super_admin | Detail: header + line items + payment + fulfillment |
| GET | `/pharmacy/orders/:id/timeline` | pharmacist, operations, super_admin | Ordered status/audit events for the detail timeline |
| POST | `/orders/:id/confirm` | pharmacist, super_admin | Confirm stock |
| POST | `/orders/:id/assign-rider` | pharmacist, operations, super_admin | `{ riderId }` |
| PATCH | `/orders/:id/status` | pharmacist, operations, super_admin | Status lifecycle |
Detail response adds `items[] { sku, name, qty, unitEtb }`, `timeline[] { at, title, detail, tone }`, and resolved `rider`.
Mock: `mocks/data.orders`, `mocks/data.orderLines`, `mocks/data.orderTimeline`
UI: `/orders/:id`
---
## Prescriptions
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/pharmacy/prescriptions` | pharmacist, super_admin | Branch-scoped queue; filters: `status`, `controlled`, `q` |
| POST | `/pharmacy/prescriptions` | pharmacist, super_admin | Staff Rx intake |
| POST | `/admin/prescriptions/:id/verify` | pharmacist, super_admin | `{ status: approved\|queried\|rejected, qtyAdjustments?, substitute?, notes? }` |
| POST | `/pharmacy/prescriptions/:id/dispense` | pharmacist | `{ batch, quantity }` stamped pharmacistId + time |
| GET | `/pharmacy/prescriptions/controlled-report` | pharmacist, super_admin | Controlled-substance report |
| GET | `/pharmacy/prescriptions/:id` | pharmacist, super_admin | Detail: items, controlled flags, review trail |
| GET | `/pharmacy/refills` | pharmacist, super_admin | Days-of-supply / script expiry oversight |
UI: `/prescriptions/:id` — Approve is the sole primary header action; Reject / Query live in an overflow menu and as an inline footer on the verification section.
---
## Medication items (catalogue)
Master product identity. **Stock rows reference an Item** via `itemId`; batch/lot, manufacture date, expiry, and branch qty live on Stock only. Do not recreate a product on every goods receipt.
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/catalog/items` | stock_manager, pharmacist (read), procurement (read), super_admin | Filters: `productType`, `efdaStatus`, `mergeStatus`, `therapeuticClass`, `q` |
| POST | `/admin/catalog/items` | stock_manager, super_admin | Create item; server assigns immutable internal SKU |
| GET | `/admin/catalog/items/:id` | stock_manager, pharmacist (read), procurement (read), super_admin | Full record + ownership tags + audit |
| PATCH | `/admin/catalog/items/:id` | stock_manager, super_admin | Update (SKU immutable); field-group ownership may reject ERP-owned writes from Platform |
| GET | `/admin/catalog/items/:id/history` | stock_manager, super_admin | Change log |
| POST | `/admin/catalog/items/match` | stock_manager, super_admin | Duplicate assist body `{ genericName, strength, dosageForm, packSize, manufacturer }` → `{ candidates: [{ id, confidence, matchKey }] }` |
| POST | `/admin/catalog/items/:id/merge` | stock_manager, super_admin | `{ candidateId, status: approved\|rejected }` — reversible 30d |
| POST | `/admin/catalog/items/:id/merge/reverse` | stock_manager, super_admin | Undo approved merge within 30 days |
| GET | `/admin/catalog/items/:id/variants` | same read | Pack-size children linked to parent |
| POST | `/admin/catalog/items/:id/variants` | stock_manager, super_admin | Attach pack variant `{ packSize, barcode, sellingPriceEtb }` |
| POST | `/admin/catalog/items/:id/media` | stock_manager, super_admin | Multipart images (min 1); badges blocked for Rx/Controlled |
**Field groups (create/detail):**
1. Core identification — SKU (immutable), generic/INN, brand, manufacturer, country, EFDA number, barcodes[]
2. Classification — product type (`OTC`\|`Rx`\|`Controlled`\|`Medical device`\|`Beauty`\|`Wellness`), therapeutic class, activeIngredients[], dosage form, route, controlled schedule
3. Packaging — pack size, base UoM, sell-by vs pack unit, parent/variant links
4. Pricing — cost (ERP-owned), selling price (+ owner ERP\|Platform), currency ETB, VAT, promo eligibility (**auto-off for Rx/Controlled**, no override), B2B price-list ref
5. Regulatory — Rx-required, EFDA status, controlled flag+schedule, advertising-restriction (auto from type), age restriction
6. Content — descriptions, dosage guidance, side effects, warnings, storage, similar alternatives[] (display text only)
7. Inventory defaults — default reorder point, expiry-alert days (on-hand/reserved/incoming are derived from stock)
8. Media — images[], thumbnail, merchandising badges (**disabled for Rx/Controlled**), SEO slug/title/description
9. Dedupe — `matchKey`, confidence, mergeCandidateId, mergeStatus, approver, timestamps
10. Ownership & audit — per field-group source of truth, last ERP sync + error, created/modified by/at
11. Relationships — B2B-eligible category tag; loyalty earn/redeem eligibility (**auto-excluded for Rx/Controlled**)
Mock: `mocks/catalog.ts` (`catalogItems`, `itemChangeHistory`, …)
UI: `/items`, `/items/new`, `/items/:id`
Stock create: `POST /pharmacy/inventory` prefers `{ itemId, branchId, batch, … }` over free-typed product fields. UI deep-link `?item=`
---
## Stock & ERP
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/pharmacy/inventory` | stock_manager, pharmacist (read), procurement (read), super_admin | Per-branch stock; filters: `branchId`, `q`, `alert=low\|mismatch` |
| POST | `/pharmacy/inventory` | stock_manager, super_admin | Receive stock against catalogue item: `{ itemId, branchId, batch, manufacturedAt?, expiresAt, qty, reorderPoint? }` — must not invent a new Item |
| GET | `/integrations/erp/sync-status` | stock_manager, super_admin | Last sync |
| GET | `/admin/stock/discrepancies` | stock_manager, super_admin | Platform vs ERP |
| POST | `/admin/stock/discrepancies/:id/resolve` | stock_manager, super_admin | One-click resolve |
| GET/PUT | `/admin/stock/field-ownership` | stock_manager, super_admin | ERP vs platform field owners |
| POST | `/admin/stock/entry/suggest` | stock_manager, super_admin | Fuzzy duplicate suggestions body `{ query }` |
| GET | `/admin/stock/merge-proposals` | stock_manager, super_admin | Merge queue |
| POST | `/admin/stock/merge-proposals/:id/approve` | stock_manager, super_admin | Human approve; reversible 30d |
| GET | `/admin/stock/alerts` | stock_manager, super_admin | Low stock / expiry watchlist |
| POST | `/admin/stock/transfers` | stock_manager, super_admin | Create/approve transfer (owner) |
| GET | `/pharmacy/inventory/:sku` | stock_manager, pharmacist (read), super_admin | Detail: batch, expiry, ERP delta, linked `itemId` |
| GET | `/pharmacy/inventory/:sku/movements` | stock_manager, super_admin | Ledger: `{ at, type, qty, note }` (sale/receive/adjust/reserve) |
UI: `/stock/:sku` · create UI deep-link `?item=`
---
## Counter POS / scan-to-purchase
Staff module (`ModuleKey: pos`) for walk-in counter sales identified by customer QR. Roles: `pharmacist`, `stock_manager`, `operations`, `super_admin`.
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| POST | `/pos/resolve` | pos roles | Resolve scanned code → customer. Body `{ code }` accepts QR payload (`gishen-customer:{id}`), loyalty URL, bare `c*`, or phone digits |
| GET | `/pos/customers/:id/context` | pos roles | Name, phone, tier, points balance, last order — shown before money moves |
| POST | `/pos/purchases` | pos roles | Commit quick purchase `{ customerId, branchId, lines: [{ sku, qty }], channel?: "pos" }` → creates order, decrements stock, awards loyalty ledger entry. **Rejects / gates Controlled & Rx-only SKUs** without a valid dispensed prescription |
| GET | `/pos/earn-preview` | pos roles | `{ subtotalEtb, pointsEarn }` using active loyalty rule + tier multiplier |
| POST | `/pos/invoices/extract` | pos roles | Multipart invoice image/PDF → assisted OCR `{ lines[], confidence, warnings[] }` (operator must confirm) |
| POST | `/pos/invoices/associate` | pos roles | `{ lines[], association: { type: procurement\|supplier\|branch, id }, note? }` — purchase association against existing record |
| GET | `/pos/tickets` | pos roles | Inbound queue from external tickets / till app (`source`, `status`, payload) |
| POST | `/pos/tickets/pull` | pos roles | Pull pending from configured integration (mockable when disconnected) |
| POST | `/pos/tickets/:id/accept` | pos roles | Materialise ticket into POS purchase draft |
| POST | `/pos/tickets/:id/dismiss` | pos roles | Discard |
| GET/PUT | `/pos/integrations/tickets` | finance\|super_admin | Connection status + credentials for tickets app |
Manual code entry and a demo “simulate scan” path must work when camera/`getUserMedia` is unavailable. Camera streams must stop on unmount.
Mock: `mocks/data` POS tickets / earn rules · UI: `/pos` (tabs: Quick purchase, Invoice scan, Ticket queue, Customer codes)
---
## Procurement
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/procurement/reports` | procurement, stock_manager, super_admin | Dead-stock / fast-mover |
| GET | `/admin/procurement/reorder-recommendations` | procurement, super_admin | From velocity |
| POST | `/admin/stock/transfers/request` | procurement, super_admin | Request only; Stock Manager approves |
| GET | `/admin/procurement/requests` | procurement, stock_manager, super_admin | Reorder requests; filters `status`, `priority`, `branchId` |
| GET | `/admin/procurement/requests/:id` | procurement, stock_manager, super_admin | Detail + workflow trail |
| POST | `/admin/procurement/requests/:id/order` | procurement, super_admin | Raise PO → `status=ordered` |
| POST | `/admin/procurement/requests/:id/receive` | procurement, stock_manager, super_admin | Goods received → `status=received`, posts stock movement |
Request shape: `{ id, sku, name, qty, branchId, status: open\|ordered\|received, priority: low\|medium\|high, requestedBy, notes, createdAt }`
Mock: `mocks/data.procurementRequests` · UI: `/procurement/:id`
---
## Finance & organisations
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/finance/settlements` | finance, super_admin | Daily by channel |
| GET | `/admin/finance/reconciliations` | finance, super_admin | Matched to orders |
| GET/PUT | `/admin/finance/gateways` | finance, super_admin | Enable/disable Chapa, ArifPay, Telebirr, M-Pesa |
| POST | `/admin/finance/refunds` | finance, super_admin | Refund handling |
| GET | `/admin/finance/loyalty-liability` | finance, super_admin | Outstanding Birr liability (points × redemption rate; must reconcile with Loyalty KPI) |
| GET | `/admin/organisations` | finance, super_admin | List |
| POST | `/admin/organisations` | finance, super_admin | **Create org from Admin** (active) |
| GET | `/admin/organisations/pending` | finance, super_admin | Self-register queue |
| POST | `/admin/organisations/:id/approve` | finance, super_admin | Activate credit |
| POST | `/admin/organisations/:id/reject` | finance, super_admin | Reject |
| GET | `/admin/organisations/:id/statements` | finance, super_admin | Consolidated invoices |
| GET | `/admin/finance/settlements/:id` | finance, super_admin | Batch detail: `txnCount`, `feesEtb`, net, unmatched slips |
| POST | `/admin/finance/settlements/:id/reconcile` | finance, super_admin | Re-run reconciliation |
| GET | `/admin/organisations/:id` | finance, super_admin | Detail: credit profile + utilization |
| GET | `/admin/organisations/:id/members` | finance, super_admin | HR roster `{ name, dept, plan }` |
| GET | `/admin/organisations/:id/invoices` | finance, super_admin | `{ id, period, amountEtb, status }` |
| PATCH | `/admin/organisations/:id/credit` | finance, super_admin | `{ creditLimitEtb }` (review-gated) |
**Cross-repo:** B2B portal `POST /company/register` creates `status=pending`. Admin approve activates credit.
UI: `/organisations/:id` (Approve primary; Reject in overflow + credit-section footer), `/finance` (tabs: Settlements / Trends / Operations + gateways), `/finance/settlements/:id`, `/finance/gateways`
---
## Marketing
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/crm/customers` | marketing_manager, super_admin | Read profiles; include `avatarUrl` |
| GET | `/admin/crm/segments` | marketing_manager, super_admin | Self-updating segments |
| GET | `/admin/crm/segments/:id` | marketing_manager, super_admin | Segment detail + members + engagement series |
| GET/POST | `/admin/campaigns` | marketing_manager, super_admin | Compose/schedule SMS/Telegram/push |
| GET | `/admin/campaigns/:id` | marketing_manager, super_admin | Campaign detail: recipients, channel mix, send timeline |
| GET/PUT | `/admin/loyalty/config` | marketing_manager, super_admin | Earn/tiers/referral bonus |
| GET | `/admin/loyalty/ledger` | marketing_manager, finance, super_admin | Points movements; filters `activity`, `direction`, `branchId`, `customerId`, `q` |
| GET | `/admin/loyalty/customers/summary` | marketing_manager, finance, super_admin | Per-customer earned / redeemed / balance / tier |
| GET | `/admin/loyalty/activities` | marketing_manager, super_admin | Earning rule catalogue |
| PATCH | `/admin/loyalty/activities/:id` | marketing_manager, super_admin | Pause/activate / edit points |
| GET | `/admin/crm/customers/:id` | marketing_manager, super_admin | Detail: tier, points, recent orders, touch history, avatar |
| POST | `/admin/crm/customers/:id/points` | marketing_manager, super_admin | `{ delta, reason }` manual loyalty adjustment |
| GET | `/admin/crm/customers/:id/qr` | marketing_manager, pharmacist, pos roles, super_admin | Loyalty QR payload + printable image (PNG/SVG) |
| GET | `/admin/kpi/summary` | marketing_manager, finance, super_admin | Scoped KPIs |
### Chart / KPI series
Dashboard, Analytics, Marketing, Dispatch, Finance, and detail pages read series from these (currently `mocks/charts.ts`):
| Method | Path | Returns |
| --- | --- | --- |
| GET | `/admin/kpi/sales?range=7d\|30d` | `[{ day, sales, orders }]` — sales trend + order overlay |
| GET | `/admin/kpi/popular-products?range=` | `[{ sku, name, units, revenueEtb }]` — ranked; deep-link `/stock/:sku` |
| GET | `/admin/kpi/category-mix` | `[{ category, value }]` — share by therapeutic / product category |
| GET | `/admin/kpi/channel-mix` | `[{ name, value }]` — order share by channel |
| GET | `/admin/kpi/branch-performance` | `[{ branch, revenue, orders, rx }]` |
| GET | `/admin/kpi/rx-funnel` | `{ submitted, underReview, approved, rejected }` |
| GET | `/admin/kpi/settlement-trend` | `[{ day, chapa, telebirr, cod }]` — stacked settlement volume |
| GET | `/admin/kpi/dispatch-load` | `[{ hour, trips }]` |
| GET | `/admin/kpi/stock-movement?sku=` | `[{ week, in, out }]` |
| GET | `/admin/kpi/org-spend?orgId=` | `[{ month, spend }]` |
| GET | `/admin/kpi/customer-points?customerId=` | `[{ month, points }]` |
| GET | `/admin/kpi/loyalty-issued-redeemed` | `[{ month, earned, redeemed }]` |
| GET | `/admin/kpi/needs-attention` | Action queue: pending Rx, low stock, unassigned deliveries, pending orgs |
**Dashboard rule:** any “today” metric must filter on `createdAt`/`submittedAt` calendar day in the staff timezone. Branch-scoped roles receive branch-scoped aggregates only.
UI: `/crm/:id`, `/marketing/:id` (segment), `/campaigns/:id`, `/loyalty`
---
## Dispatch & Telegram bot
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/dispatch/board` | operations, super_admin | Paid + Rx-approved queue |
| POST | `/dispatch/trips` | operations, super_admin | Batch fulfilments |
| POST | `/dispatch/trips/:id/assign` | operations, super_admin | `{ riderId }` → emits Telegram manifest |
| POST | `/dispatch/bot/webhook` | service | Bot status + PoD callbacks |
| GET | `/dispatch/intake` | operations, super_admin | Partner/POS webhook monitor |
| GET | `/dispatch/riders` | operations, super_admin | Roster with `avatarUrl`, on-shift flag |
| POST | `/dispatch/riders` | operations, super_admin | Link Telegram account |
| GET | `/dispatch/riders/:id` | operations, super_admin | Rider detail: stats, trips, on-time series |
**Events:** `trip.assigned` → bot push; `trip.status_updated` → board + customer tracking.
UI: `/dispatch`, `/dispatch/riders/:id`
---
## FAQ (bilingual)
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/faq` | all staff | Published articles |
| POST | `/admin/faq` | marketing_manager, super_admin | Create |
| PUT | `/admin/faq/:id` | marketing_manager, super_admin | Update / publish |
| GET | `/admin/faq/:id` | all staff | Article detail (drafts visible to `faq_write`) |
| DELETE | `/admin/faq/:id` | super_admin | Delete |
Fields: `title_en`, `title_am`, `body_en`, `body_am`, `category`, `published`.
UI: `/faq/:id` — inline bilingual editor for `faq_write`, read-only otherwise.
---
## Team, audit, settings
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET/POST | `/admin/users` | super_admin | Staff + role + branch; list supports `role`, `branchScoped`, `q`; include `avatarUrl` |
| GET | `/admin/users/:id` | super_admin | Staff detail: role, branch scope, module access, sessions, avatar |
| POST | `/admin/users/:id/reset-password` | super_admin | Email reset link |
| POST | `/admin/users/:id/disable` | super_admin | Deactivate account |
| GET | `/admin/audit` | super_admin | Audit log; filters `domain`, `category`, `outcome`, `q`. `target` deep-links to entity detail |
| GET | `/admin/audit/:id` | super_admin | Event detail: actor, before/after, IP, UA, security flag |
| GET/PUT | `/admin/settings` | super_admin | Retention, access controls |
| GET | `/admin/branches` | super_admin, operations | Branch list |
| GET | `/admin/branches/:id` | super_admin, operations | Branch profile, staff, order series |
UI: `/team/:id`, `/audit/:id`, `/settings/branches/:id`
---
## Data migration / imports
| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/admin/imports/templates` | stock_manager, finance, operations, super_admin | Entity schemas |
| POST | `/admin/imports` | same | Multipart file upload |
| POST | `/admin/imports/:id/map` | same | Column mapping (+ save template) |
| POST | `/admin/imports/:id/validate` | same | Dry-run row errors |
| POST | `/admin/imports/:id/commit` | same | Upsert commit |
| GET | `/admin/imports` | same | Job history |
| GET | `/admin/imports/:id` | same | Job detail + error rows |
| CRUD | `/admin/imports/mapping-templates` | same | Saved presets |
Entity templates: `stock`, `catalog`, `hr_members`, `branches`, `riders`, `generic`.
UI: `/migrations`, `/migrations/:id`
---
## Navigation & page directory (frontend convention)
`src/config/navigation.ts` is the **single source of truth** for every navigable
destination in the Admin SPA. It feeds both the sidebar and the Cmd+K / Ctrl+K
command palette in the topbar.
**When you add a page or feature, register it there.** A route added to
`src/app/AppRoutes.tsx` but missing from the registry is unreachable by search;
an entry whose `path` has no matching route produces a broken result.
Each entry carries `id`, `title` (+ optional `labelKey` for i18next),
`description`, `path`, `module` (`ModuleKey`, used for RBAC via `canAccess`),
`icon`, `group` (`NavGroupId`), `keywords` (search synonyms), `kind`
(`page` | `action`) and `showInSidebar`.
`group` is the operational domain — Overview, Daily operations, Inventory &
supply, Finance & accounts, Customers & growth, Administration, Support, plus a
palette-only Actions group. It drives the sidebar section headers *and* the
palette headings from one field, so the two cannot drift. Labels live in
`navGroups`; `sidebarSections(role)` returns the grouped, permission-filtered
sidebar and drops any section whose children the role cannot access.
Palette results are permission-filtered with the same `roleModules` map the
route guards use, so no backend change is needed — but any new module key added
server-side must also exist in `ModuleKey` and `roleModules`.
Current SPA destinations of note: `/pos`, `/items`, `/profile` (palette + topbar;
not a gated module), Payment Gateways `/finance/gateways` (palette-only).
Entity detail routes (`orders/:id`, `stock/:sku`, `items/:id`, …) are
deliberately not registered: they require a record id. Register the list page
instead.
**List filters (UI):** toolbars with more than three filters collapse into a
modal with staged Apply/Cancel and removable active chips. Search stays inline.
Backend continues to accept flat query params; no change required.
**Avatars:** staff, customers, and riders expose optional `avatarUrl`. Clients
resolve `avatarUrl` → generated placeholder → initials. Upload: `POST /users/me/avatar`
or admin `POST /admin/users/:id/avatar`.
---
## Open questions
1. Money units: ETB major vs cents — keep consistent with Ecom/B2B sheets
2. Driver channel assumption: Telegram bot (flag if SMS/WhatsApp preferred)
3. Checkout entitlement-split: shared component across Ecom/Mobile/B2B (out of Admin UI)
4. Amharic dates: Gregorian with Amharic month names (current) vs Ethiopian calendar (`am-ET-u-ca-ethiopic`) — year differs (~2018 E.C. for 2026 G.C.); need product decision
5. Tickets-app vendor: POS integration is pluggable — confirm production partner API once chosen
6. Invoice OCR provider for `/pos/invoices/extract` (Admin currently simulates extraction)
7. Catalogue merge queue ownership: Stock Manager vs dedicated data-steward role