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/src/mocks/data.ts
kirukib d55c88c999 Add prescription review flagging with Admin UI and specs.
Pharmacists can flag and resolve Rx risk/ops issues on the queue and detail pages; master and admin backend sheets document the flags contract (Mob still TBD).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 17:05:35 +03:00

3627 lines
148 KiB
TypeScript
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.

import { portraitFor } from '@/lib/avatar'
import type {
Branch,
Doctor,
FaqArticle,
ModuleKey,
Order,
Organisation,
Prescription,
StaffRole,
} from '@/types'
export const branches: Branch[] = [
{ id: 'br-bole', name: 'Bole Branch', zone: 'Bole', phone: '+251 930 099 850' },
{ id: 'br-cmc', name: 'CMC Branch', zone: 'CMC', phone: '+251 930 099 851' },
{ id: 'br-piassa', name: 'Piassa Branch', zone: 'Piassa', phone: '+251 930 099 852' },
]
/** `avatarUrl` is optional throughout — a real uploaded photo URL drops in unchanged. */
export const riders = [
{ id: 'rd-1', name: 'Kidus Rider', phone: '+251911000111', telegramId: '@kidus_gishen', branchId: 'br-bole', zone: 'Bole', avatarUrl: portraitFor('Kidus Rider') },
{ id: 'rd-2', name: 'Meron Rider', phone: '+251911000222', telegramId: '@meron_gishen', branchId: 'br-cmc', zone: 'CMC', avatarUrl: portraitFor('Meron Rider') },
// Samuel has no photo on file yet, alongside the missing vehicle plate in `configIssues`.
{ id: 'rd-3', name: 'Samuel Rider', phone: '+251911000333', telegramId: '@sam_gishen', branchId: 'br-bole', zone: 'Bole', avatarUrl: portraitFor('Samuel Rider') },
]
export let prescriptions: Prescription[] = [
{
id: 'rx-1001',
customerName: 'Abebe Kebede',
branchId: 'br-bole',
status: 'under_review',
items: [
{
name: 'Amoxicillin 500mg',
qty: 21,
frequency: 'TID',
times: ['08:00', '14:00', '20:00'],
},
{
name: 'Paracetamol 500mg',
qty: 10,
frequency: 'QXH',
intervalHours: 6,
times: ['08:00', '14:00', '20:00'],
},
],
submittedAt: '2026-08-06T08:10:00Z',
prescriber: 'Dr. Selam',
flags: [
{
id: 'rxf-1001-1',
code: 'interaction_concern',
severity: 'critical',
note: 'Amoxicillin + high-dose Paracetamol — confirm hepatic risk with patient history.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-06T08:25:00Z',
status: 'open',
},
],
},
{
id: 'rx-1002',
customerName: 'Marta Hailu',
branchId: 'br-bole',
status: 'submitted',
items: [
{
name: 'Metformin 850mg',
qty: 60,
controlled: false,
frequency: 'BID',
times: ['08:00', '20:00'],
},
],
submittedAt: '2026-08-06T09:20:00Z',
prescriber: 'Dr. Yohannes',
flags: [
{
id: 'rxf-1002-1',
code: 'incomplete_rx',
severity: 'warning',
note: 'Duration / days supply missing from scan crop.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-06T09:35:00Z',
status: 'open',
},
],
},
{
id: 'rx-1003',
customerName: 'Tigist Alemu',
branchId: 'br-cmc',
status: 'under_review',
items: [
{
name: 'Tramadol 50mg',
qty: 20,
controlled: true,
frequency: 'QID',
times: ['08:00', '12:00', '16:00', '20:00'],
},
],
submittedAt: '2026-08-06T07:45:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-08-06T08:05:00Z',
flags: [
{
id: 'rxf-1003-1',
code: 'controlled_substance',
severity: 'warning',
note: 'Schedule stamp + ID check required before dispense.',
createdBy: 'Rediet Tesfa',
createdAt: '2026-08-06T08:08:00Z',
status: 'open',
},
{
id: 'rxf-1003-2',
code: 'stock_shortage',
severity: 'warning',
note: 'CMC has 8 tablets on hand — restock or transfer from Bole.',
createdBy: 'Rediet Tesfa',
createdAt: '2026-08-06T08:12:00Z',
status: 'open',
},
],
},
{
id: 'rx-1004',
customerName: 'Dawit Lemma',
branchId: 'br-piassa',
status: 'submitted',
items: [
{
name: 'Salbutamol inhaler',
qty: 2,
frequency: 'custom',
times: ['08:00', '14:00', '21:00'],
},
],
submittedAt: '2026-08-05T16:30:00Z',
prescriber: 'Dr. Kalkidan',
},
{
id: 'rx-1005',
customerName: 'Hiwot Bekele',
branchId: 'br-cmc',
status: 'queried',
items: [{ name: 'Amlodipine 5mg', qty: 30, frequency: 'QD', times: ['08:00'] }],
submittedAt: '2026-08-05T11:15:00Z',
prescriber: 'Dr. Yohannes',
reviewStartedAt: '2026-08-05T11:40:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-05T12:05:00Z',
decisionNote: 'Prescriber signature is illegible — a clearer scan was requested.',
flags: [
{
id: 'rxf-1005-1',
code: 'needs_clarification',
severity: 'warning',
note: 'Signature illegible — contacted Dr. Yohannes for confirmation.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-05T11:50:00Z',
status: 'open',
},
{
id: 'rxf-1005-2',
code: 'incomplete_rx',
severity: 'info',
note: 'Initial crop missed the footer stamp; customer re-uploaded.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-05T11:42:00Z',
status: 'resolved',
resolvedBy: 'Hana Pharmacist',
resolvedAt: '2026-08-05T12:00:00Z',
resolutionNote: 'Clearer scan received; signature still queried separately.',
},
],
},
{
id: 'rx-1006',
customerName: 'Netsanet Desta',
branchId: 'br-bole',
status: 'approved',
items: [{ name: 'Cetirizine 10mg', qty: 20, frequency: 'QD', times: ['21:00'] }],
submittedAt: '2026-08-04T09:10:00Z',
prescriber: 'Dr. Selam',
reviewStartedAt: '2026-08-04T09:25:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-04T09:48:00Z',
},
{
id: 'rx-1007',
customerName: 'Almaz Girma',
branchId: 'br-bole',
status: 'approved',
items: [
{ name: 'Metformin 850mg', qty: 60, frequency: 'BID', times: ['08:00', '20:00'] },
{ name: 'Amlodipine 5mg', qty: 30, frequency: 'QD', times: ['08:00'] },
],
submittedAt: '2026-08-03T14:05:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-08-03T14:20:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-03T14:52:00Z',
},
{
id: 'rx-1008',
customerName: 'Bekele Tesfaye',
branchId: 'br-cmc',
status: 'rejected',
items: [
{
name: 'Diazepam 5mg',
qty: 14,
controlled: true,
frequency: 'BID',
times: ['08:00', '20:00'],
},
],
submittedAt: '2026-08-02T10:40:00Z',
prescriber: 'Dr. Unknown',
reviewStartedAt: '2026-08-02T11:00:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-02T11:22:00Z',
decisionNote: 'Prescriber is not on the national register.',
flags: [
{
id: 'rxf-1008-1',
code: 'fraud_suspicion',
severity: 'critical',
note: 'Prescriber name not on national register; possible forged script.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-02T11:05:00Z',
status: 'resolved',
resolvedBy: 'Hana Pharmacist',
resolvedAt: '2026-08-02T11:22:00Z',
resolutionNote: 'Rx rejected; incident logged for compliance.',
},
{
id: 'rxf-1008-2',
code: 'controlled_substance',
severity: 'warning',
note: 'Diazepam — do not dispense pending fraud check.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-02T11:02:00Z',
status: 'resolved',
resolvedBy: 'Hana Pharmacist',
resolvedAt: '2026-08-02T11:22:00Z',
resolutionNote: 'Closed with reject decision.',
},
],
},
{
id: 'rx-1009',
customerName: 'Yeshi Lemma',
branchId: 'br-bole',
status: 'approved',
items: [{ name: 'Ferrous sulphate 200mg', qty: 30, frequency: 'QD', times: ['08:00'] }],
submittedAt: '2026-08-01T08:20:00Z',
prescriber: 'Dr. Selam',
reviewStartedAt: '2026-08-01T08:35:00Z',
reviewedBy: 'Selamawit Girma',
reviewedAt: '2026-08-01T09:02:00Z',
},
{
id: 'rx-1010',
customerName: 'Sofia Ahmed',
branchId: 'br-bole',
status: 'approved',
items: [{ name: 'Azithromycin 250mg', qty: 6, frequency: 'QD', times: ['08:00'] }],
submittedAt: '2026-07-31T13:45:00Z',
prescriber: 'Dr. Yohannes',
reviewStartedAt: '2026-07-31T14:00:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-07-31T14:18:00Z',
},
{
id: 'rx-1011',
customerName: 'Genet Abera',
branchId: 'br-piassa',
status: 'approved',
items: [
{
name: 'Hydrocortisone cream 1%',
qty: 2,
frequency: 'BID',
times: ['08:00', '20:00'],
},
],
submittedAt: '2026-07-30T11:05:00Z',
prescriber: 'Dr. Kalkidan',
reviewStartedAt: '2026-07-30T11:20:00Z',
reviewedBy: 'Nardos Alemu',
reviewedAt: '2026-07-30T11:44:00Z',
},
{
id: 'rx-1012',
customerName: 'Solomon Wolde',
branchId: 'br-cmc',
status: 'queried',
items: [
{
name: 'Ibuprofen 400mg',
qty: 30,
frequency: 'QXH',
intervalHours: 8,
times: ['08:00', '16:00'],
},
],
submittedAt: '2026-07-29T15:20:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-07-29T15:35:00Z',
reviewedBy: 'Rediet Tesfa',
reviewedAt: '2026-07-29T15:58:00Z',
decisionNote: 'Dosage exceeds the maximum daily limit — prescriber clarification requested.',
},
{
id: 'rx-1013',
customerName: 'Rahel Tadesse',
branchId: 'br-bole',
status: 'approved',
items: [
{
name: 'Amoxicillin 500mg',
qty: 21,
frequency: 'TID',
times: ['08:00', '14:00', '20:00'],
},
],
submittedAt: '2026-07-28T09:30:00Z',
prescriber: 'Dr. Selam',
reviewStartedAt: '2026-07-28T09:45:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-07-28T10:05:00Z',
},
{
id: 'rx-1014',
customerName: 'Meseret Kassa',
branchId: 'br-cmc',
status: 'approved',
items: [
{
name: 'Oral rehydration salts',
qty: 10,
frequency: 'custom',
times: ['08:00', '12:00', '16:00', '20:00'],
},
],
submittedAt: '2026-07-26T16:10:00Z',
prescriber: 'Dr. Yohannes',
reviewStartedAt: '2026-07-26T16:25:00Z',
reviewedBy: 'Rediet Tesfa',
reviewedAt: '2026-07-26T16:40:00Z',
},
{
id: 'rx-1015',
customerName: 'Tesfaye Negash',
branchId: 'br-bole',
status: 'rejected',
items: [
{
name: 'Tramadol 50mg',
qty: 40,
controlled: true,
frequency: 'QID',
times: ['08:00', '12:00', '16:00', '20:00'],
},
],
submittedAt: '2026-07-24T12:00:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-07-24T12:15:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-07-24T12:38:00Z',
decisionNote: 'Controlled substance quantity is not supported by the diagnosis on file.',
},
{
id: 'rx-1016',
customerName: 'Kalkidan Berhe',
branchId: 'br-piassa',
status: 'approved',
items: [
{
name: 'Paracetamol 500mg',
qty: 20,
frequency: 'QXH',
intervalHours: 6,
times: ['06:00', '12:00', '18:00'],
},
],
submittedAt: '2026-07-22T10:15:00Z',
prescriber: 'Dr. Kalkidan',
reviewStartedAt: '2026-07-22T10:30:00Z',
reviewedBy: 'Nardos Alemu',
reviewedAt: '2026-07-22T10:47:00Z',
},
{
id: 'rx-1017',
customerName: 'Eyob Mekonnen',
branchId: 'br-bole',
status: 'approved',
items: [
{ name: 'Vitamin C 1000mg', qty: 2, frequency: 'QD', times: ['08:00'] },
{ name: 'Ferrous sulphate 200mg', qty: 30, frequency: 'QD', times: ['08:00'] },
],
submittedAt: '2026-07-20T14:40:00Z',
prescriber: 'Dr. Selam',
reviewStartedAt: '2026-07-20T14:55:00Z',
reviewedBy: 'Selamawit Girma',
reviewedAt: '2026-07-20T15:12:00Z',
},
{
id: 'rx-1018',
customerName: 'Fikru Assefa',
branchId: 'br-cmc',
status: 'approved',
items: [{ name: 'Metformin 850mg', qty: 60, frequency: 'BID', times: ['08:00', '20:00'] }],
submittedAt: '2026-07-17T09:05:00Z',
prescriber: 'Dr. Yohannes',
reviewStartedAt: '2026-07-17T09:20:00Z',
reviewedBy: 'Rediet Tesfa',
reviewedAt: '2026-07-17T09:41:00Z',
},
{
id: 'rx-1019',
customerName: 'Mulugeta Fikadu',
branchId: 'br-bole',
status: 'approved',
items: [
{
name: 'Salbutamol inhaler',
qty: 1,
frequency: 'custom',
times: ['08:00', '14:00', '21:00'],
},
],
submittedAt: '2026-07-14T17:25:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-07-14T17:40:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-07-14T17:58:00Z',
},
{
id: 'rx-1020',
customerName: 'Selam Getahun',
branchId: 'br-piassa',
status: 'rejected',
items: [{ name: 'Azithromycin 250mg', qty: 12, frequency: 'QD', times: ['08:00'] }],
submittedAt: '2026-07-11T11:50:00Z',
prescriber: 'Dr. Kalkidan',
reviewStartedAt: '2026-07-11T12:05:00Z',
reviewedBy: 'Nardos Alemu',
reviewedAt: '2026-07-11T12:26:00Z',
decisionNote: 'Duplicate submission — rx-1010 already covers this course.',
},
{
id: 'rx-1021',
customerName: 'Zenebech Haile',
branchId: 'br-cmc',
status: 'approved',
items: [{ name: 'Cetirizine 10mg', qty: 30, frequency: 'QD', times: ['21:00'] }],
submittedAt: '2026-07-09T08:45:00Z',
prescriber: 'Dr. Yohannes',
reviewStartedAt: '2026-07-09T09:00:00Z',
reviewedBy: 'Rediet Tesfa',
reviewedAt: '2026-07-09T09:19:00Z',
},
{
id: 'rx-1022',
customerName: 'Abebe Kebede',
branchId: 'br-bole',
status: 'approved',
items: [
{
name: 'Amoxicillin 500mg',
qty: 21,
frequency: 'TID',
times: ['08:00', '14:00', '20:00'],
},
],
submittedAt: '2026-08-06T07:20:00Z',
prescriber: 'Dr. Selam',
reviewStartedAt: '2026-08-06T07:35:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-06T07:52:00Z',
},
{
id: 'rx-1023',
customerName: 'Marta Hailu',
branchId: 'br-bole',
status: 'approved',
items: [
{
name: 'Tramadol 50mg',
qty: 20,
controlled: true,
frequency: 'QID',
times: ['08:00', '12:00', '16:00', '20:00'],
},
],
submittedAt: '2026-08-05T15:10:00Z',
prescriber: 'Dr. Helen',
reviewStartedAt: '2026-08-05T15:25:00Z',
reviewedBy: 'Hana Pharmacist',
reviewedAt: '2026-08-05T15:58:00Z',
decisionNote: 'Controlled register entry signed; dispense against photo ID only.',
},
]
export let orders: Order[] = [
{
id: 'ord-5001',
customerName: 'Abebe Kebede',
branchId: 'br-bole',
fulfillment: 'delivery',
status: 'confirmed',
totalEtb: 1250,
channel: 'web',
zone: 'Bole',
address: 'Bole Atlas, Addis Ababa',
createdAt: '2026-08-06T08:30:00Z',
paid: true,
rxApproved: true,
},
{
id: 'ord-5002',
customerName: 'Guest · 0911223344',
branchId: 'br-bole',
fulfillment: 'pickup',
status: 'picking',
totalEtb: 420,
channel: 'telegram',
createdAt: '2026-08-06T09:00:00Z',
paid: true,
},
{
id: 'ord-5003',
customerName: 'Bank staff · Hiwot',
branchId: 'br-cmc',
fulfillment: 'delivery',
status: 'confirmed',
totalEtb: 3400,
channel: 'b2b',
zone: 'CMC',
address: 'CMC Road',
createdAt: '2026-08-06T10:00:00Z',
paid: true,
rxApproved: true,
},
{
id: 'ord-5004',
customerName: 'Netsanet D.',
branchId: 'br-bole',
fulfillment: 'delivery',
status: 'out_for_delivery',
totalEtb: 890,
channel: 'mobile',
zone: 'Bole',
address: 'Gerji Mebrat Hail',
assignedRiderId: 'rd-1',
createdAt: '2026-08-06T07:15:00Z',
paid: true,
rxApproved: true,
},
{ id: 'ord-5005', customerName: 'Rahel Tadesse', branchId: 'br-bole', fulfillment: 'delivery', status: 'confirmed', totalEtb: 420, channel: 'mobile', zone: 'Bole', address: 'Bole Rwanda', createdAt: '2026-08-06T06:40:00Z', paid: true, rxApproved: true },
{ id: 'ord-5006', customerName: 'Guest · 0913445566', branchId: 'br-cmc', fulfillment: 'pickup', status: 'picking', totalEtb: 300, channel: 'web', createdAt: '2026-08-06T07:50:00Z', paid: true },
{ id: 'ord-5007', customerName: 'Dawit Lemma', branchId: 'br-piassa', fulfillment: 'delivery', status: 'pending', totalEtb: 420, channel: 'telegram', zone: 'Piassa', address: 'Piassa Adwa Ave', createdAt: '2026-08-06T10:40:00Z', paid: false },
{ id: 'ord-5008', customerName: 'Almaz Girma', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 458, channel: 'web', zone: 'Bole', address: 'Bole Medhanialem', assignedRiderId: 'rd-1', createdAt: '2026-08-05T08:20:00Z', paid: true, rxApproved: true },
{ id: 'ord-5009', customerName: 'Horizon Bank · Hiwot', branchId: 'br-cmc', fulfillment: 'pickup', status: 'delivered', totalEtb: 3480, channel: 'b2b', createdAt: '2026-08-05T13:10:00Z', paid: true },
{ id: 'ord-5010', customerName: 'Sofia Ahmed', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 280, channel: 'mobile', zone: 'Bole', address: 'Gerji Roba', assignedRiderId: 'rd-3', createdAt: '2026-08-04T15:25:00Z', paid: true },
{ id: 'ord-5011', customerName: 'Bekele Tesfaye', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 436, channel: 'web', zone: 'CMC', address: 'CMC Michael', assignedRiderId: 'rd-2', createdAt: '2026-08-04T11:05:00Z', paid: true, rxApproved: true },
{ id: 'ord-5012', customerName: 'Genet Abera', branchId: 'br-piassa', fulfillment: 'pickup', status: 'delivered', totalEtb: 480, channel: 'telegram', createdAt: '2026-08-03T09:35:00Z', paid: true },
{ id: 'ord-5013', customerName: 'Solomon Wolde', branchId: 'br-cmc', fulfillment: 'delivery', status: 'cancelled', totalEtb: 380, channel: 'web', zone: 'CMC', address: 'CMC Summit', createdAt: '2026-08-02T14:50:00Z', paid: false },
{ id: 'ord-5014', customerName: 'Yeshi Lemma', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 540, channel: 'mobile', zone: 'Bole', address: 'Bole Atlas', assignedRiderId: 'rd-1', createdAt: '2026-08-02T10:15:00Z', paid: true },
{ id: 'ord-5015', customerName: 'Netsanet Desta', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 460, channel: 'web', createdAt: '2026-08-01T16:20:00Z', paid: true },
{ id: 'ord-5016', customerName: 'Meseret Kassa', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 330, channel: 'telegram', zone: 'CMC', address: 'CMC Road', assignedRiderId: 'rd-2', createdAt: '2026-08-01T12:40:00Z', paid: true },
{ id: 'ord-5017', customerName: 'Abebe Kebede', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 492, channel: 'web', zone: 'Bole', address: 'Bole Atlas', assignedRiderId: 'rd-3', createdAt: '2026-07-31T09:50:00Z', paid: true, rxApproved: true },
{ id: 'ord-5018', customerName: 'Kalkidan Berhe', branchId: 'br-piassa', fulfillment: 'delivery', status: 'delivered', totalEtb: 185, channel: 'mobile', zone: 'Piassa', address: 'Piassa Arada', assignedRiderId: 'rd-3', createdAt: '2026-07-30T17:05:00Z', paid: true },
{ id: 'ord-5019', customerName: 'Horizon Bank · Daniel', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 6080, channel: 'b2b', zone: 'CMC', address: 'Horizon Bank HQ', assignedRiderId: 'rd-2', createdAt: '2026-07-30T10:20:00Z', paid: true },
{ id: 'ord-5020', customerName: 'Tigist Alemu', branchId: 'br-cmc', fulfillment: 'pickup', status: 'delivered', totalEtb: 326, channel: 'telegram', createdAt: '2026-07-29T15:40:00Z', paid: true },
{ id: 'ord-5021', customerName: 'Marta Hailu', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 1680, channel: 'mobile', zone: 'Bole', address: 'Bole Rwanda', assignedRiderId: 'rd-1', createdAt: '2026-07-28T11:30:00Z', paid: true, rxApproved: true },
{ id: 'ord-5022', customerName: 'Guest · 0917889900', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 280, channel: 'web', createdAt: '2026-07-28T08:15:00Z', paid: true },
{ id: 'ord-5023', customerName: 'Fikru Assefa', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 400, channel: 'web', zone: 'CMC', address: 'CMC Michael', assignedRiderId: 'rd-2', createdAt: '2026-07-27T13:55:00Z', paid: true },
{ id: 'ord-5024', customerName: 'Zenebech Haile', branchId: 'br-cmc', fulfillment: 'pickup', status: 'delivered', totalEtb: 370, channel: 'telegram', createdAt: '2026-07-26T16:45:00Z', paid: true },
{ id: 'ord-5025', customerName: 'Dawit Alemayehu', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 840, channel: 'mobile', zone: 'Bole', address: 'Summit Safari', assignedRiderId: 'rd-3', createdAt: '2026-07-26T09:25:00Z', paid: true },
{ id: 'ord-5026', customerName: 'Tesfaye Negash', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 598, channel: 'web', zone: 'Bole', address: 'Bole Medhanialem', assignedRiderId: 'rd-1', createdAt: '2026-07-25T14:10:00Z', paid: true },
{ id: 'ord-5027', customerName: 'Selam Getahun', branchId: 'br-piassa', fulfillment: 'pickup', status: 'delivered', totalEtb: 400, channel: 'telegram', createdAt: '2026-07-24T10:50:00Z', paid: true },
{ id: 'ord-5028', customerName: 'Mulugeta Fikadu', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 730, channel: 'mobile', zone: 'Bole', address: 'Gerji Mebrat Hail', assignedRiderId: 'rd-3', createdAt: '2026-07-24T18:30:00Z', paid: true },
{ id: 'ord-5029', customerName: 'Horizon Bank · Sara', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 2600, channel: 'b2b', zone: 'CMC', address: 'Horizon Bank HQ', assignedRiderId: 'rd-2', createdAt: '2026-07-23T11:15:00Z', paid: true },
{ id: 'ord-5030', customerName: 'Eyob Mekonnen', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 600, channel: 'web', createdAt: '2026-07-22T15:35:00Z', paid: true },
{ id: 'ord-5031', customerName: 'Yohannes Desta', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 1740, channel: 'mobile', zone: 'CMC', address: 'CMC Summit', assignedRiderId: 'rd-2', createdAt: '2026-07-22T09:05:00Z', paid: true },
{ id: 'ord-5032', customerName: 'Getachew Mola', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 255, channel: 'telegram', zone: 'Bole', address: 'Bole Atlas', assignedRiderId: 'rd-1', createdAt: '2026-07-21T12:20:00Z', paid: true },
{ id: 'ord-5033', customerName: 'Rahel Tadesse', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 290, channel: 'web', createdAt: '2026-07-20T16:50:00Z', paid: true },
{ id: 'ord-5034', customerName: 'Guest · 0919001122', branchId: 'br-piassa', fulfillment: 'delivery', status: 'delivered', totalEtb: 230, channel: 'mobile', zone: 'Piassa', address: 'Piassa Arada', assignedRiderId: 'rd-3', createdAt: '2026-07-20T10:35:00Z', paid: true },
{ id: 'ord-5035', customerName: 'Almaz Girma', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 654, channel: 'web', zone: 'Bole', address: 'Bole Medhanialem', assignedRiderId: 'rd-1', createdAt: '2026-07-19T14:45:00Z', paid: true, rxApproved: true },
{ id: 'ord-5036', customerName: 'Hiwot Bekele', branchId: 'br-cmc', fulfillment: 'pickup', status: 'delivered', totalEtb: 330, channel: 'telegram', createdAt: '2026-07-18T11:10:00Z', paid: true },
{ id: 'ord-5037', customerName: 'Sofia Ahmed', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 370, channel: 'mobile', zone: 'Bole', address: 'Gerji Roba', assignedRiderId: 'rd-3', createdAt: '2026-07-17T17:40:00Z', paid: true },
{ id: 'ord-5038', customerName: 'Horizon Bank · Hiwot', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 4560, channel: 'b2b', zone: 'CMC', address: 'Horizon Bank HQ', assignedRiderId: 'rd-2', createdAt: '2026-07-17T09:50:00Z', paid: true },
{ id: 'ord-5039', customerName: 'Meseret Kassa', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 490, channel: 'web', zone: 'CMC', address: 'CMC Road', assignedRiderId: 'rd-2', createdAt: '2026-07-16T13:25:00Z', paid: true },
{ id: 'ord-5040', customerName: 'Bekele Tesfaye', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 220, channel: 'web', createdAt: '2026-07-15T10:05:00Z', paid: true },
{ id: 'ord-5041', customerName: 'Genet Abera', branchId: 'br-piassa', fulfillment: 'delivery', status: 'delivered', totalEtb: 500, channel: 'mobile', zone: 'Piassa', address: 'Piassa Adwa Ave', assignedRiderId: 'rd-3', createdAt: '2026-07-14T15:15:00Z', paid: true },
{ id: 'ord-5042', customerName: 'Marta Hailu', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 1440, channel: 'telegram', zone: 'Bole', address: 'Bole Rwanda', assignedRiderId: 'rd-1', createdAt: '2026-07-14T08:55:00Z', paid: true, rxApproved: true },
{ id: 'ord-5043', customerName: 'Solomon Wolde', branchId: 'br-cmc', fulfillment: 'pickup', status: 'delivered', totalEtb: 190, channel: 'web', createdAt: '2026-07-13T12:35:00Z', paid: true },
{ id: 'ord-5044', customerName: 'Kalkidan Berhe', branchId: 'br-piassa', fulfillment: 'delivery', status: 'delivered', totalEtb: 410, channel: 'mobile', zone: 'Piassa', address: 'Piassa Arada', assignedRiderId: 'rd-3', createdAt: '2026-07-12T16:00:00Z', paid: true },
{ id: 'ord-5045', customerName: 'Dawit Lemma', branchId: 'br-piassa', fulfillment: 'pickup', status: 'delivered', totalEtb: 250, channel: 'telegram', createdAt: '2026-07-11T09:40:00Z', paid: true },
{ id: 'ord-5046', customerName: 'Abebe Kebede', branchId: 'br-bole', fulfillment: 'delivery', status: 'delivered', totalEtb: 612, channel: 'web', zone: 'Bole', address: 'Bole Atlas', assignedRiderId: 'rd-1', createdAt: '2026-07-10T14:20:00Z', paid: true, rxApproved: true },
{ id: 'ord-5047', customerName: 'Fikru Assefa', branchId: 'br-cmc', fulfillment: 'delivery', status: 'delivered', totalEtb: 371, channel: 'mobile', zone: 'CMC', address: 'CMC Michael', assignedRiderId: 'rd-2', createdAt: '2026-07-09T11:45:00Z', paid: true },
{ id: 'ord-5048', customerName: 'Guest · 0921223344', branchId: 'br-bole', fulfillment: 'pickup', status: 'delivered', totalEtb: 300, channel: 'web', createdAt: '2026-07-08T10:10:00Z', paid: true },
{
id: 'ord-5049',
customerName: 'Walk-in patient · Aster',
customerType: 'guest',
customerPhone: '+251911880011',
notes: 'Pediatric syrup — caregiver collecting; no app account',
branchId: 'br-piassa',
fulfillment: 'pickup',
status: 'confirmed',
totalEtb: 185,
channel: 'doctor',
createdAt: '2026-08-06T11:05:00Z',
paid: false,
rxApproved: true,
doctorId: 'doc-4',
orgId: 'org-4',
},
{
id: 'ord-5050',
customerName: 'Guest · 0917556677',
customerType: 'guest',
customerPhone: '+251917556677',
notes: 'Phone order — call back if stock substitute needed',
branchId: 'br-bole',
fulfillment: 'delivery',
status: 'pending',
totalEtb: 640,
channel: 'web',
zone: 'Bole',
address: 'Bole Atlas gate 3',
createdAt: '2026-08-06T11:40:00Z',
paid: false,
},
{
id: 'ord-5051',
customerName: 'Clinic referral · Melaku',
customerType: 'guest',
customerPhone: '+251911770033',
notes: 'Doctor-initiated for outpatient without Gishen account',
branchId: 'br-bole',
fulfillment: 'delivery',
status: 'confirmed',
totalEtb: 920,
channel: 'doctor',
zone: 'Bole',
address: 'Kazanchis UN ECA road',
createdAt: '2026-08-06T12:10:00Z',
paid: true,
rxApproved: true,
doctorId: 'doc-1',
orgId: 'org-3',
},
]
export let organisations: Organisation[] = [
{
id: 'org-1',
name: 'Horizon Bank',
orgType: 'corporate',
tin: '0001234567',
vatNumber: 'VAT-ET-0001234567',
businessLicense: 'BL-AA/48291/2014',
commercialRegistration: 'CR/015842/2014',
status: 'active',
commercial: {
credit_limit: { amount: '500000.00', currency: 'ETB' },
credit_used: { amount: '124000.00', currency: 'ETB' },
payment_terms_days: 30,
price_list_id: 'pl_corporate_2026',
contract_start: '2026-01-01',
contract_end: '2026-12-31',
activated_at: '2026-05-02T09:40:00Z',
activated_by: 'adm_finance',
},
creditLimitEtb: 500000,
usedEtb: 124000,
billingContact: 'finance@horizon.et',
source: 'admin',
createdAt: '2026-05-01T00:00:00Z',
reviewedBy: 'Yonas Finance',
reviewedAt: '2026-05-02T09:40:00Z',
},
{
id: 'org-2',
name: 'Green NGO Ethiopia',
orgType: 'ngo',
tin: '0009876543',
vatNumber: '',
businessLicense: 'BL-AA/99102/2025',
commercialRegistration: '',
status: 'pending_activation',
commercial: null,
creditLimitEtb: 100000,
usedEtb: 0,
billingContact: 'hr@greenngo.et',
source: 'self_register',
createdAt: '2026-08-05T14:00:00Z',
},
{
id: 'org-3',
name: "St. Paul's Hospital MMC",
orgType: 'hospital',
tin: '0005544332',
vatNumber: 'VAT-ET-0005544332',
businessLicense: 'BL-AA/12044/2007',
commercialRegistration: 'CR/008812/2007',
status: 'active',
commercial: {
credit_limit: { amount: '750000.00', currency: 'ETB' },
credit_used: { amount: '186000.00', currency: 'ETB' },
payment_terms_days: 30,
price_list_id: 'pl_corporate_2026',
contract_start: '2026-01-01',
contract_end: '2026-12-31',
activated_at: '2026-03-14T10:15:00Z',
activated_by: 'adm_finance',
},
creditLimitEtb: 750000,
usedEtb: 186000,
billingContact: 'pharmacy@sphmmc.et',
source: 'admin',
createdAt: '2026-03-12T00:00:00Z',
reviewedBy: 'Yonas Finance',
reviewedAt: '2026-03-14T10:15:00Z',
},
{
id: 'org-4',
name: 'Tikur Anbessa Specialized Hospital',
orgType: 'hospital',
tin: '0007788990',
vatNumber: 'VAT-ET-0007788990',
businessLicense: 'BL-AA/00418/1999',
commercialRegistration: 'CR/001102/1999',
status: 'active',
commercial: {
credit_limit: { amount: '900000.00', currency: 'ETB' },
credit_used: { amount: '412000.00', currency: 'ETB' },
payment_terms_days: 30,
price_list_id: 'pl_corporate_2026',
contract_start: '2026-01-01',
contract_end: '2026-12-31',
activated_at: '2026-02-22T08:50:00Z',
activated_by: 'adm_finance',
},
creditLimitEtb: 900000,
usedEtb: 412000,
billingContact: 'procurement@tash.et',
source: 'admin',
createdAt: '2026-02-20T00:00:00Z',
reviewedBy: 'Yonas Finance',
reviewedAt: '2026-02-22T08:50:00Z',
},
]
/** Clinical actors invited from hospital / clinic orgs — not pharmacy staff. */
export let doctors: Doctor[] = [
{
id: 'doc-1',
name: 'Dr. Selam',
specialty: 'Internal medicine',
licenseNumber: 'MD-ET-10482',
phone: '+251911400101',
email: 'selam.bekele@sphmmc.et',
status: 'active',
orgId: 'org-3',
orgName: "St. Paul's Hospital MMC",
branchId: 'br-bole',
branchName: 'Bole Branch',
hasLinkedAccount: true,
createdAt: '2026-04-01T09:00:00Z',
updatedAt: '2026-08-01T12:00:00Z',
avatarUrl: portraitFor('Dr. Selam'),
},
{
id: 'doc-2',
name: 'Dr. Yohannes',
specialty: 'Endocrinology',
licenseNumber: 'MD-ET-11820',
phone: '+251911400202',
email: 'yohannes.tesfaye@sphmmc.et',
status: 'active',
orgId: 'org-3',
orgName: "St. Paul's Hospital MMC",
branchId: 'br-cmc',
branchName: 'CMC Branch',
hasLinkedAccount: true,
createdAt: '2026-04-08T10:00:00Z',
updatedAt: '2026-07-22T09:30:00Z',
avatarUrl: portraitFor('Dr. Yohannes'),
},
{
id: 'doc-3',
name: 'Dr. Helen',
specialty: 'Orthopedics',
licenseNumber: 'MD-ET-09751',
phone: '+251911400303',
email: 'helen.mengistu@tash.et',
status: 'active',
orgId: 'org-4',
orgName: 'Tikur Anbessa Specialized Hospital',
branchId: 'br-cmc',
branchName: 'CMC Branch',
hasLinkedAccount: true,
createdAt: '2026-03-15T11:00:00Z',
updatedAt: '2026-08-05T16:00:00Z',
avatarUrl: portraitFor('Dr. Helen'),
},
{
id: 'doc-4',
name: 'Dr. Kalkidan',
specialty: 'Pediatrics',
licenseNumber: 'MD-ET-13208',
phone: '+251911400404',
email: 'kalkidan.asefa@tash.et',
status: 'active',
orgId: 'org-4',
orgName: 'Tikur Anbessa Specialized Hospital',
branchId: 'br-piassa',
branchName: 'Piassa Branch',
hasLinkedAccount: false,
createdAt: '2026-05-20T08:00:00Z',
updatedAt: '2026-06-10T14:00:00Z',
avatarUrl: portraitFor('Dr. Kalkidan'),
},
{
id: 'doc-5',
name: 'Dr. Bemnet',
specialty: 'General practice',
licenseNumber: 'MD-ET-14055',
phone: '+251911400505',
email: 'bemnet.hailu@sphmmc.et',
status: 'inactive',
orgId: 'org-3',
orgName: "St. Paul's Hospital MMC",
hasLinkedAccount: false,
createdAt: '2026-06-01T09:00:00Z',
updatedAt: '2026-07-01T09:00:00Z',
avatarUrl: portraitFor('Dr. Bemnet'),
},
]
/** Wire Rx rows to Doctor actors by denormalised prescriber name (mock convenience). */
const doctorByPrescriber = Object.fromEntries(doctors.map((d) => [d.name, d]))
for (const rx of prescriptions) {
const doc = rx.prescriber ? doctorByPrescriber[rx.prescriber] : undefined
if (doc) {
rx.doctorId = doc.id
rx.orgId = doc.orgId
}
}
/** Canonical product categories — keep `category` on every stock row in sync with this list. */
export const productCategories = [
'Antibiotics',
'Analgesics',
'Chronic care',
'Vitamins & supplements',
'Respiratory',
'Maternal & child',
'Dermatology',
'First aid',
] as const
export type ProductCategory = (typeof productCategories)[number]
export type StockRow = {
sku: string
name: string
category: ProductCategory
branchId: string
qty: number
batch: string
/** "YYYY-MM" */
expiry: string
erpQty: number
unitEtb: number
/** Master catalog Item this batch belongs to (optional for legacy lines). */
itemId?: string
/** Units reserved for open orders — ERP-synced. */
reserved?: number
}
export const stockRows: StockRow[] = [
{ sku: 'AMX-500', name: 'Amoxicillin 500mg', category: 'Antibiotics', branchId: 'br-bole', qty: 120, batch: 'B2411', expiry: '2027-01', erpQty: 118, unitEtb: 18 },
{ sku: 'PCM-500', name: 'Paracetamol 500mg', category: 'Analgesics', branchId: 'br-bole', qty: 40, batch: 'B2409', expiry: '2026-11', erpQty: 40, unitEtb: 8 },
{ sku: 'MTF-850', name: 'Metformin 850mg', category: 'Chronic care', branchId: 'br-cmc', qty: 8, batch: 'B2410', expiry: '2026-09', erpQty: 8, unitEtb: 22 },
{ sku: 'VIT-C', name: 'Vitamin C 1000mg', category: 'Vitamins & supplements', branchId: 'br-bole', qty: 200, batch: 'B2501', expiry: '2028-03', erpQty: 200, unitEtb: 120 },
{ sku: 'AMD-5', name: 'Amlodipine 5mg', category: 'Chronic care', branchId: 'br-bole', qty: 96, batch: 'B2503', expiry: '2027-06', erpQty: 96, unitEtb: 14 },
{ sku: 'CTZ-10', name: 'Cetirizine 10mg', category: 'Respiratory', branchId: 'br-bole', qty: 180, batch: 'B2502', expiry: '2027-09', erpQty: 178, unitEtb: 9 },
{ sku: 'IBU-400', name: 'Ibuprofen 400mg', category: 'Analgesics', branchId: 'br-bole', qty: 210, batch: 'B2412', expiry: '2027-04', erpQty: 210, unitEtb: 11 },
{ sku: 'ORS-20', name: 'Oral rehydration salts', category: 'First aid', branchId: 'br-cmc', qty: 18, batch: 'B2504', expiry: '2026-10', erpQty: 18, unitEtb: 25 },
{ sku: 'HYC-1', name: 'Hydrocortisone cream 1%', category: 'Dermatology', branchId: 'br-cmc', qty: 64, batch: 'B2505', expiry: '2027-02', erpQty: 64, unitEtb: 95 },
{ sku: 'AZI-250', name: 'Azithromycin 250mg', category: 'Antibiotics', branchId: 'br-cmc', qty: 16, batch: 'B2506', expiry: '2026-12', erpQty: 16, unitEtb: 46 },
{ sku: 'SLB-INH', name: 'Salbutamol inhaler', category: 'Respiratory', branchId: 'br-piassa', qty: 12, batch: 'B2507', expiry: '2027-03', erpQty: 15, unitEtb: 320 },
{ sku: 'FEF-200', name: 'Ferrous sulphate 200mg', category: 'Maternal & child', branchId: 'br-piassa', qty: 150, batch: 'B2508', expiry: '2028-01', erpQty: 150, unitEtb: 12 },
{ sku: 'MTF-850', name: 'Metformin 850mg', category: 'Chronic care', branchId: 'br-piassa', qty: 0, batch: 'B2509', expiry: '2026-11', erpQty: 0, unitEtb: 22 },
{ sku: 'AZI-250', name: 'Azithromycin 250mg', category: 'Antibiotics', branchId: 'br-piassa', qty: 0, batch: 'B2510', expiry: '2027-01', erpQty: 6, unitEtb: 46 },
{ sku: 'TRM-50', name: 'Tramadol 50mg', category: 'Analgesics', branchId: 'br-bole', qty: 24, batch: 'B2511', expiry: '2027-05', erpQty: 24, unitEtb: 34 },
{ sku: 'DZP-5', name: 'Diazepam 5mg', category: 'Chronic care', branchId: 'br-bole', qty: 26, batch: 'B2512', expiry: '2027-08', erpQty: 26, unitEtb: 28 },
]
// Linked after catalog import at file end — see `linkStockRowsToCatalog`.
export type Customer = {
id: string
name: string
phone: string
tier: string
points: number
/** Branch name, matching `branches[].name` minus the " Branch" suffix. */
branch: string
/** When the customer record was first created — drives the "new this month" KPI. */
joinedAt: string
orders: number
/** Lifetime paid spend, used for the average-lifetime-value KPI. */
spendEtb: number
lastOrderAt: string | null
/** Profile photo. A real uploaded URL drops in here unchanged. */
avatarUrl?: string
}
export const customers: Customer[] = [
{ id: 'c1', name: 'Abebe Kebede', phone: '+251911223344', tier: 'Gold', points: 2400, branch: 'Bole', joinedAt: '2025-11-14T00:00:00Z', orders: 14, spendEtb: 28400, lastOrderAt: '2026-08-06T08:30:00Z', avatarUrl: portraitFor('Abebe Kebede') },
{ id: 'c2', name: 'Marta Hailu', phone: '+251922334455', tier: 'Silver', points: 800, branch: 'Bole', joinedAt: '2026-02-02T00:00:00Z', orders: 9, spendEtb: 11200, lastOrderAt: '2026-08-06T09:00:00Z', avatarUrl: portraitFor('Marta Hailu') },
{ id: 'c3', name: 'Tigist Alemu', phone: '+251933445566', tier: 'Bronze', points: 120, branch: 'CMC', joinedAt: '2026-08-01T00:00:00Z', orders: 4, spendEtb: 3600, lastOrderAt: '2026-07-29T15:40:00Z', avatarUrl: portraitFor('Tigist Alemu') },
{ id: 'c4', name: 'Netsanet Desta', phone: '+251944556677', tier: 'Gold', points: 1650, branch: 'Bole', joinedAt: '2025-09-21T00:00:00Z', orders: 11, spendEtb: 19850, lastOrderAt: '2026-08-06T07:15:00Z', avatarUrl: portraitFor('Netsanet Desta') },
{ id: 'c5', name: 'Hiwot Bekele', phone: '+251955667788', tier: 'Silver', points: 540, branch: 'CMC', joinedAt: '2026-05-08T00:00:00Z', orders: 7, spendEtb: 9600, lastOrderAt: '2026-08-06T10:00:00Z', avatarUrl: portraitFor('Hiwot Bekele') },
// Signed up two days ago and has not added a photo yet — initials carry the row.
{ id: 'c6', name: 'Dawit Lemma', phone: '+251966778899', tier: 'Bronze', points: 260, branch: 'Piassa', joinedAt: '2026-08-04T00:00:00Z', orders: 2, spendEtb: 670, lastOrderAt: '2026-08-06T10:40:00Z' },
]
/** Backfill guest vs registered on seed orders (append-only enrichment). */
const customerByName = Object.fromEntries(customers.map((c) => [c.name, c]))
for (const order of orders) {
if (order.customerType) continue
const guestMatch = /^Guest\s*[·•-]\s*(.+)$/i.exec(order.customerName)
if (guestMatch) {
order.customerType = 'guest'
const digits = guestMatch[1].replace(/\D/g, '')
order.customerPhone = order.customerPhone ?? (digits ? `+251${digits.replace(/^0/, '')}` : undefined)
continue
}
const registered = customerByName[order.customerName]
if (registered) {
order.customerType = 'registered'
order.customerId = registered.id
order.customerPhone = order.customerPhone ?? registered.phone
} else {
order.customerType = 'guest'
}
}
export let faqs: FaqArticle[] = [
{
id: 'faq-1',
category: 'Orders',
title_en: 'How do I confirm a branch order?',
title_am: 'የቅርንጫፍ ትዕዛዝን እንዴት አረጋግጣለሁ?',
body_en: 'Open Orders, select the order for your branch, verify stock, then Confirm. For delivery, assign a rider from the order detail.',
body_am: 'ትዕዛዞችን ይክፈቱ፣ የቅርንጫፍዎን ትዕዛዝ ይምረጡ፣ ክምችት ያረጋግጡ፣ ከዚያ አረጋግጥ ይጫኑ።',
published: true,
updatedAt: '2026-08-01T00:00:00Z',
views: 1284,
},
{
id: 'faq-2',
category: 'Prescriptions',
title_en: 'Controlled substance workflow',
title_am: 'የቁጥጥር ንጥረ ነገር ሂደት',
body_en: 'Flag controlled items during review. Approval creates a dispensing record with batch and pharmacist stamp.',
body_am: 'በግምገማ ጊዜ ቁጥጥር የሚደረግባቸውን ንጥረ ነገሮች ምልክት ያድርጉ።',
published: true,
updatedAt: '2026-08-02T00:00:00Z',
views: 962,
},
{
id: 'faq-3',
category: 'Stock',
title_en: 'Resolving an ERP quantity mismatch',
title_am: 'የERP መጠን አለመጣጣምን መፍታት',
body_en: 'Open Stock, filter by ERP mismatch, then recount the shelf before you resolve to the ERP figure. Every resolution is written to the audit log.',
body_am: 'ክምችትን ይክፈቱ፣ በERP አለመጣጣም ያጣሩ፣ ከዚያ ወደ ERP አኃዝ ከመፍታት በፊት መደርደሪያውን ይቁጠሩ።',
published: true,
updatedAt: '2026-08-03T00:00:00Z',
views: 741,
},
{
id: 'faq-4',
category: 'Payments',
title_en: 'Which payment gateways can customers use?',
title_am: 'ደንበኞች የትኞቹን የክፍያ መንገዶች ሊጠቀሙ ይችላሉ?',
body_en: 'Chapa, Telebirr, M-Pesa and cash on delivery. Finance enables or disables each gateway from Finance → Gateways.',
body_am: 'ቻፓ፣ ቴሌብር፣ ኤም-ፔሳ እና በመላኪያ ጊዜ ክፍያ። ፋይናንስ እያንዳንዱን መንገድ ማንቃት ወይም ማጥፋት ይችላል።',
published: true,
updatedAt: '2026-08-04T00:00:00Z',
views: 508,
},
{
id: 'faq-5',
category: 'B2B',
title_en: 'Onboarding a corporate account',
title_am: 'የድርጅት መዝገብ ማስገባት',
body_en: 'Collect the TIN and billing contact, then raise the organisation for finance approval. Credit limits stay at zero until KYC clears.',
body_am: 'የTIN እና የክፍያ አድራሻ ይሰብስቡ፣ ከዚያ ድርጅቱን ለፋይናንስ ማጽደቅ ያቅርቡ።',
published: false,
updatedAt: '2026-08-05T00:00:00Z',
views: 0,
},
{
id: 'faq-6',
category: 'Orders',
title_en: 'What happens when a delivery fails?',
title_am: 'መላኪያ ሲከሽፍ ምን ይሆናል?',
body_en: 'The rider marks the trip failed from the Telegram bot, the order returns to the dispatch queue, and operations calls the customer before a second attempt.',
body_am: 'መላኪያው ሲከሽፍ ትዕዛዙ ወደ የመላኪያ ወረፋ ይመለሳል፣ ኦፕሬሽንም ደንበኛውን ይደውላል።',
published: false,
updatedAt: '2026-08-06T00:00:00Z',
views: 0,
},
]
export type AuditCategory =
| 'prescriptions'
| 'orders'
| 'stock'
| 'procurement'
| 'organisations'
| 'finance'
| 'marketing'
| 'migrations'
| 'settings'
| 'security'
export type AuditEvent = {
id: string
at: string
actor: string
actorRole: StaffRole
action: string
category: AuditCategory
target: string
targetLabel: string
ip: string
userAgent: string
outcome: 'success' | 'failed'
/** Flagged for the security review queue (permissions, overrides, controlled substances). */
security?: boolean
before?: Record<string, unknown> | null
after?: Record<string, unknown> | null
note?: string
}
const CHROME_MAC =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0 Safari/537.36'
const CHROME_WIN =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36'
const SAFARI_IOS =
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1'
const FIREFOX_LINUX = 'Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0'
export const auditLog: AuditEvent[] = [
{
id: 'a-1032',
at: '2026-08-06T10:12:00Z',
actor: 'Hana Pharmacist',
actorRole: 'pharmacist',
action: 'rx.approve',
category: 'prescriptions',
target: 'rx-1001',
targetLabel: 'Abebe Kebede · Amoxicillin 500mg',
ip: '196.189.44.12',
userAgent: CHROME_MAC,
outcome: 'success',
before: { status: 'under_review', approvedBy: null },
after: { status: 'approved', approvedBy: 'Hana Pharmacist', batch: 'B2411' },
},
{
id: 'a-1031',
at: '2026-08-06T10:04:00Z',
actor: 'Abel Operations',
actorRole: 'operations',
action: 'dispatch.assign_rider',
category: 'orders',
target: 'ord-5004',
targetLabel: 'Netsanet D. · Gerji Mebrat Hail',
ip: '196.189.44.31',
userAgent: CHROME_WIN,
outcome: 'success',
before: { assignedRiderId: null, status: 'confirmed' },
after: { assignedRiderId: 'rd-1', status: 'out_for_delivery' },
},
{
id: 'a-1030',
at: '2026-08-06T09:52:00Z',
actor: 'Lidya Marketing',
actorRole: 'marketing_manager',
action: 'campaign.send',
category: 'marketing',
target: 'cmp-201',
targetLabel: 'Aug refill nudge → Chronic refill',
ip: '196.189.44.58',
userAgent: CHROME_MAC,
outcome: 'success',
before: { status: 'scheduled', recipients: 1280 },
after: { status: 'sending', recipients: 1280, channel: 'SMS' },
},
{
id: 'a-1029',
at: '2026-08-06T09:40:00Z',
actor: 'Yonas Finance',
actorRole: 'finance',
action: 'org.approve',
category: 'organisations',
target: 'org-1',
targetLabel: 'Horizon Bank',
ip: '196.189.44.77',
userAgent: CHROME_WIN,
outcome: 'success',
before: { status: 'pending_activation', creditLimitEtb: 250000 },
after: { status: 'active', creditLimitEtb: 500000 },
},
{
id: 'a-1028',
at: '2026-08-06T09:21:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'auth.role_change',
category: 'security',
target: 'tm-ops',
targetLabel: 'Abel Operations',
ip: '196.189.44.2',
userAgent: CHROME_MAC,
outcome: 'success',
security: true,
before: { role: 'operations', modules: 6 },
after: { role: 'operations', modules: 7, added: ['migrations'] },
note: 'Temporary elevation for the Q3 branch data import.',
},
{
id: 'a-1027',
at: '2026-08-06T09:05:00Z',
actor: 'Dawit Stock',
actorRole: 'stock_manager',
action: 'migration.commit',
category: 'migrations',
target: 'mig-1048',
targetLabel: 'stock-cmc-aug.csv',
ip: '196.189.44.19',
userAgent: FIREFOX_LINUX,
outcome: 'success',
before: { rowsPending: 412 },
after: { rowsImported: 404, rowsFailed: 8 },
},
{
id: 'a-1026',
at: '2026-08-06T08:47:00Z',
actor: 'Tigist Alemu',
actorRole: 'pharmacist',
action: 'auth.login_failed',
category: 'security',
target: 'tm-pharmacist',
targetLabel: 'pharmacist@gishen.et',
ip: '41.86.190.204',
userAgent: SAFARI_IOS,
outcome: 'failed',
security: true,
note: 'Third consecutive failure from an unrecognised network. Account throttled for 15 minutes.',
},
{
id: 'a-1025',
at: '2026-08-06T08:30:00Z',
actor: 'Hana Pharmacist',
actorRole: 'pharmacist',
action: 'order.confirm',
category: 'orders',
target: 'ord-5001',
targetLabel: 'Abebe Kebede · Bole Branch',
ip: '196.189.44.12',
userAgent: CHROME_MAC,
outcome: 'success',
before: { status: 'pending', rxApproved: false },
after: { status: 'confirmed', rxApproved: true },
},
{
id: 'a-1024',
at: '2026-08-06T08:05:00Z',
actor: 'Dawit Stock',
actorRole: 'stock_manager',
action: 'stock.resolve_discrepancy',
category: 'stock',
target: 'AMX-500',
targetLabel: 'Amoxicillin 500mg · Bole Branch',
ip: '196.189.44.19',
userAgent: FIREFOX_LINUX,
outcome: 'success',
before: { platformQty: 120, erpQty: 118 },
after: { platformQty: 118, erpQty: 118, adjustment: -2 },
},
{
id: 'a-1023',
at: '2026-08-06T07:58:00Z',
actor: 'Sara Procurement',
actorRole: 'procurement',
action: 'procurement.raise_request',
category: 'procurement',
target: 'pr-301',
targetLabel: 'Metformin 850mg × 500 · CMC Branch',
ip: '196.189.44.63',
userAgent: CHROME_WIN,
outcome: 'success',
before: null,
after: { status: 'open', qty: 500, priority: 'high' },
},
{
id: 'a-1022',
at: '2026-08-06T07:31:00Z',
actor: 'Abel Operations',
actorRole: 'operations',
action: 'order.cancel',
category: 'orders',
target: 'ord-4996',
targetLabel: 'Guest · 0912004411',
ip: '196.189.44.31',
userAgent: CHROME_WIN,
outcome: 'success',
before: { status: 'picking' },
after: { status: 'cancelled', reason: 'Customer unreachable after three attempts' },
},
{
id: 'a-1021',
at: '2026-08-06T07:02:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'settings.update',
category: 'settings',
target: 'platform.retention',
targetLabel: 'Data retention policy',
ip: '196.189.44.2',
userAgent: CHROME_MAC,
outcome: 'success',
before: { retentionDays: 180 },
after: { retentionDays: 365 },
},
{
id: 'a-1020',
at: '2026-08-05T17:44:00Z',
actor: 'Yonas Finance',
actorRole: 'finance',
action: 'finance.settlement_reconcile',
category: 'finance',
target: 'st-chapa-0805',
targetLabel: 'Chapa · 05 Aug 2026',
ip: '196.189.44.77',
userAgent: CHROME_WIN,
outcome: 'success',
before: { status: 'pending', varianceEtb: 240 },
after: { status: 'reconciled', varianceEtb: 0 },
},
{
id: 'a-1019',
at: '2026-08-05T16:20:00Z',
actor: 'Hana Pharmacist',
actorRole: 'pharmacist',
action: 'rx.dispense_controlled',
category: 'prescriptions',
target: 'rx-1003',
targetLabel: 'Tigist Alemu · Tramadol 50mg',
ip: '196.189.44.12',
userAgent: CHROME_MAC,
outcome: 'success',
security: true,
before: { status: 'approved', dispensed: false },
after: { status: 'dispensed', dispensed: true, witness: 'Super Admin' },
note: 'Controlled substance — dual sign-off recorded.',
},
{
id: 'a-1018',
at: '2026-08-05T15:02:00Z',
actor: 'Lidya Marketing',
actorRole: 'marketing_manager',
action: 'segment.update',
category: 'marketing',
target: 'seg-lapsed',
targetLabel: 'Lapsed buyers',
ip: '196.189.44.58',
userAgent: CHROME_MAC,
outcome: 'success',
before: { window: '90 days', size: 812 },
after: { window: '120 days', size: 640 },
},
{
id: 'a-1017',
at: '2026-08-05T14:11:00Z',
actor: 'Yonas Finance',
actorRole: 'finance',
action: 'org.credit_limit_change',
category: 'organisations',
target: 'org-2',
targetLabel: 'Green NGO Ethiopia',
ip: '196.189.44.77',
userAgent: CHROME_WIN,
outcome: 'failed',
note: 'Rejected — organisation is still pending KYC verification.',
before: { creditLimitEtb: 100000 },
after: null,
},
{
id: 'a-1016',
at: '2026-08-05T13:35:00Z',
actor: 'Dawit Stock',
actorRole: 'stock_manager',
action: 'stock.transfer',
category: 'stock',
target: 'VIT-C',
targetLabel: 'Vitamin C 1000mg · Bole → CMC',
ip: '196.189.44.19',
userAgent: FIREFOX_LINUX,
outcome: 'success',
before: { boleQty: 260, cmcQty: 20 },
after: { boleQty: 200, cmcQty: 80 },
},
{
id: 'a-1015',
at: '2026-08-05T11:48:00Z',
actor: 'Sara Procurement',
actorRole: 'procurement',
action: 'procurement.mark_ordered',
category: 'procurement',
target: 'pr-302',
targetLabel: 'Paracetamol 500mg × 1000',
ip: '196.189.44.63',
userAgent: CHROME_WIN,
outcome: 'success',
before: { status: 'open' },
after: { status: 'ordered', po: 'PO-891', supplier: 'Ethio Pharma Import' },
},
{
id: 'a-1014',
at: '2026-08-05T10:12:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'auth.permission_override',
category: 'security',
target: 'tm-marketing',
targetLabel: 'Lidya Marketing',
ip: '196.189.44.2',
userAgent: CHROME_MAC,
outcome: 'success',
security: true,
before: { modules: ['marketing', 'crm', 'loyalty', 'campaigns'] },
after: { modules: ['marketing', 'crm', 'loyalty', 'campaigns', 'analytics'] },
},
{
id: 'a-1013',
at: '2026-08-05T09:30:00Z',
actor: 'Abel Operations',
actorRole: 'operations',
action: 'migration.commit',
category: 'migrations',
target: 'mig-1046',
targetLabel: 'riders-august.csv',
ip: '196.189.44.31',
userAgent: CHROME_WIN,
outcome: 'success',
before: { rowsPending: 12 },
after: { rowsImported: 12, rowsFailed: 0 },
},
{
id: 'a-1012',
at: '2026-08-04T16:55:00Z',
actor: 'Hana Pharmacist',
actorRole: 'pharmacist',
action: 'rx.query',
category: 'prescriptions',
target: 'rx-1002',
targetLabel: 'Marta Hailu · Metformin 850mg',
ip: '196.189.44.12',
userAgent: SAFARI_IOS,
outcome: 'success',
before: { status: 'submitted' },
after: { status: 'queried', query: 'Prescriber signature is illegible — resubmit a clearer scan.' },
},
{
id: 'a-1011',
at: '2026-08-04T15:22:00Z',
actor: 'Yonas Finance',
actorRole: 'finance',
action: 'finance.gateway_toggle',
category: 'finance',
target: 'gw-mpesa',
targetLabel: 'M-Pesa gateway',
ip: '196.189.44.77',
userAgent: CHROME_WIN,
outcome: 'success',
security: true,
before: { enabled: false },
after: { enabled: true, feePercent: 2 },
},
{
id: 'a-1010',
at: '2026-08-04T14:03:00Z',
actor: 'Dawit Stock',
actorRole: 'stock_manager',
action: 'stock.adjust',
category: 'stock',
target: 'MTF-850',
targetLabel: 'Metformin 850mg · CMC Branch',
ip: '196.189.44.19',
userAgent: FIREFOX_LINUX,
outcome: 'success',
before: { qty: 14 },
after: { qty: 8, reason: 'Damaged in transit — six blisters written off' },
},
{
id: 'a-1009',
at: '2026-08-04T12:40:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'auth.login_failed',
category: 'security',
target: 'tm-admin',
targetLabel: 'admin@gishen.et',
ip: '102.213.9.88',
userAgent: CHROME_WIN,
outcome: 'failed',
security: true,
note: 'Login attempt from an IP outside Ethiopia — blocked by geo policy.',
},
{
id: 'a-1008',
at: '2026-08-04T11:15:00Z',
actor: 'Lidya Marketing',
actorRole: 'marketing_manager',
action: 'campaign.schedule',
category: 'marketing',
target: 'cmp-204',
targetLabel: 'Gold tier perks → High-value households',
ip: '196.189.44.58',
userAgent: CHROME_MAC,
outcome: 'success',
before: { status: 'draft' },
after: { status: 'scheduled', scheduledFor: '2026-08-07T06:00:00Z' },
},
{
id: 'a-1007',
at: '2026-08-04T09:28:00Z',
actor: 'Abel Operations',
actorRole: 'operations',
action: 'order.refund',
category: 'orders',
target: 'ord-4988',
targetLabel: 'Selam G. · Piassa Branch',
ip: '196.189.44.31',
userAgent: CHROME_WIN,
outcome: 'success',
before: { paid: true, refundedEtb: 0 },
after: { paid: true, refundedEtb: 640, reason: 'Item out of stock at pickup' },
},
{
id: 'a-1006',
at: '2026-08-03T16:10:00Z',
actor: 'Sara Procurement',
actorRole: 'procurement',
action: 'procurement.receive',
category: 'procurement',
target: 'pr-303',
targetLabel: 'Amoxicillin 500mg × 300',
ip: '196.189.44.63',
userAgent: CHROME_WIN,
outcome: 'success',
before: { status: 'ordered' },
after: { status: 'received', po: 'PO-884', batch: 'B2411' },
},
{
id: 'a-1005',
at: '2026-08-03T13:47:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'settings.branch_update',
category: 'settings',
target: 'br-piassa',
targetLabel: 'Piassa Branch',
ip: '196.189.44.2',
userAgent: CHROME_MAC,
outcome: 'success',
before: { openingHours: '08:00–20:00' },
after: { openingHours: '07:30–21:00' },
},
{
id: 'a-1004',
at: '2026-08-03T10:05:00Z',
actor: 'Dawit Stock',
actorRole: 'stock_manager',
action: 'migration.validate_failed',
category: 'migrations',
target: 'mig-1044',
targetLabel: 'catalog-q3.csv',
ip: '196.189.44.19',
userAgent: FIREFOX_LINUX,
outcome: 'failed',
before: { rowsTotal: 1840 },
after: { rowsImported: 0, rowsFailed: 1840, reason: 'Required column "sku" was not mapped' },
},
{
id: 'a-1003',
at: '2026-08-02T15:33:00Z',
actor: 'Yonas Finance',
actorRole: 'finance',
action: 'finance.invoice_issue',
category: 'finance',
target: 'org-1',
targetLabel: 'Horizon Bank · inv-881',
ip: '196.189.44.77',
userAgent: CHROME_WIN,
outcome: 'success',
before: null,
after: { invoice: 'inv-881', amountEtb: 32000, period: 'Aug 2026' },
},
{
id: 'a-1002',
at: '2026-08-02T11:12:00Z',
actor: 'Hana Pharmacist',
actorRole: 'pharmacist',
action: 'rx.reject',
category: 'prescriptions',
target: 'rx-0994',
targetLabel: 'Guest · Diazepam 5mg',
ip: '196.189.44.12',
userAgent: CHROME_MAC,
outcome: 'success',
security: true,
before: { status: 'under_review' },
after: { status: 'rejected', reason: 'Prescriber not on the national register' },
},
{
id: 'a-1001',
at: '2026-08-01T14:20:00Z',
actor: 'Super Admin',
actorRole: 'super_admin',
action: 'auth.user_create',
category: 'security',
target: 'tm-marketing',
targetLabel: 'Lidya Marketing',
ip: '196.189.44.2',
userAgent: CHROME_MAC,
outcome: 'success',
security: true,
before: null,
after: { role: 'marketing_manager', email: 'marketing@gishen.et' },
},
{
id: 'a-1000',
at: '2026-08-01T09:02:00Z',
actor: 'Abel Operations',
actorRole: 'operations',
action: 'dispatch.rider_link',
category: 'orders',
target: 'rd-3',
targetLabel: 'Samuel Rider · @sam_gishen',
ip: '196.189.44.31',
userAgent: CHROME_WIN,
outcome: 'success',
before: { telegramLinked: false },
after: { telegramLinked: true, telegramId: '@sam_gishen' },
},
]
export const importTemplates = [
{ id: 'stock', label: 'Stock / inventory', fields: ['sku', 'barcode', 'branchId', 'qty', 'batch', 'expiry', 'cost', 'sell'] },
{ id: 'catalog', label: 'Catalog products', fields: ['sku', 'name', 'strength', 'form', 'category', 'rxFlag', 'manufacturer'] },
{ id: 'hr', label: 'HR / org members', fields: ['orgId', 'phone', 'name', 'department', 'plan', 'memberId'] },
{ id: 'branches', label: 'Branches', fields: ['id', 'name', 'zone', 'phone', 'lat', 'lng'] },
{ id: 'riders', label: 'Delivery people', fields: ['name', 'phone', 'telegramId', 'branchId', 'zone'] },
]
export type OrderLine = { sku: string; name: string; qty: number; unitEtb: number }
export const orderLines: Record<string, OrderLine[]> = {
'ord-5001': [
{ sku: 'AMX-500', name: 'Amoxicillin 500mg', qty: 21, unitEtb: 18 },
{ sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 10, unitEtb: 8 },
{ sku: 'VIT-C', name: 'Vitamin C 1000mg', qty: 2, unitEtb: 120 },
],
'ord-5002': [
{ sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 20, unitEtb: 8 },
{ sku: 'VIT-C', name: 'Vitamin C 1000mg', qty: 1, unitEtb: 120 },
],
'ord-5003': [
{ sku: 'MTF-850', name: 'Metformin 850mg', qty: 120, unitEtb: 22 },
{ sku: 'AMX-500', name: 'Amoxicillin 500mg', qty: 40, unitEtb: 18 },
],
'ord-5004': [
{ sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 30, unitEtb: 8 },
{ sku: 'AMX-500', name: 'Amoxicillin 500mg', qty: 14, unitEtb: 18 },
],
}
export const orderTimeline: Record<string, { id: string; at: string; title: string; detail?: string; tone?: 'default' | 'success' | 'warning' | 'danger' }[]> = {
'ord-5001': [
{ id: '1', at: '2026-08-06T08:30:00Z', title: 'Order placed', detail: 'Web · paid via Chapa', tone: 'success' },
{ id: '2', at: '2026-08-06T08:32:00Z', title: 'Rx linked', detail: 'rx-1001 approved', tone: 'success' },
{ id: '3', at: '2026-08-06T08:40:00Z', title: 'Branch confirmed', detail: 'Bole pharmacist', tone: 'default' },
{ id: '4', at: '2026-08-06T09:05:00Z', title: 'Awaiting rider', detail: 'Delivery zone Bole', tone: 'warning' },
],
'ord-5002': [
{ id: '1', at: '2026-08-06T09:00:00Z', title: 'Order placed', detail: 'Telegram guest', tone: 'success' },
{ id: '2', at: '2026-08-06T09:10:00Z', title: 'Picking started', detail: 'Bole counter', tone: 'default' },
],
'ord-5003': [
{ id: '1', at: '2026-08-06T10:00:00Z', title: 'B2B order placed', detail: 'Horizon Bank credit', tone: 'success' },
{ id: '2', at: '2026-08-06T10:05:00Z', title: 'Finance approved credit draw', tone: 'success' },
{ id: '3', at: '2026-08-06T10:15:00Z', title: 'Confirmed for delivery', tone: 'default' },
],
'ord-5004': [
{ id: '1', at: '2026-08-06T07:15:00Z', title: 'Order placed', detail: 'Mobile app', tone: 'success' },
{ id: '2', at: '2026-08-06T07:40:00Z', title: 'Rider assigned', detail: 'Kidus Rider', tone: 'default' },
{ id: '3', at: '2026-08-06T08:00:00Z', title: 'Out for delivery', detail: 'Telegram bot notified', tone: 'warning' },
],
}
export const stockMovements: Record<string, { id: string; at: string; type: string; qty: number; note: string }[]> = {
'AMX-500': [
{ id: 'm1', at: '2026-08-06T08:00:00Z', type: 'sale', qty: -21, note: 'ord-5001' },
{ id: 'm2', at: '2026-08-05T14:00:00Z', type: 'receive', qty: 100, note: 'PO-884' },
{ id: 'm3', at: '2026-08-04T11:00:00Z', type: 'adjust', qty: -2, note: 'ERP sync discrepancy' },
],
'PCM-500': [
{ id: 'm1', at: '2026-08-06T09:00:00Z', type: 'sale', qty: -20, note: 'ord-5002' },
{ id: 'm2', at: '2026-08-03T10:00:00Z', type: 'receive', qty: 200, note: 'PO-871' },
],
'MTF-850': [
{ id: 'm1', at: '2026-08-06T10:00:00Z', type: 'reserve', qty: -120, note: 'ord-5003 B2B' },
{ id: 'm2', at: '2026-07-28T09:00:00Z', type: 'receive', qty: 80, note: 'PO-840' },
],
'VIT-C': [
{ id: 'm1', at: '2026-08-01T12:00:00Z', type: 'receive', qty: 200, note: 'PO-860' },
{ id: 'm2', at: '2026-08-06T08:30:00Z', type: 'sale', qty: -2, note: 'ord-5001' },
],
'AMD-5': [
{ id: 'm1', at: '2026-08-05T13:10:00Z', type: 'sale', qty: -60, note: 'ord-5009' },
{ id: 'm2', at: '2026-07-30T10:20:00Z', type: 'sale', qty: -120, note: 'ord-5019 B2B' },
{ id: 'm3', at: '2026-07-25T09:00:00Z', type: 'receive', qty: 300, note: 'PO-878' },
],
'CTZ-10': [
{ id: 'm1', at: '2026-08-06T06:40:00Z', type: 'sale', qty: -20, note: 'ord-5005' },
{ id: 'm2', at: '2026-08-02T10:15:00Z', type: 'sale', qty: -20, note: 'ord-5014' },
{ id: 'm3', at: '2026-07-24T08:00:00Z', type: 'receive', qty: 240, note: 'PO-869' },
{ id: 'm4', at: '2026-07-21T11:00:00Z', type: 'adjust', qty: -2, note: 'ERP sync discrepancy' },
],
'IBU-400': [
{ id: 'm1', at: '2026-08-06T07:50:00Z', type: 'sale', qty: -20, note: 'ord-5006' },
{ id: 'm2', at: '2026-08-01T16:20:00Z', type: 'sale', qty: -20, note: 'ord-5015' },
{ id: 'm3', at: '2026-07-19T09:00:00Z', type: 'receive', qty: 300, note: 'PO-862' },
],
'ORS-20': [
{ id: 'm1', at: '2026-08-06T10:40:00Z', type: 'reserve', qty: -4, note: 'ord-5007' },
{ id: 'm2', at: '2026-07-23T11:15:00Z', type: 'sale', qty: -40, note: 'ord-5029 B2B' },
{ id: 'm3', at: '2026-07-15T08:00:00Z', type: 'receive', qty: 120, note: 'PO-856' },
],
'HYC-1': [
{ id: 'm1', at: '2026-08-04T15:25:00Z', type: 'sale', qty: -2, note: 'ord-5010' },
{ id: 'm2', at: '2026-07-17T17:40:00Z', type: 'sale', qty: -2, note: 'ord-5037' },
{ id: 'm3', at: '2026-07-12T10:00:00Z', type: 'receive', qty: 80, note: 'PO-851' },
],
'AZI-250': [
{ id: 'm1', at: '2026-08-04T11:05:00Z', type: 'sale', qty: -6, note: 'ord-5011' },
{ id: 'm2', at: '2026-07-29T15:40:00Z', type: 'sale', qty: -6, note: 'ord-5020' },
{ id: 'm3', at: '2026-07-18T09:00:00Z', type: 'receive', qty: 60, note: 'PO-860' },
],
'SLB-INH': [
{ id: 'm1', at: '2026-08-06T10:40:00Z', type: 'reserve', qty: -1, note: 'ord-5007' },
{ id: 'm2', at: '2026-07-24T18:30:00Z', type: 'sale', qty: -2, note: 'ord-5028' },
{ id: 'm3', at: '2026-07-16T08:00:00Z', type: 'receive', qty: 24, note: 'PO-857' },
],
'FEF-200': [
{ id: 'm1', at: '2026-08-03T09:35:00Z', type: 'sale', qty: -30, note: 'ord-5012' },
{ id: 'm2', at: '2026-07-26T09:25:00Z', type: 'sale', qty: -30, note: 'ord-5025' },
{ id: 'm3', at: '2026-07-13T09:00:00Z', type: 'receive', qty: 300, note: 'PO-853' },
],
'TRM-50': [
{ id: 'm1', at: '2026-08-05T10:20:00Z', type: 'sale', qty: -6, note: 'ord-5013 · rx-1015 stamp' },
{ id: 'm2', at: '2026-07-20T08:00:00Z', type: 'receive', qty: 30, note: 'PO-866 · controlled register signed' },
],
'DZP-5': [
{ id: 'm1', at: '2026-08-02T14:45:00Z', type: 'sale', qty: -4, note: 'ord-5018 · rx-1008 stamp' },
{ id: 'm2', at: '2026-07-20T08:00:00Z', type: 'receive', qty: 30, note: 'PO-866 · controlled register signed' },
],
}
/**
* Stock ledger documents — ERPNext-style receipts, deliveries, transfers and
* adjustments. Inventory qty is still owned by `stockRows`; these rows make the
* Transactions tab feel like a real stock book.
*/
export type StockTxnType = 'receipt' | 'delivery' | 'transfer' | 'adjustment'
export type StockTxnStatus = 'draft' | 'submitted' | 'cancelled'
export type StockTransaction = {
id: string
type: StockTxnType
sku: string
name: string
qty: number
fromBranchId?: string
toBranchId?: string
reference?: string
at: string
status: StockTxnStatus
postedBy: string
}
export let stockTransactions: StockTransaction[] = [
{
id: 'stx-901',
type: 'receipt',
sku: 'AMX-500',
name: 'Amoxicillin 500mg',
qty: 100,
toBranchId: 'br-bole',
reference: 'PO-884',
at: '2026-08-05T14:00:00Z',
status: 'submitted',
postedBy: 'Dawit Stock',
},
{
id: 'stx-902',
type: 'delivery',
sku: 'PCM-500',
name: 'Paracetamol 500mg',
qty: 20,
fromBranchId: 'br-bole',
reference: 'ord-5002',
at: '2026-08-06T09:00:00Z',
status: 'submitted',
postedBy: 'Hana Pharmacist',
},
{
id: 'stx-903',
type: 'transfer',
sku: 'VIT-C',
name: 'Vitamin C 1000mg',
qty: 40,
fromBranchId: 'br-bole',
toBranchId: 'br-cmc',
reference: 'TRF-112',
at: '2026-08-04T16:20:00Z',
status: 'submitted',
postedBy: 'Dawit Stock',
},
{
id: 'stx-904',
type: 'receipt',
sku: 'MTF-850',
name: 'Metformin 850mg',
qty: 80,
toBranchId: 'br-cmc',
reference: 'PO-840',
at: '2026-07-28T09:00:00Z',
status: 'submitted',
postedBy: 'Sara Procurement',
},
{
id: 'stx-905',
type: 'delivery',
sku: 'MTF-850',
name: 'Metformin 850mg',
qty: 120,
fromBranchId: 'br-cmc',
reference: 'ord-5003',
at: '2026-08-06T10:00:00Z',
status: 'submitted',
postedBy: 'System',
},
{
id: 'stx-906',
type: 'adjustment',
sku: 'CTZ-10',
name: 'Cetirizine 10mg',
qty: -2,
fromBranchId: 'br-bole',
reference: 'ERP-SYNC',
at: '2026-07-21T11:00:00Z',
status: 'submitted',
postedBy: 'Dawit Stock',
},
{
id: 'stx-907',
type: 'transfer',
sku: 'IBU-400',
name: 'Ibuprofen 400mg',
qty: 50,
fromBranchId: 'br-cmc',
toBranchId: 'br-piassa',
reference: 'TRF-118',
at: '2026-08-06T11:30:00Z',
status: 'draft',
postedBy: 'Dawit Stock',
},
{
id: 'stx-908',
type: 'receipt',
sku: 'AZI-250',
name: 'Azithromycin 250mg',
qty: 60,
toBranchId: 'br-bole',
reference: 'PO-860',
at: '2026-07-18T09:00:00Z',
status: 'cancelled',
postedBy: 'Sara Procurement',
},
]
/** Physical-count reconciliation lines for the Stock Reconciliation tab. */
export type StockReconciliationLine = {
id: string
sku: string
name: string
branchId: string
bookQty: number
countedQty: number
unitEtb: number
}
export const stockReconciliationLines: StockReconciliationLine[] = [
{ id: 'rcl-1', sku: 'AMX-500', name: 'Amoxicillin 500mg', branchId: 'br-bole', bookQty: 140, countedQty: 138, unitEtb: 45 },
{ id: 'rcl-2', sku: 'PCM-500', name: 'Paracetamol 500mg', branchId: 'br-bole', bookQty: 320, countedQty: 320, unitEtb: 8 },
{ id: 'rcl-3', sku: 'VIT-C', name: 'Vitamin C 1000mg', branchId: 'br-bole', bookQty: 200, countedQty: 195, unitEtb: 120 },
{ id: 'rcl-4', sku: 'MTF-850', name: 'Metformin 850mg', branchId: 'br-cmc', bookQty: 80, countedQty: 76, unitEtb: 18 },
{ id: 'rcl-5', sku: 'CTZ-10', name: 'Cetirizine 10mg', branchId: 'br-bole', bookQty: 88, countedQty: 90, unitEtb: 12 },
]
export let procurementRequests = [
{
id: 'pr-301',
sku: 'MTF-850',
name: 'Metformin 850mg',
qty: 500,
branchId: 'br-cmc',
status: 'open' as const,
priority: 'high' as const,
requestedBy: 'Sara Procurement',
createdAt: '2026-08-06T07:00:00Z',
notes: 'CMC below reorder point',
},
{
id: 'pr-302',
sku: 'PCM-500',
name: 'Paracetamol 500mg',
qty: 1000,
branchId: 'br-bole',
status: 'ordered' as const,
priority: 'medium' as const,
requestedBy: 'Dawit Stock',
createdAt: '2026-08-05T11:00:00Z',
notes: 'Weekend demand buffer',
},
{
id: 'pr-303',
sku: 'AMX-500',
name: 'Amoxicillin 500mg',
qty: 300,
branchId: 'br-bole',
status: 'received' as const,
priority: 'low' as const,
requestedBy: 'Sara Procurement',
createdAt: '2026-08-01T09:00:00Z',
notes: 'PO-884 received',
},
]
export const orgMembers: Record<string, { id: string; name: string; dept: string; plan: string; avatarUrl?: string }[]> = {
'org-1': [
{ id: 'om-1', name: 'Hiwot Bekele', dept: 'Retail banking', plan: 'Staff+', avatarUrl: portraitFor('Hiwot Bekele') },
{ id: 'om-2', name: 'Daniel Tadesse', dept: 'IT', plan: 'Staff', avatarUrl: portraitFor('Daniel Tadesse') },
{ id: 'om-3', name: 'Sara Worku', dept: 'HR', plan: 'Dependent', avatarUrl: portraitFor('Sara Worku') },
],
// Placeholder seat on an org still pending KYC — no real person behind it, so no photo.
'org-2': [{ id: 'om-4', name: 'Pending admin', dept: 'Operations', plan: 'Staff' }],
'org-3': [
{ id: 'om-5', name: 'Dr. Selam', dept: 'Internal medicine', plan: 'Clinician', avatarUrl: portraitFor('Dr. Selam') },
{ id: 'om-6', name: 'Pharmacy liaison', dept: 'Hospital pharmacy', plan: 'Staff', avatarUrl: portraitFor('Pharmacy liaison') },
],
'org-4': [
{ id: 'om-7', name: 'Dr. Helen', dept: 'Orthopedics', plan: 'Clinician', avatarUrl: portraitFor('Dr. Helen') },
{ id: 'om-8', name: 'Dr. Kalkidan', dept: 'Pediatrics', plan: 'Clinician', avatarUrl: portraitFor('Dr. Kalkidan') },
],
}
export const orgInvoices: Record<string, { id: string; period: string; amountEtb: number; status: string }[]> = {
'org-1': [
{ id: 'inv-880', period: 'Jul 2026', amountEtb: 48000, status: 'paid' },
{ id: 'inv-881', period: 'Aug 2026 (MTD)', amountEtb: 32000, status: 'open' },
],
'org-2': [],
'org-3': [
{ id: 'inv-910', period: 'Jul 2026', amountEtb: 62000, status: 'paid' },
{ id: 'inv-911', period: 'Aug 2026 (MTD)', amountEtb: 41000, status: 'open' },
],
'org-4': [
{ id: 'inv-920', period: 'Jul 2026', amountEtb: 98000, status: 'paid' },
{ id: 'inv-921', period: 'Aug 2026 (MTD)', amountEtb: 72000, status: 'open' },
],
}
export const customerOrders: Record<string, string[]> = {
c1: ['ord-5001'],
c2: ['ord-5002'],
c3: [],
c4: ['ord-5004'],
c5: ['ord-5003'],
c6: [],
}
export const settlementsDetailed = [
{ id: 'st-chapa-0805', channel: 'Chapa', date: '2026-08-05', volumeEtb: 84200, status: 'reconciled', txnCount: 186, feesEtb: 1684 },
{ id: 'st-tele-0805', channel: 'Telebirr', date: '2026-08-05', volumeEtb: 52100, status: 'reconciled', txnCount: 142, feesEtb: 1042 },
{ id: 'st-cod-0805', channel: 'COD', date: '2026-08-05', volumeEtb: 18300, status: 'pending', txnCount: 28, feesEtb: 0 },
{ id: 'st-mpesa-0805', channel: 'M-Pesa', date: '2026-08-05', volumeEtb: 9400, status: 'reconciled', txnCount: 19, feesEtb: 188 },
]
export type LoyaltyLedgerEntry = {
id: string
customerId: string
customerName: string
activity: string
points: number
orderId?: string
branch: string
at: string
}
export const loyaltyActivityLabels: Record<string, string> = {
purchase: 'Purchase spend',
refill: 'Prescription refill',
app_order: 'Mobile app order',
referral: 'Friend referral',
review: 'Product review',
birthday: 'Birthday bonus',
health_check: 'Health-check attendance',
org_signup: 'B2B member signup',
redemption: 'Points redemption',
}
export const loyaltyLedger: LoyaltyLedgerEntry[] = [
{ id: 'lp-1001', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'purchase', points: 125, orderId: 'ord-5001', branch: 'Bole', at: '2026-08-06T08:35:00Z' },
{ id: 'lp-1002', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'review', points: 50, branch: 'Bole', at: '2026-08-04T16:20:00Z' },
{ id: 'lp-1003', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'refill', points: 75, branch: 'Bole', at: '2026-07-29T10:05:00Z' },
{ id: 'lp-1004', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'redemption', points: -200, branch: 'Bole', at: '2026-07-22T12:00:00Z' },
{ id: 'lp-1005', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'purchase', points: 180, branch: 'Bole', at: '2026-07-14T09:30:00Z' },
{ id: 'lp-1006', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'referral', points: 150, branch: 'Bole', at: '2026-07-02T11:10:00Z' },
{ id: 'lp-1007', customerId: 'c1', customerName: 'Abebe Kebede', activity: 'birthday', points: 250, branch: 'Bole', at: '2026-06-18T07:00:00Z' },
{ id: 'lp-1008', customerId: 'c2', customerName: 'Marta Hailu', activity: 'purchase', points: 42, orderId: 'ord-5002', branch: 'Bole', at: '2026-08-06T09:05:00Z' },
{ id: 'lp-1009', customerId: 'c2', customerName: 'Marta Hailu', activity: 'refill', points: 75, branch: 'Bole', at: '2026-08-03T14:40:00Z' },
{ id: 'lp-1010', customerId: 'c2', customerName: 'Marta Hailu', activity: 'app_order', points: 100, branch: 'Bole', at: '2026-07-27T18:15:00Z' },
{ id: 'lp-1011', customerId: 'c2', customerName: 'Marta Hailu', activity: 'redemption', points: -150, branch: 'Bole', at: '2026-07-19T13:25:00Z' },
{ id: 'lp-1012', customerId: 'c2', customerName: 'Marta Hailu', activity: 'purchase', points: 96, branch: 'Bole', at: '2026-07-08T10:50:00Z' },
{ id: 'lp-1013', customerId: 'c2', customerName: 'Marta Hailu', activity: 'health_check', points: 120, branch: 'Bole', at: '2026-06-25T09:15:00Z' },
{ id: 'lp-1014', customerId: 'c3', customerName: 'Tigist Alemu', activity: 'purchase', points: 55, branch: 'CMC', at: '2026-08-05T15:30:00Z' },
{ id: 'lp-1015', customerId: 'c3', customerName: 'Tigist Alemu', activity: 'review', points: 50, branch: 'CMC', at: '2026-07-31T17:05:00Z' },
{ id: 'lp-1016', customerId: 'c3', customerName: 'Tigist Alemu', activity: 'refill', points: 75, branch: 'CMC', at: '2026-07-21T11:45:00Z' },
{ id: 'lp-1017', customerId: 'c3', customerName: 'Tigist Alemu', activity: 'redemption', points: -100, branch: 'CMC', at: '2026-07-05T16:00:00Z' },
{ id: 'lp-1018', customerId: 'c3', customerName: 'Tigist Alemu', activity: 'purchase', points: 64, branch: 'CMC', at: '2026-06-28T12:20:00Z' },
{ id: 'lp-1019', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'purchase', points: 89, orderId: 'ord-5004', branch: 'Bole', at: '2026-08-06T07:20:00Z' },
{ id: 'lp-1020', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'app_order', points: 100, branch: 'Bole', at: '2026-08-02T20:10:00Z' },
{ id: 'lp-1021', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'referral', points: 150, branch: 'Bole', at: '2026-07-25T08:35:00Z' },
{ id: 'lp-1022', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'redemption', points: -300, branch: 'Bole', at: '2026-07-11T15:50:00Z' },
{ id: 'lp-1023', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'purchase', points: 210, branch: 'Bole', at: '2026-06-30T13:05:00Z' },
{ id: 'lp-1024', customerId: 'c4', customerName: 'Netsanet Desta', activity: 'refill', points: 75, branch: 'Bole', at: '2026-06-21T10:25:00Z' },
{ id: 'lp-1025', customerId: 'c5', customerName: 'Hiwot Bekele', activity: 'purchase', points: 340, orderId: 'ord-5003', branch: 'CMC', at: '2026-08-06T10:10:00Z' },
{ id: 'lp-1026', customerId: 'c5', customerName: 'Hiwot Bekele', activity: 'org_signup', points: 200, branch: 'CMC', at: '2026-07-30T09:00:00Z' },
{ id: 'lp-1027', customerId: 'c5', customerName: 'Hiwot Bekele', activity: 'redemption', points: -250, branch: 'CMC', at: '2026-07-16T14:15:00Z' },
{ id: 'lp-1028', customerId: 'c5', customerName: 'Hiwot Bekele', activity: 'health_check', points: 120, branch: 'CMC', at: '2026-06-24T08:45:00Z' },
{ id: 'lp-1029', customerId: 'c6', customerName: 'Dawit Lemma', activity: 'purchase', points: 72, branch: 'Piassa', at: '2026-08-01T11:30:00Z' },
{ id: 'lp-1030', customerId: 'c6', customerName: 'Dawit Lemma', activity: 'birthday', points: 250, branch: 'Piassa', at: '2026-07-23T07:00:00Z' },
{ id: 'lp-1031', customerId: 'c6', customerName: 'Dawit Lemma', activity: 'review', points: 50, branch: 'Piassa', at: '2026-07-09T19:40:00Z' },
{ id: 'lp-1032', customerId: 'c6', customerName: 'Dawit Lemma', activity: 'redemption', points: -120, branch: 'Piassa', at: '2026-06-26T16:30:00Z' },
]
export type LoyaltyActivity = {
id: string
name: string
description: string
points: number
unit: string
tierMultiplier?: string
active: boolean
}
export const loyaltyActivities: LoyaltyActivity[] = [
{
id: 'purchase',
name: 'Purchase spend',
description: 'Every paid order accrues points on the net basket value, all channels.',
points: 1,
unit: 'per 100 ETB',
tierMultiplier: 'Gold ×1.5 · Silver ×1.2',
active: true,
},
{
id: 'refill',
name: 'Prescription refill on time',
description: 'Chronic patients who refill inside the reminder window keep their streak.',
points: 75,
unit: 'flat per refill',
active: true,
},
{
id: 'app_order',
name: 'First mobile app order',
description: 'One-off bonus to move Telegram and walk-in customers onto the app.',
points: 100,
unit: 'flat, once per customer',
active: true,
},
{
id: 'referral',
name: 'Friend referral',
description: 'Awarded once the referred customer completes their first paid order.',
points: 150,
unit: 'flat per referral',
tierMultiplier: 'Gold ×2',
active: true,
},
{
id: 'review',
name: 'Product review left',
description: 'Verified purchase reviews only, capped at three rewarded reviews a month.',
points: 50,
unit: 'flat per review',
active: true,
},
{
id: 'birthday',
name: 'Birthday bonus',
description: 'Issued on the customer birthday, expires after 30 days if unused.',
points: 250,
unit: 'flat per year',
tierMultiplier: 'Gold ×1.5',
active: true,
},
{
id: 'health_check',
name: 'Health-check attendance',
description: 'In-branch blood pressure and glucose screening campaigns.',
points: 120,
unit: 'flat per visit',
active: true,
},
{
id: 'org_signup',
name: 'B2B member signup',
description: 'Organisation staff activating their pharmacy benefit for the first time.',
points: 200,
unit: 'flat per member',
tierMultiplier: 'None',
active: false,
},
]
/* ------------------------------------------------------------------ *
* Marketing — segments
* ------------------------------------------------------------------ */
export type MarketingChannel = 'SMS' | 'Telegram' | 'Push' | 'Email'
export type MarketingSegment = {
id: string
name: string
criteria: string
size: number
channel: MarketingChannel
status: 'active' | 'paused' | 'draft'
/** Share of members who opened or clicked the last campaign, as a percentage. */
engagementRate: number
lastUsedAt: string | null
owner: string
createdAt: string
refresh: 'live' | 'daily' | 'manual'
}
export const marketingSegments: MarketingSegment[] = [
{
id: 'seg-chronic',
name: 'Chronic refill',
criteria: 'Has ≥2 repeat prescriptions in 180d AND refill due within 14d',
size: 1280,
channel: 'SMS',
status: 'active',
engagementRate: 58,
lastUsedAt: '2026-08-06T06:00:00Z',
owner: 'Lidya Marketing',
createdAt: '2026-03-02T09:00:00Z',
refresh: 'live',
},
{
id: 'seg-bole',
name: 'Bole catchment',
criteria: 'Delivery zone = Bole OR preferred branch = Bole Branch',
size: 940,
channel: 'Telegram',
status: 'active',
engagementRate: 45,
lastUsedAt: '2026-08-04T07:30:00Z',
owner: 'Lidya Marketing',
createdAt: '2026-03-18T09:00:00Z',
refresh: 'daily',
},
{
id: 'seg-lapsed',
name: 'Lapsed buyers',
criteria: 'No paid order in 120d AND lifetime orders ≥1',
size: 640,
channel: 'SMS',
status: 'active',
engagementRate: 19,
lastUsedAt: '2026-07-29T06:00:00Z',
owner: 'Lidya Marketing',
createdAt: '2026-04-05T09:00:00Z',
refresh: 'daily',
},
{
id: 'seg-new30',
name: 'New customers (30d)',
criteria: 'First paid order within the last 30 days',
size: 428,
channel: 'Push',
status: 'active',
engagementRate: 54,
lastUsedAt: '2026-08-02T10:00:00Z',
owner: 'Lidya Marketing',
createdAt: '2026-05-11T09:00:00Z',
refresh: 'live',
},
{
id: 'seg-rxfollow',
name: 'Rx follow-up',
criteria: 'Prescription approved but not collected within 48h',
size: 315,
channel: 'Telegram',
status: 'active',
engagementRate: 66,
lastUsedAt: '2026-08-05T12:00:00Z',
owner: 'Hana Pharmacist',
createdAt: '2026-06-01T09:00:00Z',
refresh: 'live',
},
{
id: 'seg-highvalue',
name: 'High-value households',
criteria: 'Lifetime spend ≥ 25,000 ETB OR loyalty tier = Gold',
size: 210,
channel: 'Email',
status: 'active',
engagementRate: 70,
lastUsedAt: '2026-07-21T08:00:00Z',
owner: 'Lidya Marketing',
createdAt: '2026-03-02T09:00:00Z',
refresh: 'daily',
},
{
id: 'seg-institutional',
name: 'Institutional accounts',
criteria: 'Linked to an active organisation with credit ≥ 100,000 ETB',
size: 18,
channel: 'Email',
status: 'paused',
engagementRate: 78,
lastUsedAt: null,
owner: 'Yonas Finance',
createdAt: '2026-04-22T09:00:00Z',
refresh: 'manual',
},
]
export type SegmentMember = {
id: string
name: string
phone: string
tier: string
/** Present only for members who exist in the CRM customer table. */
customerId?: string
joinedAt: string
orders: number
spendEtb: number
lastOrderAt: string | null
}
export const segmentMembers: Record<string, SegmentMember[]> = {
'seg-chronic': [
{ id: 'sm-1', name: 'Abebe Kebede', phone: '+251911223344', tier: 'Gold', customerId: 'c1', joinedAt: '2026-03-04T00:00:00Z', orders: 14, spendEtb: 28400, lastOrderAt: '2026-08-06T08:30:00Z' },
{ id: 'sm-2', name: 'Marta Hailu', phone: '+251922334455', tier: 'Silver', customerId: 'c2', joinedAt: '2026-03-12T00:00:00Z', orders: 9, spendEtb: 11200, lastOrderAt: '2026-08-06T09:00:00Z' },
{ id: 'sm-3', name: 'Tigist Alemu', phone: '+251933445566', tier: 'Bronze', customerId: 'c3', joinedAt: '2026-05-02T00:00:00Z', orders: 4, spendEtb: 3600, lastOrderAt: '2026-07-30T00:00:00Z' },
{ id: 'sm-4', name: 'Bekele Tesfaye', phone: '+251944556677', tier: 'Silver', joinedAt: '2026-03-22T00:00:00Z', orders: 11, spendEtb: 15800, lastOrderAt: '2026-08-04T00:00:00Z' },
{ id: 'sm-5', name: 'Almaz Girma', phone: '+251955667788', tier: 'Gold', joinedAt: '2026-04-01T00:00:00Z', orders: 18, spendEtb: 34200, lastOrderAt: '2026-08-05T00:00:00Z' },
{ id: 'sm-6', name: 'Yohannes Desta', phone: '+251966778899', tier: 'Bronze', joinedAt: '2026-05-18T00:00:00Z', orders: 3, spendEtb: 2400, lastOrderAt: '2026-07-19T00:00:00Z' },
{ id: 'sm-7', name: 'Hiwot Bekele', phone: '+251977889900', tier: 'Silver', joinedAt: '2026-04-14T00:00:00Z', orders: 8, spendEtb: 9600, lastOrderAt: '2026-08-01T00:00:00Z' },
{ id: 'sm-8', name: 'Getachew Mola', phone: '+251988990011', tier: 'Bronze', joinedAt: '2026-06-08T00:00:00Z', orders: 2, spendEtb: 1800, lastOrderAt: '2026-07-11T00:00:00Z' },
],
'seg-bole': [
{ id: 'sm-9', name: 'Abebe Kebede', phone: '+251911223344', tier: 'Gold', customerId: 'c1', joinedAt: '2026-03-19T00:00:00Z', orders: 14, spendEtb: 28400, lastOrderAt: '2026-08-06T08:30:00Z' },
{ id: 'sm-10', name: 'Marta Hailu', phone: '+251922334455', tier: 'Silver', customerId: 'c2', joinedAt: '2026-03-19T00:00:00Z', orders: 9, spendEtb: 11200, lastOrderAt: '2026-08-06T09:00:00Z' },
{ id: 'sm-11', name: 'Netsanet Demeke', phone: '+251911445566', tier: 'Silver', joinedAt: '2026-04-02T00:00:00Z', orders: 7, spendEtb: 8900, lastOrderAt: '2026-08-06T07:15:00Z' },
{ id: 'sm-12', name: 'Sofia Ahmed', phone: '+251912334455', tier: 'Bronze', joinedAt: '2026-05-27T00:00:00Z', orders: 3, spendEtb: 2100, lastOrderAt: '2026-07-25T00:00:00Z' },
{ id: 'sm-13', name: 'Dawit Alemayehu', phone: '+251913445566', tier: 'Gold', joinedAt: '2026-03-30T00:00:00Z', orders: 21, spendEtb: 41000, lastOrderAt: '2026-08-05T00:00:00Z' },
{ id: 'sm-14', name: 'Rahel Tadesse', phone: '+251914556677', tier: 'Silver', joinedAt: '2026-06-14T00:00:00Z', orders: 6, spendEtb: 7200, lastOrderAt: '2026-08-03T00:00:00Z' },
],
'seg-lapsed': [
{ id: 'sm-15', name: 'Tigist Alemu', phone: '+251933445566', tier: 'Bronze', customerId: 'c3', joinedAt: '2026-04-08T00:00:00Z', orders: 4, spendEtb: 3600, lastOrderAt: '2026-04-02T00:00:00Z' },
{ id: 'sm-16', name: 'Solomon Wolde', phone: '+251915667788', tier: 'Bronze', joinedAt: '2026-04-08T00:00:00Z', orders: 1, spendEtb: 420, lastOrderAt: '2026-03-28T00:00:00Z' },
{ id: 'sm-17', name: 'Meseret Kassa', phone: '+251916778899', tier: 'Silver', joinedAt: '2026-04-08T00:00:00Z', orders: 5, spendEtb: 6100, lastOrderAt: '2026-04-01T00:00:00Z' },
{ id: 'sm-18', name: 'Fikru Assefa', phone: '+251917889900', tier: 'Bronze', joinedAt: '2026-05-01T00:00:00Z', orders: 2, spendEtb: 980, lastOrderAt: '2026-03-30T00:00:00Z' },
{ id: 'sm-19', name: 'Zenebech Haile', phone: '+251918990011', tier: 'Bronze', joinedAt: '2026-05-01T00:00:00Z', orders: 1, spendEtb: 310, lastOrderAt: '2026-04-04T00:00:00Z' },
],
'seg-new30': [
{ id: 'sm-20', name: 'Kalkidan Berhe', phone: '+251919001122', tier: 'Bronze', joinedAt: '2026-07-18T00:00:00Z', orders: 1, spendEtb: 640, lastOrderAt: '2026-07-18T00:00:00Z' },
{ id: 'sm-21', name: 'Eyob Mekonnen', phone: '+251920112233', tier: 'Bronze', joinedAt: '2026-07-24T00:00:00Z', orders: 2, spendEtb: 1420, lastOrderAt: '2026-08-02T00:00:00Z' },
{ id: 'sm-22', name: 'Selam Getahun', phone: '+251921223344', tier: 'Bronze', joinedAt: '2026-08-01T00:00:00Z', orders: 1, spendEtb: 890, lastOrderAt: '2026-08-01T00:00:00Z' },
{ id: 'sm-23', name: 'Mulugeta Fikadu', phone: '+251923334455', tier: 'Bronze', joinedAt: '2026-08-03T00:00:00Z', orders: 1, spendEtb: 1240, lastOrderAt: '2026-08-03T00:00:00Z' },
],
'seg-rxfollow': [
{ id: 'sm-24', name: 'Marta Hailu', phone: '+251922334455', tier: 'Silver', customerId: 'c2', joinedAt: '2026-08-04T00:00:00Z', orders: 9, spendEtb: 11200, lastOrderAt: '2026-08-06T09:00:00Z' },
{ id: 'sm-25', name: 'Tigist Alemu', phone: '+251933445566', tier: 'Bronze', customerId: 'c3', joinedAt: '2026-08-05T00:00:00Z', orders: 4, spendEtb: 3600, lastOrderAt: '2026-07-30T00:00:00Z' },
{ id: 'sm-26', name: 'Yeshi Lemma', phone: '+251924445566', tier: 'Silver', joinedAt: '2026-08-05T00:00:00Z', orders: 6, spendEtb: 7800, lastOrderAt: '2026-08-04T00:00:00Z' },
{ id: 'sm-27', name: 'Tesfaye Negash', phone: '+251925556677', tier: 'Bronze', joinedAt: '2026-08-06T00:00:00Z', orders: 3, spendEtb: 2900, lastOrderAt: '2026-08-05T00:00:00Z' },
],
'seg-highvalue': [
{ id: 'sm-28', name: 'Abebe Kebede', phone: '+251911223344', tier: 'Gold', customerId: 'c1', joinedAt: '2026-03-02T00:00:00Z', orders: 14, spendEtb: 28400, lastOrderAt: '2026-08-06T08:30:00Z' },
{ id: 'sm-29', name: 'Almaz Girma', phone: '+251955667788', tier: 'Gold', joinedAt: '2026-03-02T00:00:00Z', orders: 18, spendEtb: 34200, lastOrderAt: '2026-08-05T00:00:00Z' },
{ id: 'sm-30', name: 'Dawit Alemayehu', phone: '+251913445566', tier: 'Gold', joinedAt: '2026-03-30T00:00:00Z', orders: 21, spendEtb: 41000, lastOrderAt: '2026-08-05T00:00:00Z' },
{ id: 'sm-31', name: 'Genet Abera', phone: '+251926667788', tier: 'Gold', joinedAt: '2026-04-11T00:00:00Z', orders: 16, spendEtb: 26800, lastOrderAt: '2026-08-02T00:00:00Z' },
],
'seg-institutional': [
{ id: 'sm-32', name: 'Hiwot Bekele (Horizon Bank)', phone: '+251977889900', tier: 'Staff+', joinedAt: '2026-05-01T00:00:00Z', orders: 12, spendEtb: 48000, lastOrderAt: '2026-08-06T10:00:00Z' },
{ id: 'sm-33', name: 'Daniel Tadesse (Horizon Bank)', phone: '+251927778899', tier: 'Staff', joinedAt: '2026-05-01T00:00:00Z', orders: 7, spendEtb: 21400, lastOrderAt: '2026-08-01T00:00:00Z' },
{ id: 'sm-34', name: 'Sara Worku (Horizon Bank)', phone: '+251928889900', tier: 'Dependent', joinedAt: '2026-05-14T00:00:00Z', orders: 4, spendEtb: 9200, lastOrderAt: '2026-07-28T00:00:00Z' },
],
}
/* ------------------------------------------------------------------ *
* Marketing — campaigns
* ------------------------------------------------------------------ */
export type Campaign = {
id: string
name: string
channel: MarketingChannel
segmentId: string
segmentName: string
status: 'draft' | 'scheduled' | 'sending' | 'sent'
recipients: number
delivered: number
opened: number
clicked: number
redeemed: number
bounced: number
message: string
createdBy: string
createdAt: string
sentAt: string | null
scheduledFor: string | null
budgetEtb: number
}
export const campaigns: Campaign[] = [
{
id: 'cmp-201',
name: 'Aug refill nudge',
channel: 'SMS',
segmentId: 'seg-chronic',
segmentName: 'Chronic refill',
status: 'sending',
recipients: 1280,
delivered: 1204,
opened: 698,
clicked: 241,
redeemed: 88,
bounced: 76,
message: 'Your refill is due soon. Tap to reorder from Gishen — free delivery over 500 ETB.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-05T14:00:00Z',
sentAt: '2026-08-06T06:00:00Z',
scheduledFor: null,
budgetEtb: 6400,
},
{
id: 'cmp-202',
name: 'Bole weekend offer',
channel: 'Telegram',
segmentId: 'seg-bole',
segmentName: 'Bole catchment',
status: 'sent',
recipients: 940,
delivered: 902,
opened: 388,
clicked: 132,
redeemed: 51,
bounced: 38,
message: '15% off vitamins and supplements at Bole Branch this weekend only.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-03T10:00:00Z',
sentAt: '2026-08-04T07:30:00Z',
scheduledFor: null,
budgetEtb: 4200,
},
{
id: 'cmp-203',
name: 'Win-back 20%',
channel: 'SMS',
segmentId: 'seg-lapsed',
segmentName: 'Lapsed buyers',
status: 'sent',
recipients: 640,
delivered: 618,
opened: 141,
clicked: 44,
redeemed: 12,
bounced: 22,
message: 'We miss you. Here is 20% off your next Gishen order — valid for 14 days.',
createdBy: 'Lidya Marketing',
createdAt: '2026-07-28T09:00:00Z',
sentAt: '2026-07-29T06:00:00Z',
scheduledFor: null,
budgetEtb: 3200,
},
{
id: 'cmp-204',
name: 'Gold tier perks',
channel: 'Email',
segmentId: 'seg-highvalue',
segmentName: 'High-value households',
status: 'scheduled',
recipients: 210,
delivered: 0,
opened: 0,
clicked: 0,
redeemed: 0,
bounced: 0,
message: 'Your Gold benefits for August: priority dispatch, 2× points, free consultations.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-04T11:15:00Z',
sentAt: null,
scheduledFor: '2026-08-07T06:00:00Z',
budgetEtb: 1800,
},
{
id: 'cmp-205',
name: 'Rx pickup reminder',
channel: 'Telegram',
segmentId: 'seg-rxfollow',
segmentName: 'Rx follow-up',
status: 'sent',
recipients: 315,
delivered: 311,
opened: 214,
clicked: 96,
redeemed: 74,
bounced: 4,
message: 'Your approved prescription is ready for collection. We hold it for 72 hours.',
createdBy: 'Hana Pharmacist',
createdAt: '2026-08-05T09:00:00Z',
sentAt: '2026-08-05T12:00:00Z',
scheduledFor: null,
budgetEtb: 900,
},
{
id: 'cmp-206',
name: 'Ramadan wellness',
channel: 'SMS',
segmentId: 'seg-bole',
segmentName: 'Bole catchment',
status: 'sent',
recipients: 1480,
delivered: 1420,
opened: 604,
clicked: 188,
redeemed: 63,
bounced: 60,
message: 'Ramadan wellness bundle: hydration salts, dates and multivitamins from 380 ETB.',
createdBy: 'Lidya Marketing',
createdAt: '2026-07-10T08:00:00Z',
sentAt: '2026-07-12T05:30:00Z',
scheduledFor: null,
budgetEtb: 7400,
},
{
id: 'cmp-207',
name: 'Corporate renewal',
channel: 'Email',
segmentId: 'seg-institutional',
segmentName: 'Institutional accounts',
status: 'sent',
recipients: 18,
delivered: 18,
opened: 14,
clicked: 9,
redeemed: 6,
bounced: 0,
message: 'Your Gishen corporate agreement renews on 1 September. Review your usage report.',
createdBy: 'Yonas Finance',
createdAt: '2026-07-20T10:00:00Z',
sentAt: '2026-07-21T08:00:00Z',
scheduledFor: null,
budgetEtb: 200,
},
{
id: 'cmp-208',
name: 'Flu season prep',
channel: 'Push',
segmentId: 'seg-new30',
segmentName: 'New customers (30d)',
status: 'sent',
recipients: 902,
delivered: 874,
opened: 402,
clicked: 143,
redeemed: 39,
bounced: 28,
message: 'Flu season is here. Stock up on paracetamol, zinc and vitamin C at Gishen.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-01T09:00:00Z',
sentAt: '2026-08-02T10:00:00Z',
scheduledFor: null,
budgetEtb: 0,
},
{
id: 'cmp-209',
name: 'Diabetes screening drive',
channel: 'Telegram',
segmentId: 'seg-chronic',
segmentName: 'Chronic refill',
status: 'draft',
recipients: 1280,
delivered: 0,
opened: 0,
clicked: 0,
redeemed: 0,
bounced: 0,
message: 'Free glucose screening at CMC Branch every Saturday in August. Walk in any time.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-06T08:20:00Z',
sentAt: null,
scheduledFor: null,
budgetEtb: 2600,
},
{
id: 'cmp-210',
name: 'App download push',
channel: 'Push',
segmentId: 'seg-new30',
segmentName: 'New customers (30d)',
status: 'scheduled',
recipients: 428,
delivered: 0,
opened: 0,
clicked: 0,
redeemed: 0,
bounced: 0,
message: 'Get 100 bonus points on your first order in the Gishen app.',
createdBy: 'Lidya Marketing',
createdAt: '2026-08-06T09:10:00Z',
sentAt: null,
scheduledFor: '2026-08-08T06:00:00Z',
budgetEtb: 0,
},
]
export type CampaignRecipient = {
id: string
name: string
phone: string
customerId?: string
status: 'delivered' | 'opened' | 'clicked' | 'redeemed' | 'bounced'
sentAt: string
}
export const campaignRecipients: Record<string, CampaignRecipient[]> = {
'cmp-201': [
{ id: 'cr-1', name: 'Abebe Kebede', phone: '+251911223344', customerId: 'c1', status: 'redeemed', sentAt: '2026-08-06T06:00:00Z' },
{ id: 'cr-2', name: 'Marta Hailu', phone: '+251922334455', customerId: 'c2', status: 'clicked', sentAt: '2026-08-06T06:00:00Z' },
{ id: 'cr-3', name: 'Tigist Alemu', phone: '+251933445566', customerId: 'c3', status: 'opened', sentAt: '2026-08-06T06:01:00Z' },
{ id: 'cr-4', name: 'Bekele Tesfaye', phone: '+251944556677', status: 'opened', sentAt: '2026-08-06T06:01:00Z' },
{ id: 'cr-5', name: 'Almaz Girma', phone: '+251955667788', status: 'redeemed', sentAt: '2026-08-06T06:02:00Z' },
{ id: 'cr-6', name: 'Yohannes Desta', phone: '+251966778899', status: 'delivered', sentAt: '2026-08-06T06:02:00Z' },
{ id: 'cr-7', name: 'Hiwot Bekele', phone: '+251977889900', status: 'clicked', sentAt: '2026-08-06T06:03:00Z' },
{ id: 'cr-8', name: 'Getachew Mola', phone: '+251988990011', status: 'bounced', sentAt: '2026-08-06T06:03:00Z' },
],
'cmp-202': [
{ id: 'cr-9', name: 'Abebe Kebede', phone: '+251911223344', customerId: 'c1', status: 'clicked', sentAt: '2026-08-04T07:30:00Z' },
{ id: 'cr-10', name: 'Netsanet Demeke', phone: '+251911445566', status: 'redeemed', sentAt: '2026-08-04T07:30:00Z' },
{ id: 'cr-11', name: 'Sofia Ahmed', phone: '+251912334455', status: 'delivered', sentAt: '2026-08-04T07:31:00Z' },
{ id: 'cr-12', name: 'Dawit Alemayehu', phone: '+251913445566', status: 'opened', sentAt: '2026-08-04T07:31:00Z' },
{ id: 'cr-13', name: 'Rahel Tadesse', phone: '+251914556677', status: 'opened', sentAt: '2026-08-04T07:32:00Z' },
{ id: 'cr-14', name: 'Marta Hailu', phone: '+251922334455', customerId: 'c2', status: 'bounced', sentAt: '2026-08-04T07:32:00Z' },
],
'cmp-203': [
{ id: 'cr-15', name: 'Tigist Alemu', phone: '+251933445566', customerId: 'c3', status: 'opened', sentAt: '2026-07-29T06:00:00Z' },
{ id: 'cr-16', name: 'Solomon Wolde', phone: '+251915667788', status: 'delivered', sentAt: '2026-07-29T06:00:00Z' },
{ id: 'cr-17', name: 'Meseret Kassa', phone: '+251916778899', status: 'redeemed', sentAt: '2026-07-29T06:01:00Z' },
{ id: 'cr-18', name: 'Fikru Assefa', phone: '+251917889900', status: 'bounced', sentAt: '2026-07-29T06:01:00Z' },
{ id: 'cr-19', name: 'Zenebech Haile', phone: '+251918990011', status: 'delivered', sentAt: '2026-07-29T06:02:00Z' },
],
'cmp-205': [
{ id: 'cr-20', name: 'Marta Hailu', phone: '+251922334455', customerId: 'c2', status: 'redeemed', sentAt: '2026-08-05T12:00:00Z' },
{ id: 'cr-21', name: 'Tigist Alemu', phone: '+251933445566', customerId: 'c3', status: 'clicked', sentAt: '2026-08-05T12:00:00Z' },
{ id: 'cr-22', name: 'Yeshi Lemma', phone: '+251924445566', status: 'redeemed', sentAt: '2026-08-05T12:01:00Z' },
{ id: 'cr-23', name: 'Tesfaye Negash', phone: '+251925556677', status: 'opened', sentAt: '2026-08-05T12:01:00Z' },
],
'cmp-207': [
{ id: 'cr-24', name: 'Hiwot Bekele (Horizon Bank)', phone: '+251977889900', status: 'redeemed', sentAt: '2026-07-21T08:00:00Z' },
{ id: 'cr-25', name: 'Daniel Tadesse (Horizon Bank)', phone: '+251927778899', status: 'clicked', sentAt: '2026-07-21T08:00:00Z' },
{ id: 'cr-26', name: 'Sara Worku (Horizon Bank)', phone: '+251928889900', status: 'opened', sentAt: '2026-07-21T08:01:00Z' },
],
}
/* ------------------------------------------------------------------ *
* Migrations — job history
* ------------------------------------------------------------------ */
export type MigrationJob = {
id: string
templateId: string
templateLabel: string
filename: string
rowsTotal: number
rowsImported: number
rowsFailed: number
status: 'completed' | 'partial' | 'failed' | 'running'
runBy: string
runAt: string
durationSec: number
mapping: Record<string, string>
}
export const migrationJobs: MigrationJob[] = [
{
id: 'mig-1048',
templateId: 'stock',
templateLabel: 'Stock / inventory',
filename: 'stock-cmc-aug.csv',
rowsTotal: 412,
rowsImported: 404,
rowsFailed: 8,
status: 'partial',
runBy: 'Dawit Stock',
runAt: '2026-08-06T09:05:00Z',
durationSec: 34,
mapping: { sku: 'item_code', branchId: 'branch', qty: 'qty_on_hand', batch: 'batch_no', expiry: 'exp_date', cost: 'unit_cost', sell: 'unit_price' },
},
{
id: 'mig-1047',
templateId: 'catalog',
templateLabel: 'Catalog products',
filename: 'catalog-additions.csv',
rowsTotal: 186,
rowsImported: 186,
rowsFailed: 0,
status: 'completed',
runBy: 'Dawit Stock',
runAt: '2026-08-05T14:22:00Z',
durationSec: 19,
mapping: { sku: 'sku', name: 'product_name', strength: 'strength', form: 'dosage_form', category: 'category', rxFlag: 'is_rx', manufacturer: 'maker' },
},
{
id: 'mig-1046',
templateId: 'riders',
templateLabel: 'Delivery people',
filename: 'riders-august.csv',
rowsTotal: 12,
rowsImported: 12,
rowsFailed: 0,
status: 'completed',
runBy: 'Abel Operations',
runAt: '2026-08-05T09:30:00Z',
durationSec: 3,
mapping: { name: 'full_name', phone: 'msisdn', telegramId: 'telegram', branchId: 'branch', zone: 'zone' },
},
{
id: 'mig-1045',
templateId: 'hr',
templateLabel: 'HR / org members',
filename: 'horizon-bank-staff.xlsx',
rowsTotal: 248,
rowsImported: 231,
rowsFailed: 17,
status: 'partial',
runBy: 'Yonas Finance',
runAt: '2026-08-04T13:10:00Z',
durationSec: 41,
mapping: { orgId: 'org', phone: 'mobile', name: 'employee_name', department: 'dept', plan: 'benefit_plan', memberId: 'staff_no' },
},
{
id: 'mig-1044',
templateId: 'catalog',
templateLabel: 'Catalog products',
filename: 'catalog-q3.csv',
rowsTotal: 1840,
rowsImported: 0,
rowsFailed: 1840,
status: 'failed',
runBy: 'Dawit Stock',
runAt: '2026-08-03T10:05:00Z',
durationSec: 8,
mapping: { name: 'title', strength: 'dose', form: 'form', category: 'group' },
},
{
id: 'mig-1043',
templateId: 'stock',
templateLabel: 'Stock / inventory',
filename: 'stock-bole-opening.csv',
rowsTotal: 604,
rowsImported: 604,
rowsFailed: 0,
status: 'completed',
runBy: 'Dawit Stock',
runAt: '2026-08-02T08:40:00Z',
durationSec: 52,
mapping: { sku: 'sku', branchId: 'branch_id', qty: 'quantity', batch: 'lot', expiry: 'expiry', cost: 'cost', sell: 'price' },
},
{
id: 'mig-1042',
templateId: 'branches',
templateLabel: 'Branches',
filename: 'branches-geo.csv',
rowsTotal: 3,
rowsImported: 3,
rowsFailed: 0,
status: 'completed',
runBy: 'Super Admin',
runAt: '2026-08-01T16:12:00Z',
durationSec: 2,
mapping: { id: 'branch_id', name: 'branch_name', zone: 'zone', phone: 'phone', lat: 'latitude', lng: 'longitude' },
},
{
id: 'mig-1041',
templateId: 'hr',
templateLabel: 'HR / org members',
filename: 'green-ngo-pilot.csv',
rowsTotal: 64,
rowsImported: 58,
rowsFailed: 6,
status: 'partial',
runBy: 'Yonas Finance',
runAt: '2026-07-30T11:25:00Z',
durationSec: 12,
mapping: { orgId: 'organisation', phone: 'phone_number', name: 'name', department: 'unit', plan: 'plan' },
},
]
export type MigrationRowError = {
id: string
row: number
column: string
value: string
reason: string
}
export const migrationJobErrors: Record<string, MigrationRowError[]> = {
'mig-1048': [
{ id: 'er-1', row: 14, column: 'qty_on_hand', value: 'n/a', reason: 'Quantity must be a whole number' },
{ id: 'er-2', row: 37, column: 'exp_date', value: '2025-02', reason: 'Expiry date is already in the past' },
{ id: 'er-3', row: 88, column: 'item_code', value: 'AMX500', reason: 'No catalog product matches this SKU' },
{ id: 'er-4', row: 112, column: 'branch', value: 'br-mexico', reason: 'Unknown branch id' },
{ id: 'er-5', row: 190, column: 'unit_cost', value: '-40', reason: 'Cost cannot be negative' },
{ id: 'er-6', row: 244, column: 'batch_no', value: '', reason: 'Batch number is required for tracked items' },
{ id: 'er-7', row: 301, column: 'qty_on_hand', value: '1.5', reason: 'Quantity must be a whole number' },
{ id: 'er-8', row: 388, column: 'item_code', value: 'PCM-500', reason: 'Duplicate row — SKU already imported in this file' },
],
'mig-1045': [
{ id: 'er-9', row: 8, column: 'mobile', value: '09112233', reason: 'Phone number is too short' },
{ id: 'er-10', row: 22, column: 'benefit_plan', value: 'Platinum', reason: 'Plan is not configured for this organisation' },
{ id: 'er-11', row: 41, column: 'staff_no', value: 'HB-0091', reason: 'Member id already exists' },
{ id: 'er-12', row: 77, column: 'mobile', value: '', reason: 'Phone number is required' },
{ id: 'er-13', row: 103, column: 'dept', value: '', reason: 'Department is required' },
],
'mig-1044': [
{ id: 'er-14', row: 1, column: 'sku', value: '', reason: 'Required column "sku" was not mapped — the whole file was rejected' },
],
'mig-1041': [
{ id: 'er-15', row: 5, column: 'organisation', value: 'org-3', reason: 'Unknown organisation id' },
{ id: 'er-16', row: 19, column: 'phone_number', value: '+251911223344', reason: 'Phone already linked to another member' },
{ id: 'er-17', row: 33, column: 'plan', value: '', reason: 'Plan is required' },
],
}
/* ------------------------------------------------------------------ *
* Dispatch — rider profiles and trip history
* ------------------------------------------------------------------ */
export type RiderTrip = {
id: string
orderId: string
customerName: string
zone: string
assignedAt: string
completedAt: string | null
status: 'delivered' | 'failed' | 'en_route' | 'returned'
minutes: number
onTime: boolean
codEtb: number
}
export const riderStats: Record<
string,
{
status: 'on_shift' | 'off_shift' | 'on_break'
shift: string
vehicle: string
plate: string
joinedAt: string
tripsToday: number
tripsTotal: number
onTimeRate: number
avgMinutes: number
rating: number
codHeldEtb: number
telegramLinked: boolean
lastSeenAt: string
}
> = {
'rd-1': {
status: 'on_shift',
shift: '07:00 – 16:00',
vehicle: 'Motorbike',
plate: 'AA-3-91422',
joinedAt: '2026-02-11T00:00:00Z',
tripsToday: 7,
tripsTotal: 486,
onTimeRate: 94,
avgMinutes: 28,
rating: 4.8,
codHeldEtb: 3240,
telegramLinked: true,
lastSeenAt: '2026-08-06T10:22:00Z',
},
'rd-2': {
status: 'on_break',
shift: '09:00 – 18:00',
vehicle: 'Motorbike',
plate: 'AA-3-88107',
joinedAt: '2026-03-04T00:00:00Z',
tripsToday: 5,
tripsTotal: 372,
onTimeRate: 87,
avgMinutes: 34,
rating: 4.5,
codHeldEtb: 1180,
telegramLinked: true,
lastSeenAt: '2026-08-06T09:58:00Z',
},
'rd-3': {
status: 'off_shift',
shift: '13:00 – 22:00',
vehicle: 'Bicycle',
plate: '—',
joinedAt: '2026-05-20T00:00:00Z',
tripsToday: 0,
tripsTotal: 141,
onTimeRate: 96,
avgMinutes: 24,
rating: 4.9,
codHeldEtb: 0,
telegramLinked: true,
lastSeenAt: '2026-08-05T21:40:00Z',
},
}
export const riderTrips: Record<string, RiderTrip[]> = {
'rd-1': [
{ id: 'tr-101', orderId: 'ord-5004', customerName: 'Netsanet D.', zone: 'Bole', assignedAt: '2026-08-06T07:40:00Z', completedAt: null, status: 'en_route', minutes: 0, onTime: true, codEtb: 0 },
{ id: 'tr-102', orderId: 'ord-5001', customerName: 'Abebe Kebede', zone: 'Bole', assignedAt: '2026-08-06T06:20:00Z', completedAt: '2026-08-06T06:52:00Z', status: 'delivered', minutes: 32, onTime: true, codEtb: 0 },
{ id: 'tr-103', orderId: 'ord-4998', customerName: 'Sofia Ahmed', zone: 'Bole', assignedAt: '2026-08-05T17:10:00Z', completedAt: '2026-08-05T17:38:00Z', status: 'delivered', minutes: 28, onTime: true, codEtb: 1240 },
{ id: 'tr-104', orderId: 'ord-4995', customerName: 'Dawit Alemayehu', zone: 'Bole', assignedAt: '2026-08-05T14:05:00Z', completedAt: '2026-08-05T14:29:00Z', status: 'delivered', minutes: 24, onTime: true, codEtb: 0 },
{ id: 'tr-105', orderId: 'ord-4991', customerName: 'Rahel Tadesse', zone: 'Bole', assignedAt: '2026-08-05T11:20:00Z', completedAt: '2026-08-05T12:14:00Z', status: 'delivered', minutes: 54, onTime: false, codEtb: 890 },
{ id: 'tr-106', orderId: 'ord-4987', customerName: 'Almaz Girma', zone: 'Bole', assignedAt: '2026-08-04T16:40:00Z', completedAt: '2026-08-04T17:05:00Z', status: 'delivered', minutes: 25, onTime: true, codEtb: 0 },
{ id: 'tr-107', orderId: 'ord-4982', customerName: 'Getachew Mola', zone: 'Bole', assignedAt: '2026-08-04T12:00:00Z', completedAt: '2026-08-04T12:31:00Z', status: 'delivered', minutes: 31, onTime: true, codEtb: 420 },
{ id: 'tr-108', orderId: 'ord-4979', customerName: 'Yohannes Desta', zone: 'Bole', assignedAt: '2026-08-03T15:15:00Z', completedAt: '2026-08-03T15:41:00Z', status: 'delivered', minutes: 26, onTime: true, codEtb: 0 },
{ id: 'tr-109', orderId: 'ord-4974', customerName: 'Kalkidan Berhe', zone: 'Bole', assignedAt: '2026-08-03T10:30:00Z', completedAt: null, status: 'failed', minutes: 41, onTime: false, codEtb: 0 },
{ id: 'tr-110', orderId: 'ord-4970', customerName: 'Bekele Tesfaye', zone: 'Bole', assignedAt: '2026-08-02T18:05:00Z', completedAt: '2026-08-02T18:28:00Z', status: 'delivered', minutes: 23, onTime: true, codEtb: 1600 },
{ id: 'tr-111', orderId: 'ord-4966', customerName: 'Hiwot Bekele', zone: 'Bole', assignedAt: '2026-08-02T13:40:00Z', completedAt: '2026-08-02T14:09:00Z', status: 'delivered', minutes: 29, onTime: true, codEtb: 0 },
{ id: 'tr-112', orderId: 'ord-4961', customerName: 'Eyob Mekonnen', zone: 'Bole', assignedAt: '2026-08-01T16:20:00Z', completedAt: '2026-08-01T16:44:00Z', status: 'delivered', minutes: 24, onTime: true, codEtb: 640 },
{ id: 'tr-113', orderId: 'ord-4957', customerName: 'Selam Getahun', zone: 'Bole', assignedAt: '2026-08-01T11:10:00Z', completedAt: '2026-08-01T11:52:00Z', status: 'returned', minutes: 42, onTime: false, codEtb: 0 },
],
'rd-2': [
{ id: 'tr-201', orderId: 'ord-5003', customerName: 'Bank staff · Hiwot', zone: 'CMC', assignedAt: '2026-08-06T10:20:00Z', completedAt: null, status: 'en_route', minutes: 0, onTime: true, codEtb: 0 },
{ id: 'tr-202', orderId: 'ord-4999', customerName: 'Meseret Kassa', zone: 'CMC', assignedAt: '2026-08-06T08:15:00Z', completedAt: '2026-08-06T08:53:00Z', status: 'delivered', minutes: 38, onTime: true, codEtb: 980 },
{ id: 'tr-203', orderId: 'ord-4994', customerName: 'Solomon Wolde', zone: 'CMC', assignedAt: '2026-08-05T15:30:00Z', completedAt: '2026-08-05T16:22:00Z', status: 'delivered', minutes: 52, onTime: false, codEtb: 0 },
{ id: 'tr-204', orderId: 'ord-4990', customerName: 'Genet Abera', zone: 'CMC', assignedAt: '2026-08-05T12:40:00Z', completedAt: '2026-08-05T13:11:00Z', status: 'delivered', minutes: 31, onTime: true, codEtb: 2400 },
{ id: 'tr-205', orderId: 'ord-4986', customerName: 'Fikru Assefa', zone: 'CMC', assignedAt: '2026-08-04T17:05:00Z', completedAt: '2026-08-04T17:39:00Z', status: 'delivered', minutes: 34, onTime: true, codEtb: 0 },
{ id: 'tr-206', orderId: 'ord-4981', customerName: 'Zenebech Haile', zone: 'CMC', assignedAt: '2026-08-04T11:50:00Z', completedAt: null, status: 'failed', minutes: 48, onTime: false, codEtb: 0 },
{ id: 'tr-207', orderId: 'ord-4977', customerName: 'Tesfaye Negash', zone: 'CMC', assignedAt: '2026-08-03T16:20:00Z', completedAt: '2026-08-03T16:55:00Z', status: 'delivered', minutes: 35, onTime: true, codEtb: 1120 },
{ id: 'tr-208', orderId: 'ord-4972', customerName: 'Yeshi Lemma', zone: 'CMC', assignedAt: '2026-08-03T09:40:00Z', completedAt: '2026-08-03T10:18:00Z', status: 'delivered', minutes: 38, onTime: true, codEtb: 0 },
{ id: 'tr-209', orderId: 'ord-4968', customerName: 'Mulugeta Fikadu', zone: 'CMC', assignedAt: '2026-08-02T14:15:00Z', completedAt: '2026-08-02T15:02:00Z', status: 'delivered', minutes: 47, onTime: false, codEtb: 760 },
{ id: 'tr-210', orderId: 'ord-4963', customerName: 'Daniel Tadesse', zone: 'CMC', assignedAt: '2026-08-02T10:00:00Z', completedAt: '2026-08-02T10:33:00Z', status: 'delivered', minutes: 33, onTime: true, codEtb: 0 },
{ id: 'tr-211', orderId: 'ord-4959', customerName: 'Sara Worku', zone: 'CMC', assignedAt: '2026-08-01T15:45:00Z', completedAt: '2026-08-01T16:19:00Z', status: 'delivered', minutes: 34, onTime: true, codEtb: 1450 },
{ id: 'tr-212', orderId: 'ord-4954', customerName: 'Tigist Alemu', zone: 'CMC', assignedAt: '2026-08-01T09:20:00Z', completedAt: '2026-08-01T09:58:00Z', status: 'delivered', minutes: 38, onTime: true, codEtb: 0 },
],
'rd-3': [
{ id: 'tr-301', orderId: 'ord-4997', customerName: 'Netsanet Demeke', zone: 'Bole', assignedAt: '2026-08-05T18:30:00Z', completedAt: '2026-08-05T18:52:00Z', status: 'delivered', minutes: 22, onTime: true, codEtb: 0 },
{ id: 'tr-302', orderId: 'ord-4993', customerName: 'Marta Hailu', zone: 'Bole', assignedAt: '2026-08-05T16:10:00Z', completedAt: '2026-08-05T16:33:00Z', status: 'delivered', minutes: 23, onTime: true, codEtb: 420 },
{ id: 'tr-303', orderId: 'ord-4989', customerName: 'Abebe Kebede', zone: 'Bole', assignedAt: '2026-08-04T19:05:00Z', completedAt: '2026-08-04T19:26:00Z', status: 'delivered', minutes: 21, onTime: true, codEtb: 0 },
{ id: 'tr-304', orderId: 'ord-4984', customerName: 'Sofia Ahmed', zone: 'Bole', assignedAt: '2026-08-04T15:20:00Z', completedAt: '2026-08-04T15:47:00Z', status: 'delivered', minutes: 27, onTime: true, codEtb: 890 },
{ id: 'tr-305', orderId: 'ord-4980', customerName: 'Rahel Tadesse', zone: 'Bole', assignedAt: '2026-08-03T20:00:00Z', completedAt: '2026-08-03T20:24:00Z', status: 'delivered', minutes: 24, onTime: true, codEtb: 0 },
{ id: 'tr-306', orderId: 'ord-4976', customerName: 'Dawit Alemayehu', zone: 'Bole', assignedAt: '2026-08-03T17:15:00Z', completedAt: '2026-08-03T17:41:00Z', status: 'delivered', minutes: 26, onTime: true, codEtb: 1300 },
{ id: 'tr-307', orderId: 'ord-4971', customerName: 'Almaz Girma', zone: 'Bole', assignedAt: '2026-08-02T19:40:00Z', completedAt: '2026-08-02T20:11:00Z', status: 'delivered', minutes: 31, onTime: false, codEtb: 0 },
{ id: 'tr-308', orderId: 'ord-4967', customerName: 'Kalkidan Berhe', zone: 'Bole', assignedAt: '2026-08-02T16:00:00Z', completedAt: '2026-08-02T16:23:00Z', status: 'delivered', minutes: 23, onTime: true, codEtb: 640 },
{ id: 'tr-309', orderId: 'ord-4962', customerName: 'Bekele Tesfaye', zone: 'Bole', assignedAt: '2026-08-01T18:25:00Z', completedAt: '2026-08-01T18:47:00Z', status: 'delivered', minutes: 22, onTime: true, codEtb: 0 },
{ id: 'tr-310', orderId: 'ord-4958', customerName: 'Selam Getahun', zone: 'Bole', assignedAt: '2026-08-01T14:10:00Z', completedAt: '2026-08-01T14:35:00Z', status: 'delivered', minutes: 25, onTime: true, codEtb: 380 },
],
}
/* ------------------------------------------------------------------ *
* Settings — branch profiles, staff and configuration history
* ------------------------------------------------------------------ */
export const branchProfiles: Record<
string,
{
manager: string
address: string
openingHours: string
email: string
staffCount: number
ordersToday: number
revenueTodayEtb: number
skuCount: number
lowStockCount: number
expiringSoonCount: number
openedAt: string
status: 'open' | 'closed' | 'maintenance'
}
> = {
'br-bole': {
manager: 'Hana Pharmacist',
address: 'Bole Atlas, Cameroon St, Addis Ababa',
openingHours: '07:00 – 22:00',
email: 'bole@gishen.et',
staffCount: 9,
ordersToday: 34,
revenueTodayEtb: 41000,
skuCount: 1284,
lowStockCount: 12,
expiringSoonCount: 6,
openedAt: '2024-06-01T00:00:00Z',
status: 'open',
},
'br-cmc': {
manager: 'Dawit Stock',
address: 'CMC Road, near St Michael Church, Addis Ababa',
openingHours: '08:00 – 21:00',
email: 'cmc@gishen.et',
staffCount: 6,
ordersToday: 21,
revenueTodayEtb: 27000,
skuCount: 1041,
lowStockCount: 19,
expiringSoonCount: 4,
openedAt: '2025-02-14T00:00:00Z',
status: 'open',
},
'br-piassa': {
manager: 'Abel Operations',
address: 'Piassa, Adwa Ave, Addis Ababa',
openingHours: '07:30 – 21:00',
email: 'piassa@gishen.et',
staffCount: 4,
ordersToday: 14,
revenueTodayEtb: 17000,
skuCount: 826,
lowStockCount: 8,
expiringSoonCount: 11,
openedAt: '2025-11-03T00:00:00Z',
status: 'maintenance',
},
}
export const branchStaff: Record<string, { id: string; name: string; role: string; shift: string; phone: string; avatarUrl?: string }[]> = {
'br-bole': [
{ id: 'bs-1', name: 'Hana Pharmacist', role: 'Branch manager · Pharmacist', shift: '07:00 – 15:00', phone: '+251930099850', avatarUrl: portraitFor('Hana Pharmacist') },
{ id: 'bs-2', name: 'Selamawit Girma', role: 'Pharmacist', shift: '14:00 – 22:00', phone: '+251930099853', avatarUrl: portraitFor('Selamawit Girma') },
{ id: 'bs-3', name: 'Biruk Haile', role: 'Pharmacy technician', shift: '07:00 – 15:00', phone: '+251930099854', avatarUrl: portraitFor('Biruk Haile') },
{ id: 'bs-4', name: 'Kidus Rider', role: 'Delivery', shift: '07:00 – 16:00', phone: '+251911000111', avatarUrl: portraitFor('Kidus Rider') },
// Matches riders['rd-3'] — Samuel has no photo on file.
{ id: 'bs-5', name: 'Samuel Rider', role: 'Delivery', shift: '13:00 – 22:00', phone: '+251911000333' },
],
'br-cmc': [
{ id: 'bs-6', name: 'Dawit Stock', role: 'Branch manager · Stock', shift: '08:00 – 17:00', phone: '+251930099851', avatarUrl: portraitFor('Dawit Stock') },
{ id: 'bs-7', name: 'Rediet Tesfa', role: 'Pharmacist', shift: '08:00 – 16:00', phone: '+251930099855', avatarUrl: portraitFor('Rediet Tesfa') },
{ id: 'bs-8', name: 'Meron Rider', role: 'Delivery', shift: '09:00 – 18:00', phone: '+251911000222', avatarUrl: portraitFor('Meron Rider') },
],
'br-piassa': [
{ id: 'bs-9', name: 'Abel Operations', role: 'Branch manager', shift: '07:30 – 16:30', phone: '+251930099852', avatarUrl: portraitFor('Abel Operations') },
{ id: 'bs-10', name: 'Nardos Alemu', role: 'Pharmacist', shift: '12:00 – 21:00', phone: '+251930099856', avatarUrl: portraitFor('Nardos Alemu') },
],
}
/** Staff invited to the console who have not accepted yet — drives the Team pending-invite KPI. */
export type StaffInvitation = {
id: string
email: string
name: string
role: StaffRole
branchId?: string
invitedBy: string
invitedAt: string
expiresAt: string
/**
* Deliberately unset on every pending invite — nobody has uploaded a photo
* before accepting, so invite rows carry initials.
*/
avatarUrl?: string
}
export const staffInvitations: StaffInvitation[] = [
{
id: 'inv-1',
email: 'nardos.alemu@gishen.et',
name: 'Nardos Alemu',
role: 'pharmacist',
branchId: 'br-piassa',
invitedBy: 'Super Admin',
invitedAt: '2026-08-06T08:20:00Z',
expiresAt: '2026-08-13T08:20:00Z',
},
{
id: 'inv-2',
email: 'biruk.haile@gishen.et',
name: 'Biruk Haile',
role: 'stock_manager',
branchId: 'br-bole',
invitedBy: 'Dawit Stock',
invitedAt: '2026-08-04T14:05:00Z',
expiresAt: '2026-08-11T14:05:00Z',
},
{
id: 'inv-3',
email: 'rediet.tesfa@gishen.et',
name: 'Rediet Tesfa',
role: 'pharmacist',
branchId: 'br-cmc',
invitedBy: 'Hana Pharmacist',
invitedAt: '2026-08-02T09:30:00Z',
expiresAt: '2026-08-09T09:30:00Z',
},
]
export type SettingsChange = {
id: string
at: string
actor: string
area: 'Platform' | 'Branches' | 'Delivery' | 'Security' | 'Integrations' | 'Notifications'
change: string
detail: string
}
export const settingsActivity: SettingsChange[] = [
{ id: 'sc-1', at: '2026-08-06T07:02:00Z', actor: 'Super Admin', area: 'Platform', change: 'Data retention raised to 365 days', detail: 'Was 180 days · required by the pharmacy regulator' },
{ id: 'sc-2', at: '2026-08-05T18:40:00Z', actor: 'Abel Operations', area: 'Delivery', change: 'Bole delivery radius extended to 9 km', detail: 'Adds Gerji and Summit to the Bole catchment' },
{ id: 'sc-3', at: '2026-08-05T11:05:00Z', actor: 'Super Admin', area: 'Security', change: 'Two-factor authentication enforced for admins', detail: 'Applies to super_admin and finance roles' },
{ id: 'sc-4', at: '2026-08-04T15:22:00Z', actor: 'Yonas Finance', area: 'Integrations', change: 'M-Pesa gateway enabled', detail: 'Fee 2% · settlement T+1' },
{ id: 'sc-5', at: '2026-08-03T13:47:00Z', actor: 'Super Admin', area: 'Branches', change: 'Piassa opening hours changed', detail: '07:30 – 21:00, was 08:00 – 20:00' },
{ id: 'sc-6', at: '2026-08-02T09:30:00Z', actor: 'Lidya Marketing', area: 'Notifications', change: 'SMS sender id updated to GISHEN', detail: 'Approved by the telecom regulator on 1 Aug' },
{ id: 'sc-7', at: '2026-08-01T16:12:00Z', actor: 'Super Admin', area: 'Branches', change: 'Branch geo-coordinates imported', detail: 'Job mig-1042 · 3 rows' },
{ id: 'sc-8', at: '2026-07-31T10:18:00Z', actor: 'Dawit Stock', area: 'Platform', change: 'Low-stock threshold set to 20 units', detail: 'Applies to all branches and drives reorder alerts' },
{ id: 'sc-9', at: '2026-07-30T14:55:00Z', actor: 'Super Admin', area: 'Security', change: 'Session timeout reduced to 30 minutes', detail: 'Idle sessions now expire faster on shared terminals' },
{ id: 'sc-10', at: '2026-07-28T08:20:00Z', actor: 'Abel Operations', area: 'Delivery', change: 'Telegram dispatch bot token rotated', detail: 'Routine 90-day rotation' },
]
export const configIssues = [
{ id: 'ci-1', severity: 'warning' as const, area: 'Branches', message: 'Piassa Branch has no backup pharmacist assigned' },
{ id: 'ci-2', severity: 'warning' as const, area: 'Integrations', message: 'ERP sync credentials expire in 12 days' },
{ id: 'ci-3', severity: 'info' as const, area: 'Delivery', message: 'Samuel Rider has no vehicle plate on file' },
]
/* ------------------------------------------------------------------ *
* Profile — the signed-in admin's own account, sessions and activity
* Keyed by the demoAccounts id (tm-*) so /team/:id and /profile agree.
* ------------------------------------------------------------------ */
export type StaffProfile = {
id: string
jobTitle: string
phone: string
status: 'active' | 'invited' | 'suspended'
joinedAt: string
lastSignInAt: string
lastSignInFrom: string
passwordUpdatedAt: string
twoFactorEnabled: boolean
preferredLocale: 'en' | 'am'
notifications: {
orderAlerts: boolean
lowStockAlerts: boolean
weeklyDigest: boolean
securityAlerts: boolean
}
}
export const staffProfiles: Record<string, StaffProfile> = {
'tm-pharmacist': {
id: 'tm-pharmacist',
jobTitle: 'Branch manager · Pharmacist',
phone: '+251 930 099 850',
status: 'active',
joinedAt: '2024-06-01T00:00:00Z',
lastSignInAt: '2026-08-06T06:40:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.12',
passwordUpdatedAt: '2026-05-18T09:12:00Z',
twoFactorEnabled: true,
preferredLocale: 'am',
notifications: { orderAlerts: true, lowStockAlerts: true, weeklyDigest: false, securityAlerts: true },
},
'tm-stock': {
id: 'tm-stock',
jobTitle: 'Stock manager',
phone: '+251 930 099 851',
status: 'active',
joinedAt: '2024-09-12T00:00:00Z',
lastSignInAt: '2026-08-06T05:55:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.20',
passwordUpdatedAt: '2026-06-02T14:30:00Z',
twoFactorEnabled: false,
preferredLocale: 'en',
notifications: { orderAlerts: false, lowStockAlerts: true, weeklyDigest: true, securityAlerts: true },
},
'tm-procurement': {
id: 'tm-procurement',
jobTitle: 'Procurement lead',
phone: '+251 930 099 857',
status: 'active',
joinedAt: '2025-01-20T00:00:00Z',
lastSignInAt: '2026-08-05T15:10:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.44',
passwordUpdatedAt: '2026-04-11T08:05:00Z',
twoFactorEnabled: false,
preferredLocale: 'en',
notifications: { orderAlerts: false, lowStockAlerts: true, weeklyDigest: true, securityAlerts: true },
},
'tm-finance': {
id: 'tm-finance',
jobTitle: 'Finance manager',
phone: '+251 930 099 858',
status: 'active',
joinedAt: '2024-07-08T00:00:00Z',
lastSignInAt: '2026-08-06T07:25:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.51',
passwordUpdatedAt: '2026-07-01T11:45:00Z',
twoFactorEnabled: true,
preferredLocale: 'en',
notifications: { orderAlerts: false, lowStockAlerts: false, weeklyDigest: true, securityAlerts: true },
},
'tm-marketing': {
id: 'tm-marketing',
jobTitle: 'Marketing manager',
phone: '+251 930 099 859',
status: 'active',
joinedAt: '2025-03-03T00:00:00Z',
lastSignInAt: '2026-08-05T18:02:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.62',
passwordUpdatedAt: '2026-02-19T16:20:00Z',
twoFactorEnabled: false,
preferredLocale: 'am',
notifications: { orderAlerts: false, lowStockAlerts: false, weeklyDigest: true, securityAlerts: false },
},
'tm-ops': {
id: 'tm-ops',
jobTitle: 'Operations manager',
phone: '+251 930 099 852',
status: 'active',
joinedAt: '2024-11-15T00:00:00Z',
lastSignInAt: '2026-08-06T04:30:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.31',
passwordUpdatedAt: '2026-06-27T07:40:00Z',
twoFactorEnabled: true,
preferredLocale: 'en',
notifications: { orderAlerts: true, lowStockAlerts: false, weeklyDigest: false, securityAlerts: true },
},
'tm-admin': {
id: 'tm-admin',
jobTitle: 'Platform super admin',
phone: '+251 930 099 800',
status: 'active',
joinedAt: '2024-05-01T00:00:00Z',
lastSignInAt: '2026-08-06T09:48:00Z',
lastSignInFrom: 'Addis Ababa, Ethiopia · 196.189.44.10',
passwordUpdatedAt: '2026-07-22T10:00:00Z',
twoFactorEnabled: true,
preferredLocale: 'en',
notifications: { orderAlerts: true, lowStockAlerts: true, weeklyDigest: true, securityAlerts: true },
},
}
export type StaffSession = {
id: string
device: string
browser: string
location: string
ip: string
lastActiveAt: string
current: boolean
}
export const staffSessions: Record<string, StaffSession[]> = {
'tm-pharmacist': [
{ id: 'ss-101', device: 'MacBook Pro', browser: 'Chrome 141', location: 'Bole Branch, Addis Ababa', ip: '196.189.44.12', lastActiveAt: '2026-08-06T10:12:00Z', current: true },
{ id: 'ss-102', device: 'iPhone 15', browser: 'Safari 18', location: 'Addis Ababa', ip: '10.42.7.19', lastActiveAt: '2026-08-05T19:40:00Z', current: false },
],
'tm-stock': [
{ id: 'ss-201', device: 'Dell Latitude', browser: 'Chrome 140', location: 'CMC Branch, Addis Ababa', ip: '196.189.44.20', lastActiveAt: '2026-08-06T09:30:00Z', current: true },
{ id: 'ss-202', device: 'Warehouse terminal', browser: 'Firefox 135', location: 'Central warehouse', ip: '10.42.9.4', lastActiveAt: '2026-08-04T13:05:00Z', current: false },
],
'tm-procurement': [
{ id: 'ss-301', device: 'ThinkPad X1', browser: 'Chrome 141', location: 'Head office, Addis Ababa', ip: '196.189.44.44', lastActiveAt: '2026-08-05T15:55:00Z', current: true },
],
'tm-finance': [
{ id: 'ss-401', device: 'MacBook Air', browser: 'Chrome 141', location: 'Head office, Addis Ababa', ip: '196.189.44.51', lastActiveAt: '2026-08-06T08:15:00Z', current: true },
{ id: 'ss-402', device: 'iPad Pro', browser: 'Safari 18', location: 'Addis Ababa', ip: '10.42.3.61', lastActiveAt: '2026-08-03T11:20:00Z', current: false },
],
'tm-marketing': [
{ id: 'ss-501', device: 'MacBook Pro', browser: 'Chrome 140', location: 'Head office, Addis Ababa', ip: '196.189.44.62', lastActiveAt: '2026-08-05T18:44:00Z', current: true },
{ id: 'ss-502', device: 'Pixel 8', browser: 'Chrome mobile', location: 'Addis Ababa', ip: '10.42.5.22', lastActiveAt: '2026-08-02T20:10:00Z', current: false },
],
'tm-ops': [
{ id: 'ss-601', device: 'HP EliteBook', browser: 'Chrome 141', location: 'Dispatch hub, Addis Ababa', ip: '196.189.44.31', lastActiveAt: '2026-08-06T07:05:00Z', current: true },
{ id: 'ss-602', device: 'Samsung Galaxy A54', browser: 'Chrome mobile', location: 'Piassa, Addis Ababa', ip: '10.42.8.77', lastActiveAt: '2026-08-05T22:35:00Z', current: false },
],
'tm-admin': [
{ id: 'ss-701', device: 'MacBook Pro', browser: 'Chrome 141', location: 'Head office, Addis Ababa', ip: '196.189.44.10', lastActiveAt: '2026-08-06T10:20:00Z', current: true },
{ id: 'ss-702', device: 'Windows workstation', browser: 'Edge 140', location: 'Head office, Addis Ababa', ip: '196.189.44.11', lastActiveAt: '2026-08-05T17:12:00Z', current: false },
{ id: 'ss-703', device: 'iPhone 16 Pro', browser: 'Safari 18', location: 'Addis Ababa', ip: '10.42.1.5', lastActiveAt: '2026-08-04T21:48:00Z', current: false },
],
}
export type StaffActivityEntry = {
id: string
at: string
action: string
summary: string
module: ModuleKey
target?: string
targetPath?: string
outcome: 'success' | 'failed'
}
export const staffActivity: Record<string, StaffActivityEntry[]> = {
'tm-pharmacist': [
{ id: 'pa-101', at: '2026-08-06T10:12:00Z', action: 'rx.approve', summary: 'Approved a prescription for Abebe Kebede', module: 'prescriptions', target: 'rx-1001', targetPath: '/prescriptions/rx-1001', outcome: 'success' },
{ id: 'pa-102', at: '2026-08-06T09:35:00Z', action: 'order.confirm', summary: 'Confirmed order for pickup at Bole', module: 'orders', target: 'ord-5002', targetPath: '/orders/ord-5002', outcome: 'success' },
{ id: 'pa-103', at: '2026-08-06T08:50:00Z', action: 'stock.adjust', summary: 'Adjusted Amoxicillin 500mg after a dispensing count', module: 'stock', outcome: 'success' },
{ id: 'pa-104', at: '2026-08-05T17:20:00Z', action: 'rx.query', summary: 'Queried a prescription with the prescriber', module: 'prescriptions', target: 'rx-1003', targetPath: '/prescriptions/rx-1003', outcome: 'success' },
{ id: 'pa-105', at: '2026-08-05T07:02:00Z', action: 'auth.login', summary: 'Signed in from Bole Branch', module: 'dashboard', outcome: 'success' },
{ id: 'pa-106', at: '2026-08-04T16:44:00Z', action: 'faq.view', summary: 'Read the controlled substances guidance article', module: 'faq', outcome: 'success' },
],
'tm-stock': [
{ id: 'pa-201', at: '2026-08-06T09:28:00Z', action: 'stock.receive', summary: 'Received a delivery into the CMC store room', module: 'stock', outcome: 'success' },
{ id: 'pa-202', at: '2026-08-06T08:05:00Z', action: 'migration.run', summary: 'Ran the supplier catalogue import', module: 'migrations', outcome: 'success' },
{ id: 'pa-203', at: '2026-08-05T15:12:00Z', action: 'procurement.request', summary: 'Raised a reorder request for low-stock lines', module: 'procurement', outcome: 'success' },
{ id: 'pa-204', at: '2026-08-05T11:40:00Z', action: 'stock.export', summary: 'Exported the near-expiry report', module: 'stock', outcome: 'success' },
{ id: 'pa-205', at: '2026-08-04T18:30:00Z', action: 'migration.run', summary: 'Retried a failed ERP sync job', module: 'migrations', outcome: 'failed' },
{ id: 'pa-206', at: '2026-08-04T07:15:00Z', action: 'auth.login', summary: 'Signed in from the warehouse terminal', module: 'dashboard', outcome: 'success' },
],
'tm-procurement': [
{ id: 'pa-301', at: '2026-08-05T15:48:00Z', action: 'procurement.order', summary: 'Placed a purchase order with a supplier', module: 'procurement', outcome: 'success' },
{ id: 'pa-302', at: '2026-08-05T13:22:00Z', action: 'procurement.compare', summary: 'Compared supplier quotes for antibiotics', module: 'procurement', outcome: 'success' },
{ id: 'pa-303', at: '2026-08-04T16:10:00Z', action: 'stock.view', summary: 'Reviewed low-stock coverage across branches', module: 'stock', outcome: 'success' },
{ id: 'pa-304', at: '2026-08-04T09:05:00Z', action: 'procurement.receive', summary: 'Marked a request as received', module: 'procurement', outcome: 'success' },
{ id: 'pa-305', at: '2026-08-03T08:30:00Z', action: 'auth.login', summary: 'Signed in from the head office', module: 'dashboard', outcome: 'success' },
],
'tm-finance': [
{ id: 'pa-401', at: '2026-08-06T08:15:00Z', action: 'settlement.reconcile', summary: 'Reconciled the Telebirr settlement batch', module: 'finance', outcome: 'success' },
{ id: 'pa-402', at: '2026-08-05T16:40:00Z', action: 'organisation.approve', summary: 'Approved a B2B credit limit increase', module: 'organisations', outcome: 'success' },
{ id: 'pa-403', at: '2026-08-05T10:05:00Z', action: 'analytics.export', summary: 'Exported the monthly revenue breakdown', module: 'analytics', outcome: 'success' },
{ id: 'pa-404', at: '2026-08-04T14:25:00Z', action: 'invoice.issue', summary: 'Issued invoices for corporate accounts', module: 'finance', outcome: 'success' },
{ id: 'pa-405', at: '2026-08-03T09:50:00Z', action: 'migration.run', summary: 'Imported the bank statement file', module: 'migrations', outcome: 'failed' },
{ id: 'pa-406', at: '2026-08-03T07:30:00Z', action: 'auth.login', summary: 'Signed in from the head office', module: 'dashboard', outcome: 'success' },
],
'tm-marketing': [
{ id: 'pa-501', at: '2026-08-05T18:44:00Z', action: 'campaign.launch', summary: 'Launched the rainy season wellness campaign', module: 'campaigns', outcome: 'success' },
{ id: 'pa-502', at: '2026-08-05T14:10:00Z', action: 'segment.update', summary: 'Refreshed the lapsed customers segment', module: 'marketing', outcome: 'success' },
{ id: 'pa-503', at: '2026-08-04T11:35:00Z', action: 'loyalty.adjust', summary: 'Granted bonus points to gold tier members', module: 'loyalty', outcome: 'success' },
{ id: 'pa-504', at: '2026-08-04T09:20:00Z', action: 'faq.publish', summary: 'Published a new customer help article', module: 'faq_write', outcome: 'success' },
{ id: 'pa-505', at: '2026-08-03T15:05:00Z', action: 'crm.view', summary: 'Reviewed the top spending customers', module: 'crm', outcome: 'success' },
{ id: 'pa-506', at: '2026-08-02T08:40:00Z', action: 'auth.login', summary: 'Signed in from the head office', module: 'dashboard', outcome: 'success' },
],
'tm-ops': [
{ id: 'pa-601', at: '2026-08-06T07:05:00Z', action: 'dispatch.assign_rider', summary: 'Assigned a rider to a Gerji delivery', module: 'dispatch', target: 'ord-5004', targetPath: '/orders/ord-5004', outcome: 'success' },
{ id: 'pa-602', at: '2026-08-05T20:15:00Z', action: 'order.reroute', summary: 'Rerouted a delayed delivery to a second rider', module: 'orders', target: 'ord-5003', targetPath: '/orders/ord-5003', outcome: 'success' },
{ id: 'pa-603', at: '2026-08-05T12:30:00Z', action: 'dispatch.shift', summary: 'Updated the evening rider shift plan', module: 'dispatch', outcome: 'success' },
{ id: 'pa-604', at: '2026-08-04T19:10:00Z', action: 'migration.run', summary: 'Imported delivery zone boundaries', module: 'migrations', outcome: 'success' },
{ id: 'pa-605', at: '2026-08-04T06:50:00Z', action: 'auth.login', summary: 'Signed in from the dispatch hub', module: 'dashboard', outcome: 'success' },
],
'tm-admin': [
{ id: 'pa-701', at: '2026-08-06T09:48:00Z', action: 'settings.update', summary: 'Raised data retention to 365 days', module: 'settings', outcome: 'success' },
{ id: 'pa-702', at: '2026-08-06T08:20:00Z', action: 'team.invite', summary: 'Invited a new pharmacist to the Piassa branch', module: 'team', outcome: 'success' },
{ id: 'pa-703', at: '2026-08-05T17:12:00Z', action: 'audit.export', summary: 'Exported the security review queue', module: 'audit', outcome: 'success' },
{ id: 'pa-704', at: '2026-08-05T11:05:00Z', action: 'settings.security', summary: 'Enforced two-factor authentication for admins', module: 'settings', outcome: 'success' },
{ id: 'pa-705', at: '2026-08-04T15:30:00Z', action: 'organisation.approve', summary: 'Approved a new corporate account', module: 'organisations', outcome: 'success' },
{ id: 'pa-706', at: '2026-08-04T10:02:00Z', action: 'auth.login', summary: 'Signed in from the head office', module: 'dashboard', outcome: 'success' },
{ id: 'pa-707', at: '2026-08-03T14:18:00Z', action: 'migration.run', summary: 'Imported branch geo-coordinates', module: 'migrations', outcome: 'success' },
],
}
/* ── Scan to purchase (counter POS) ─────────────────────────────────────────
* Fixtures for the QR scan-to-purchase tool, the assisted invoice reader and
* the inbound purchase-ticket integration. IDs deliberately reuse the existing
* customer (`c1`–`c6`), branch, SKU and procurement keys so every association
* resolves against real records.
*/
/**
* How a SKU is allowed to leave the counter. `rx` needs an approved
* prescription naming the medicine (a pharmacist may record an emergency
* override); `controlled` can never be overridden.
*/
export type DispenseClass = 'otc' | 'rx' | 'controlled'
export const dispenseClassBySku: Record<string, DispenseClass> = {
'AMX-500': 'rx',
'PCM-500': 'otc',
'MTF-850': 'rx',
'VIT-C': 'otc',
'AMD-5': 'rx',
'CTZ-10': 'otc',
'IBU-400': 'otc',
'ORS-20': 'otc',
'HYC-1': 'otc',
'AZI-250': 'rx',
'SLB-INH': 'rx',
'FEF-200': 'otc',
'TRM-50': 'controlled',
'DZP-5': 'controlled',
}
/** Loyalty earn rule, mirroring the `purchase` row in `loyaltyActivities`. */
export const posEarnRule = { pointsPer: 1, perEtb: 100 }
/** Tier multipliers stacked on the base purchase award. */
export const posTierMultipliers: Record<string, number> = { Gold: 1.5, Silver: 1.2, Bronze: 1 }
/** Namespace stamped into every customer QR code the console prints. */
export const customerQrPrefix = 'GISHEN-CUST'
export type ScanSource = 'camera' | 'manual' | 'simulated' | 'ticket'
export type CounterScan = {
id: string
at: string
/** Raw payload as it arrived from the camera, keypad or ticket feed. */
code: string
customerId?: string
customerName: string
source: ScanSource
outcome: 'resolved' | 'unknown'
branchId: string
staff: string
orderId?: string
totalEtb?: number
pointsAwarded?: number
}
export const counterScanHistory: CounterScan[] = [
{ id: 'scn-4001', at: '2026-08-06T10:42:00Z', code: 'GISHEN-CUST:c1', customerId: 'c1', customerName: 'Abebe Kebede', source: 'camera', outcome: 'resolved', branchId: 'br-bole', staff: 'Hana Pharmacist', orderId: 'ord-5041', totalEtb: 336, pointsAwarded: 5 },
{ id: 'scn-4002', at: '2026-08-06T10:18:00Z', code: 'GISHEN-CUST:c4', customerId: 'c4', customerName: 'Netsanet Desta', source: 'camera', outcome: 'resolved', branchId: 'br-bole', staff: 'Hana Pharmacist', orderId: 'ord-5040', totalEtb: 188, pointsAwarded: 3 },
{ id: 'scn-4003', at: '2026-08-06T09:55:00Z', code: '+251933445566', customerId: 'c3', customerName: 'Tigist Alemu', source: 'manual', outcome: 'resolved', branchId: 'br-cmc', staff: 'Rediet Tesfa', orderId: 'ord-5039', totalEtb: 75, pointsAwarded: 1 },
{ id: 'scn-4004', at: '2026-08-06T09:31:00Z', code: 'GISHEN-CUST:c9x', customerName: 'Not recognised', source: 'camera', outcome: 'unknown', branchId: 'br-bole', staff: 'Hana Pharmacist' },
{ id: 'scn-4005', at: '2026-08-06T09:04:00Z', code: 'GISHEN-CUST:c2', customerId: 'c2', customerName: 'Marta Hailu', source: 'camera', outcome: 'resolved', branchId: 'br-bole', staff: 'Hana Pharmacist', orderId: 'ord-5038', totalEtb: 260, pointsAwarded: 3 },
{ id: 'scn-4006', at: '2026-08-06T08:47:00Z', code: 'TCK-88399', customerId: 'c6', customerName: 'Dawit Lemma', source: 'ticket', outcome: 'resolved', branchId: 'br-piassa', staff: 'Abel Operations', orderId: 'ord-5037', totalEtb: 440, pointsAwarded: 4 },
{ id: 'scn-4007', at: '2026-08-05T17:12:00Z', code: 'GISHEN-CUST:c5', customerId: 'c5', customerName: 'Hiwot Bekele', source: 'simulated', outcome: 'resolved', branchId: 'br-cmc', staff: 'Rediet Tesfa', orderId: 'ord-5036', totalEtb: 190, pointsAwarded: 2 },
{ id: 'scn-4008', at: '2026-08-05T16:35:00Z', code: 'unreadable', customerName: 'Not recognised', source: 'camera', outcome: 'unknown', branchId: 'br-cmc', staff: 'Rediet Tesfa' },
]
export type PurchaseTicketLine = { sku: string; name: string; qty: number; unitEtb: number }
/**
* An inbound purchase raised outside the console — the tickets app, the counter
* POS bridge or the Telegram bot. The operator accepts it into an order.
*/
export type PurchaseTicket = {
id: string
sourceId: string
externalRef: string
customerId?: string
customerName: string
phone?: string
branchId: string
lines: PurchaseTicketLine[]
totalEtb: number
raisedAt: string
status: 'pending' | 'accepted' | 'rejected'
note?: string
acceptedOrderId?: string
}
export const inboundPurchaseTickets: PurchaseTicket[] = [
{
id: 'tkt-7001',
sourceId: 'int-tickets',
externalRef: 'TCK-88421',
customerId: 'c1',
customerName: 'Abebe Kebede',
phone: '+251911223344',
branchId: 'br-bole',
lines: [
{ sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 10, unitEtb: 8 },
{ sku: 'VIT-C', name: 'Vitamin C 1000mg', qty: 1, unitEtb: 120 },
],
totalEtb: 200,
raisedAt: '2026-08-06T10:52:00Z',
status: 'pending',
note: 'Loyalty QR matched at the ticket counter.',
},
{
id: 'tkt-7002',
sourceId: 'int-tickets',
externalRef: 'TCK-88437',
customerId: 'c3',
customerName: 'Tigist Alemu',
phone: '+251933445566',
branchId: 'br-cmc',
lines: [
{ sku: 'MTF-850', name: 'Metformin 850mg', qty: 12, unitEtb: 22 },
{ sku: 'ORS-20', name: 'Oral rehydration salts', qty: 2, unitEtb: 25 },
],
totalEtb: 314,
raisedAt: '2026-08-06T10:35:00Z',
status: 'pending',
note: 'Quantity exceeds CMC shelf count — check before accepting.',
},
{
id: 'tkt-7003',
sourceId: 'int-tickets',
externalRef: 'TCK-88452',
customerName: 'Guest · 0921334455',
phone: '+251921334455',
branchId: 'br-bole',
lines: [
{ sku: 'IBU-400', name: 'Ibuprofen 400mg', qty: 6, unitEtb: 11 },
{ sku: 'CTZ-10', name: 'Cetirizine 10mg', qty: 4, unitEtb: 9 },
],
totalEtb: 102,
raisedAt: '2026-08-06T10:11:00Z',
status: 'pending',
note: 'No loyalty id on the ticket — resolve the customer before confirming.',
},
{
id: 'tkt-7004',
sourceId: 'int-pos-bridge',
externalRef: 'POS-1194',
customerId: 'c2',
customerName: 'Marta Hailu',
phone: '+251922334455',
branchId: 'br-bole',
lines: [
{ sku: 'TRM-50', name: 'Tramadol 50mg', qty: 10, unitEtb: 34 },
{ sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 5, unitEtb: 8 },
],
totalEtb: 380,
raisedAt: '2026-08-06T09:48:00Z',
status: 'pending',
note: 'Contains a controlled line — pharmacist must clear it against rx-1023.',
},
{
id: 'tkt-7005',
sourceId: 'int-tickets',
externalRef: 'TCK-88399',
customerId: 'c6',
customerName: 'Dawit Lemma',
phone: '+251966778899',
branchId: 'br-piassa',
lines: [
{ sku: 'SLB-INH', name: 'Salbutamol inhaler', qty: 1, unitEtb: 320 },
{ sku: 'FEF-200', name: 'Ferrous sulphate 200mg', qty: 10, unitEtb: 12 },
],
totalEtb: 440,
raisedAt: '2026-08-06T08:40:00Z',
status: 'accepted',
acceptedOrderId: 'ord-5037',
},
]
/** A pluggable inbound purchase source. Manual counter entry never depends on one. */
export type PurchaseSource = {
id: string
name: string
vendor: string
status: 'connected' | 'degraded' | 'disconnected'
lastSyncAt?: string
webhookUrl: string
note: string
}
export const purchaseSources: PurchaseSource[] = [
{
id: 'int-tickets',
name: 'Tickets app',
vendor: 'Yaltopia Tickets',
status: 'connected',
lastSyncAt: '2026-08-06T10:52:00Z',
webhookUrl: 'https://api.gishen.et/hooks/tickets',
note: 'Counter tickets stream in as pending purchases for an operator to accept.',
},
{
id: 'int-pos-bridge',
name: 'Counter POS bridge',
vendor: 'Legacy till (CSV bridge)',
status: 'degraded',
lastSyncAt: '2026-08-06T09:48:00Z',
webhookUrl: 'https://api.gishen.et/hooks/pos-bridge',
note: 'Polling every 15 minutes; the till clock drifts, so timestamps need a sanity check.',
},
{
id: 'int-telegram',
name: 'Telegram order bot',
vendor: 'Gishen Bot',
status: 'disconnected',
lastSyncAt: '2026-08-04T18:20:00Z',
webhookUrl: 'https://api.gishen.et/hooks/telegram',
note: 'Token expired. Manual counter entry is unaffected.',
},
]
/** Suppliers that issue the paper invoices the reader ingests. */
export type InvoiceSupplier = { id: string; name: string; tin: string; contact: string }
export const invoiceSuppliers: InvoiceSupplier[] = [
{ id: 'sup-1', name: 'Ethio Pharma Import', tin: '0012345678', contact: 'sales@ethiopharma.et' },
{ id: 'sup-2', name: 'Addis Pharma Dist.', tin: '0087654321', contact: 'orders@addispharma.et' },
{ id: 'sup-3', name: 'Rift Valley Medical', tin: '0055667788', contact: 'accounts@riftvalleymed.et' },
]
/**
* One line the assisted reader pulled off an invoice. `confidence` is the
* extractor's own score — anything under 0.8 is highlighted for the operator,
* and `matchedSku` is false when the printed code matched no catalogue entry.
*/
export type ScannedInvoiceLine = {
id: string
sku: string
name: string
qty: number
unitEtb: number
confidence: number
matchedSku: boolean
}
export type ScannedInvoice = {
id: string
supplierId: string
supplierName: string
invoiceNo: string
invoiceDate: string
branchId: string
subtotalEtb: number
vatEtb: number
totalEtb: number
lines: ScannedInvoiceLine[]
/** What the extractor thinks this paperwork belongs to; the operator confirms. */
suggestedLink?: { kind: PurchaseAssociationKind; id: string; label: string }
}
export const scannedInvoiceFixtures: ScannedInvoice[] = [
{
id: 'sinv-2001',
supplierId: 'sup-1',
supplierName: 'Ethio Pharma Import',
invoiceNo: 'EPI-2026-4471',
invoiceDate: '2026-08-05',
branchId: 'br-cmc',
subtotalEtb: 11800,
vatEtb: 1770,
totalEtb: 13570,
lines: [
{ id: 'sl-1', sku: 'MTF-850', name: 'Metformin 850mg', qty: 500, unitEtb: 20, confidence: 0.97, matchedSku: true },
{ id: 'sl-2', sku: 'ORS-20', name: 'Oral rehydration salts', qty: 40, unitEtb: 22, confidence: 0.91, matchedSku: true },
{ id: 'sl-3', sku: 'AZI-250', name: 'Azithromycin 250mg', qty: 20, unitEtb: 44, confidence: 0.62, matchedSku: true },
],
suggestedLink: { kind: 'procurement', id: 'pr-301', label: 'pr-301 · Metformin 850mg × 500' },
},
{
id: 'sinv-2002',
supplierId: 'sup-2',
supplierName: 'Addis Pharma Dist.',
invoiceNo: 'APD-8890',
invoiceDate: '2026-08-04',
branchId: 'br-bole',
subtotalEtb: 8600,
vatEtb: 1290,
totalEtb: 9890,
lines: [
{ id: 'sl-1', sku: 'PCM-500', name: 'Paracetamol 500mg', qty: 1000, unitEtb: 7, confidence: 0.95, matchedSku: true },
{ id: 'sl-2', sku: 'IBU-400', name: 'Ibuprofen 400mg', qty: 100, unitEtb: 10, confidence: 0.88, matchedSku: true },
{ id: 'sl-3', sku: '', name: 'Zinc sulphate 20mg (not in catalogue)', qty: 60, unitEtb: 10, confidence: 0.54, matchedSku: false },
],
suggestedLink: { kind: 'procurement', id: 'pr-302', label: 'pr-302 · Paracetamol 500mg × 1000' },
},
{
id: 'sinv-2003',
supplierId: 'sup-3',
supplierName: 'Rift Valley Medical',
invoiceNo: 'RVM-0771',
invoiceDate: '2026-08-03',
branchId: 'br-piassa',
subtotalEtb: 6720,
vatEtb: 1008,
totalEtb: 7728,
lines: [
{ id: 'sl-1', sku: 'SLB-INH', name: 'Salbutamol inhaler', qty: 20, unitEtb: 296, confidence: 0.93, matchedSku: true },
{ id: 'sl-2', sku: 'FEF-200', name: 'Ferrous sulphate 200mg', qty: 70, unitEtb: 11, confidence: 0.79, matchedSku: true },
],
suggestedLink: { kind: 'branch', id: 'br-piassa', label: 'Piassa Branch · stock arrival' },
},
]
/** What a scanned invoice can be attached to. */
export type PurchaseAssociationKind = 'procurement' | 'supplier' | 'branch'
export type PurchaseAssociation = {
id: string
invoiceId: string
invoiceNo: string
supplierName: string
linkKind: PurchaseAssociationKind
linkId: string
linkLabel: string
totalEtb: number
lineCount: number
createdAt: string
createdBy: string
/** `review` when the operator confirmed the link but flagged a line for finance. */
status: 'linked' | 'review'
}
export let purchaseAssociations: PurchaseAssociation[] = [
{
id: 'pas-9001',
invoiceId: 'sinv-2004',
invoiceNo: 'EPI-2026-4402',
supplierName: 'Ethio Pharma Import',
linkKind: 'procurement',
linkId: 'pr-303',
linkLabel: 'pr-303 · Amoxicillin 500mg × 300',
totalEtb: 5980,
lineCount: 2,
createdAt: '2026-08-05T12:20:00Z',
createdBy: 'Dawit Stock',
status: 'linked',
},
{
id: 'pas-9002',
invoiceId: 'sinv-2005',
invoiceNo: 'APD-8871',
supplierName: 'Addis Pharma Dist.',
linkKind: 'supplier',
linkId: 'sup-2',
linkLabel: 'Addis Pharma Dist. · August statement',
totalEtb: 3240,
lineCount: 3,
createdAt: '2026-08-04T16:05:00Z',
createdBy: 'Sara Procurement',
status: 'review',
},
]
export {
catalogItems,
itemChangeHistory,
therapeuticClasses,
priceLists,
productTypes,
dosageForms,
adminRoutes,
controlledSchedules,
efdaStatuses,
ageRestrictions,
mergeStatuses,
merchandisingBadges,
normalizeMatchKey,
scoreMatchKey,
nextItemSku,
slugifyItem,
findCatalogItem,
itemVariants,
skuToItemId,
buildDefaultUoms,
resolveItemUoms,
toStockQty,
fromStockQty,
stockUomOf,
storefrontPdpPreview,
commonUoms,
} from './catalog'
export type {
MedicationItem,
ItemChangeEvent,
PriceList,
ProductType,
DosageForm,
AdminRoute,
ControlledSchedule,
EfdaStatus,
AgeRestriction,
MergeStatus,
OwnershipSource,
MerchandisingBadge,
ActiveIngredient,
CatalogBarcode,
DraftPackVariant,
FieldGroupOwnership,
ItemUom,
} from './catalog'
import { skuToItemId as _skuToItemId } from './catalog'
/** Additive link: attach catalog `itemId` + a mock reserved qty without renaming stock fields. */
for (const row of stockRows) {
const linked = _skuToItemId[row.sku]
if (linked) row.itemId = linked
if (row.reserved === undefined) row.reserved = Math.max(0, Math.floor(row.qty * 0.08))
}