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