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 e9a8e0ffc2 Add multi-provider staff login and Rx dosing schedules.
Google, phone OTP, and Telegram join email auth; prescription lines carry frequency, intervalHours, and times, with backend spec updated for this batch.

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

41 KiB
Raw Blame History

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
2026-08-07 Doctor actor: hospital-affiliated clinicians; Admin roster under organisations; create Rx & orders; guest patients
2026-08-07 Guest / unauthenticated orders: customerType registered|guest; staff/doctor authenticated, end customer optional
2026-08-07 Rx medication schedule: frequency, intervalHours, times[] on prescription line items; Admin create/detail + dosingSchedule helpers
2026-08-07 Catalogue multi-UOM: uoms[] with conversionFactor into stock base; toStockQty / fromStockQty
2026-08-07 Catalogue storefront / mobile PDP fields + Admin detail preview (storefrontPdpPreview)
2026-08-07 Auth providers: Google, Email, Phone (OTP), Telegram — staff login UI + endpoint sketches
2026-08-07 Push batch: multi-provider staff login + Rx dosing schedule (Admin UI/mocks) + spec alignment for doctor/guest orders, multi-UOM, storefront PDP; open questions 8–17

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.

External actors (not Admin staff roles)

Actor Auth Affiliation Can
Doctor Separate doctor JWT (or shared IdP with actor=doctor) — not a pharmacy StaffRole Must belong to ≥1 hospital or clinic organisation Create prescriptions; create orders (including for guest patients without platform accounts)
End customer (guest) None required for admin/doctor-assisted checkout — Is the subject of an order/Rx; may later claim/merge into a registered CRM profile

Doctors cannot manage platform catalogue, team RBAC, finance settlements, or other hospitals’ rosters. AuthZ is scoped to their hospital org(s).


Auth & me

Staff Admin authenticates with Bearer JWT. Login supports multiple identity providers; all paths mint (or exchange into) the same staff access/refresh pair and GET /auth/me principal.

Method Path Roles Description
POST /auth/staff/login — Email/password → tokens + user
POST /auth/staff/oauth/google — Exchange Google ID token (GIS / OAuth) → staff tokens + user
POST /auth/staff/phone/start — { phone } → start OTP (rate-limited); { challengeId, expiresIn }
POST /auth/staff/phone/verify — { challengeId, code } → tokens + user when phone is linked to a staff account
POST /auth/staff/telegram — Telegram Login Widget payload (hash-verified) → tokens + user
POST /auth/refresh refresh Rotate access
POST /auth/logout staff Revoke
GET /auth/me staff Current principal, roles, branchId, avatarUrl, authProvider
PATCH /users/me staff { preferredLocale?: "en"|"am", name?, phone?, avatarUrl? }
POST /users/me/avatar staff Multipart image upload → { avatarUrl }

Auth providers

Provider Flow Staff Admin Doctor / external
Email POST /auth/staff/login with password (magic-link optional later — not in Admin mock) Primary for pharmacy staff Doctor uses POST /auth/doctor/login (email/invite); same IdP may be shared later with actor=doctor
Google Browser Google Identity Services → ID token → POST /auth/staff/oauth/google Allowed when staff email is in Google Workspace / allow-listed domain TBD for doctors (hospital Google vs separate realm)
Phone (OTP) phone/start → SMS/WhatsApp OTP → phone/verify Staff accounts with verified phone on user profile Useful for guest order claim / patient flows (out of Admin staff session); doctor phone login TBD
Telegram Telegram Login Widget on Admin SPA → server verifies hash with bot token → POST /auth/staff/telegram Optional for staff who linked Telegram id; also used for rider dispatch identity (see Dispatch) Not primary for doctor clinical auth

Login response sketch:

{
  "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",
    "authProvider": "email"
  }
}

authProvider values: email | google | phone | telegram.

Google body sketch: { "idToken": "..." }
Phone start: { "phone": "+251911000000" }
Phone verify: { "challengeId": "uuid", "code": "123456" }
Telegram body: widget fields (id, first_name, username, auth_date, hash, …) as returned by the Login Widget.

