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

26 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

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:

{
  "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