Mock keys:

  • authStore.login → /auth/staff/login
  • authStore.loginWithProvider('google'|'phone'|'telegram') → oauth / phone verify / telegram (Admin SPA mock signs in as demo pharmacist unless overridden)

UI: /login (provider buttons + email form + phone OTP step + Telegram CTA + demo picker), /profile (every authenticated role; not permission-gated), topbar “My profile”

Env placeholders:

Side Vars
Client (Admin SPA / .env.example) VITE_GOOGLE_CLIENT_ID, VITE_TELEGRAM_BOT_USERNAME — empty = mock provider UX
Server only (never ship to Vite) Google token audience / Workspace allow-list; TELEGRAM_BOT_TOKEN (Login Widget hash verify); SMS/OTP gateway credentials (AfricasTalking / Twilio / …)

Typed on client as ImportMetaEnv in src/vite-env.d.ts.


Orders (branch queue)

Method Path Roles Description
GET /pharmacy/orders pharmacist, operations, super_admin ?branchId= required for pharmacist; filters: status, fulfillment, channel, customerType, q
POST /pharmacy/orders pharmacist, operations, super_admin, doctor Staff- or doctor-created order (call centre / walk-in / clinical). Supports guest customers
GET /pharmacy/orders/:id pharmacist, operations, super_admin, doctor (own) Detail: header + line items + payment + fulfillment + customer type
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

Guest / unauthenticated end-customer orders

The authenticated principal is always staff or a doctor. The end customer may be unauthenticated:

Field Notes
customerType registered | guest (required on create)
customerId Required when registered; omit when guest
customerName Display name (guest walk-in / phone caller / patient)
customerPhone Required for guests; recommended for registered (denormalised)
notes Optional staff/doctor free text
doctorId / orgId Optional; set when a Doctor actor initiates the order

Limits (proposed): guest orders must include a valid phone; controlled SKUs still require an approved Rx; no loyalty earn by default for guests unless a later claim/merge attaches the order to a CRM profile. Doctors may place guest orders for patients without accounts, scoped to their hospital.

Create body sketch:

{
  "customerType": "guest",
  "customerName": "Walk-in · Aster",
  "customerPhone": "+251911880011",
  "notes": "Caregiver collecting",
  "branchId": "uuid",
  "fulfillment": "pickup",
  "channel": "doctor",
  "doctorId": "uuid",
  "lines": [{ "sku": "PCM-500", "qty": 20 }]
}

Detail response adds items[] { sku, name, qty, unitEtb }, timeline[] { at, title, detail, tone }, resolved rider, and optional doctor / organisation.

Mock: mocks/data.orders, mocks/data.orderLines, mocks/data.orderTimeline
UI: /orders/new (Guest customer vs Registered customer toggle), /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, doctor Staff Rx intake or doctor-authored Rx; body includes doctorId when attributed
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, doctor (own) Detail: items (incl. dosing schedule), controlled flags, review trail, linked doctor
GET /pharmacy/refills pharmacist, super_admin Days-of-supply / script expiry oversight

UI: /prescriptions/new (doctor select + per-line dosing schedule), /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. Medications table shows interval/frequency and dose times.

Prescription medication line items (schedule)

Each items[] entry on create / detail carries dosing instructions used by pharmacist counsel and mobile dose reminders. Clock times use the same shape as mobile Reminder.times (HH:mm strings, 24h). Backend should persist the pharmacist’s final times list (and intervalHours for QXH) so the patient app can seed reminders without re-deriving.

Field Type Notes
name string Medicine label / strength as entered
qty number Dispense quantity
controlled? boolean Controlled-substance flag
frequency? QD | BID | TID | QID | QXH | custom Once / 2× / 3× / 4× daily, every N hours, or custom clock list
intervalHours? number Required when frequency === "QXH" (hours between doses, ≥ 1)
times? string[] Clock times HH:mm (24h), e.g. ["08:00","14:00","20:00"]. Admin intake requires ≥ 1 time

Frequency → default suggested times (Admin defaultTimesForFrequency; pharmacist may add/remove slots):

Frequency Default times
QD 08:00
BID 08:00, 20:00
TID 08:00, 14:00, 20:00
QID 08:00, 12:00, 16:00, 20:00
QXH Generated from intervalHours starting 08:00 until midnight (default interval 8h if omitted in UI)
custom Seed 08:00; pharmacist edits freely

Create body sketch: POST /pharmacy/prescriptions / POST /doctor/prescriptions:

{
  "customerName": "Guest patient · no account",
  "branchId": "uuid",
  "doctorId": "uuid",
  "orgId": "uuid",
  "items": [
    {
      "name": "Amoxicillin 500mg",
      "qty": 21,
      "controlled": false,
      "frequency": "TID",
      "times": ["08:00", "14:00", "20:00"]
    },
    {
      "name": "Paracetamol 500mg",
      "qty": 20,
      "frequency": "QXH",
      "intervalHours": 6,
      "times": ["08:00", "14:00", "20:00"]
    }
  ]
}

Rx header also stores denormalised prescriber (doctor display name). Patient on Admin intake may be a guest name (no CRM customerId required).

Helper (Admin): src/lib/dosingSchedule.ts — DOSING_FREQUENCY_OPTIONS, defaultTimesForFrequency, formatFrequencyLabel, formatDoseTimes.

Mock: mocks/data.prescriptions · Types: PrescriptionItem, DosingFrequency
UI: create sets schedule per line; detail medications table shows frequency label + dose times.


Doctors (hospital-affiliated actors)

Doctors are not floating platform users. Onboarding is via a hospital or clinic organisation (orgType). Finance / super_admin manage the roster in Admin; doctors authenticate separately to create clinical artefacts.

Method Path Roles Description
GET /admin/doctors finance, super_admin; pharmacist (read for attribution) Filters: orgId, status, q
POST /admin/doctors finance, super_admin Invite/create { orgId, name, specialty, licenseNumber, phone, email, branchId?, sendInvite? } — rejects if org is not hospital/clinic
GET /admin/doctors/:id finance, super_admin; pharmacist (read) Profile + recent Rx/orders
PATCH /admin/doctors/:id finance, super_admin Update profile / deactivate
POST /admin/doctors/:id/invite finance, super_admin Provision or re-send linked login
GET /admin/organisations/:id/doctors finance, super_admin Doctors for one hospital org
POST /auth/doctor/login — Doctor email/password or invite token → doctor JWT
GET /doctor/me doctor Principal + hospital memberships
POST /doctor/prescriptions doctor Create Rx (patient may be guest name-only)
POST /doctor/orders doctor Create order for registered or guest patient; hospital-scoped

AuthZ notes

  • Doctor tokens are scoped to their orgId(s); cannot read other hospitals’ doctors or orgs
  • Cannot mutate catalogue, stock master, finance, team, or audit
  • Pharmacist verification remains the gate for controlled dispensing
  • Admin UI module: existing organisations permission (finance + super_admin)

Fields: id, name, specialty, licenseNumber, phone, email, status (active|inactive), orgId, orgName, branchId?, hasLinkedAccount, createdAt, updatedAt, avatarUrl?

Mock: mocks/data.doctors
UI: /doctors, /doctors/new?orgId=, /doctors/:id; hospital org detail → Add doctor


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, baseUnit (stock UOM), sellByUnit, packUnit, uoms[] multi-UOM table, 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, optional compareAtPriceEtb
  5. Regulatory — Rx-required, EFDA status, controlled flag+schedule, advertising-restriction (auto from type), age restriction
  6. Content — short description, dosage guidance, side effects (free text + structured mild/severe), warnings, storage (+ structured storageNotes[]), directions[], pharmacistTip, useCase, similar alternatives[]
  7. Storefront / mobile PDP — fields below; Admin detail shows a live preview shaped for Gishen mobile
  8. Inventory defaults — default reorder point, expiry-alert days (on-hand/reserved/incoming are derived from stock)
  9. Media — images[], thumbnail, merchandising badges (disabled for Rx/Controlled), SEO slug/title/description
  10. Dedupe — matchKey, confidence, mergeCandidateId, mergeStatus, approver, timestamps
  11. Ownership & audit — per field-group source of truth, last ERP sync + error, created/modified by/at
  12. Relationships — B2B-eligible category tag; loyalty earn/redeem eligibility (auto-excluded for Rx/Controlled)

Multi-UOM (ERPNext-style)

Warehouse quantity is always counted in the stock / base UOM (baseUnit). Additional purchase/sales units convert via conversionFactor = how many stock units equal one of that UOM. The stock UOM row always has conversionFactor: 1.

Field Type Notes
uoms[] ItemUom[] Allowed units for the item
uoms[].uom string e.g. tablet, strip, box
uoms[].conversionFactor number Stock qty per 1 of this UOM
uoms[].isStockUom? boolean Exactly one true; matches baseUnit
uoms[].isPurchaseUom? boolean Used on PO / receive
uoms[].isSalesUom? boolean Used on sales / POS lines

Helpers (Admin mock / shared): resolveItemUoms (falls back to buildDefaultUoms from pack labels when uoms empty), toStockQty(qty, fromUom, item), fromStockQty(stockQty, toUom, item), stockUomOf(item).

API expectation: POST/PATCH /admin/catalog/items accept uoms; stock receive / POS / order lines may submit qty in a sales/purchase UOM — server converts with toStockQty before decrementing inventory. Reject unknown UOM labels.

Example: 1 strip = 10 tablet → selling 2 strips deducts 20 stock units.

Storefront / mobile PDP fields

Aligned with Gishen mobile product detail (product/[id]). Admin create/edit captures these; detail page previews via storefrontPdpPreview(item).

Field Notes
storefrontUnit? Shelf / pack label shown as mobile product.unit (e.g. 10 capsules/strip); falls back to packSize / sellByUnit
useCase? Chip above name (e.g. Pain & Fever); falls back to therapeutic class
compareAtPriceEtb? Strike-through compare-at price
sideEffectsMild? / sideEffectsSevere? Structured lists; mild falls back to parsing free-text sideEffects
directions? { title, body }[] stepper; falls back from dosageGuidance + warnings
storageNotes? { title, body }[] cards; falls back from storageInstructions
pharmacistTip? Callout; falls back to warnings

Preview mapping also exposes name, brand, slug, sku, price, images, Rx flag, badge, form, generic, drug class, ingredient strengths.

Mock: mocks/catalog.ts (catalogItems, itemChangeHistory, ItemUom, …)
UI: /items, /items/new (multi-UOM editor + PDP fields), /items/:id (UOM table + Storefront / Mobile PDP section)
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, uom?, reorderPoint? } — qty may be in a purchase UOM; server converts via item uoms → stock base. 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; filter orgType, status, source
POST /admin/organisations finance, super_admin Create org from Admin (active); body includes orgType
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 + orgType
GET /admin/organisations/:id/members finance, super_admin HR roster { name, dept, plan }
GET /admin/organisations/:id/doctors finance, super_admin Hospital/clinic doctor roster (empty for other types)
GET /admin/organisations/:id/invoices finance, super_admin { id, period, amountEtb, status }
PATCH /admin/organisations/:id/credit finance, super_admin { creditLimitEtb } (review-gated)

orgType: corporate · hospital · clinic · ngo · other. Doctor invitation is only valid for hospital / clinic.

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; Add doctor on hospital/clinic), /doctors, /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, /doctors (orgs module — hospital/clinic roster), /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
  8. Doctor identity provider: dedicated doctor realm vs B2B portal users with role=doctor under hospital orgs
  9. Guest order claim: can a later registered customer attach historical guest orders by phone OTP? Retention / masking of guest PII?
  10. Guest order ceilings: max ETB / controlled-item rules without registered identity — confirm with compliance
  11. Multi-hospital doctors: allow one clinician affiliation to multiple hospital orgs?
  12. Multi-UOM source of truth: Platform-editable vs ERP-synced conversion factors — which field-group ownership wins on conflict?
  13. Mobile reminder sync: push dose times on Rx approve vs on first dispense / patient claim?
  14. Google OAuth: production Web client ID + allowed staff email domains / Workspace org for Admin
  15. Telegram Login Widget: production bot username and whether staff must pre-link Telegram id in Admin before first login
  16. OTP provider for staff phone login: AfricasTalking vs Twilio vs local SMS gateway; shared with guest-order claim OTP?
  17. Should doctor login reuse Google / phone / Telegram providers or stay email+invite only?