Compare commits
No commits in common. "12a4aa8a9fca8e465f4405da7811bec724fd23f9" and "065423efbf5a79decd9f4b7f1a09d30f84c8e1fc" have entirely different histories.
12a4aa8a9f
...
065423efbf
|
|
@ -7,7 +7,7 @@
|
||||||
| **Home repo (this file)** | `Gishen-Admin` |
|
| **Home repo (this file)** | `Gishen-Admin` |
|
||||||
| **Sibling root** | `/Users/kirukib/Desktop/Yaltopia Project/` |
|
| **Sibling root** | `/Users/kirukib/Desktop/Yaltopia Project/` |
|
||||||
| **Shared API base (TBD)** | Prefer `https://api.gishenpharmacy.org/v1` (B2B draft / Admin alignment). Legacy sheets still show `api.gishen.example` — unify TBD |
|
| **Shared API base (TBD)** | Prefer `https://api.gishenpharmacy.org/v1` (B2B draft / Admin alignment). Legacy sheets still show `api.gishen.example` — unify TBD |
|
||||||
| **Last reconciled** | 2026-08-10 |
|
| **Last reconciled** | 2026-08-08 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
| Date | Change |
|
| Date | Change |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 2026-08-10 | **Admin Rx flagging:** prescription `flags[]` (code, severity, note, open/resolved) with list badges/filters + detail add/resolve. Mob Rx Care has **no** pharmacist flagging surface yet — customer visibility / sync **TBD**. Specs updated in Admin backend sheet. |
|
|
||||||
| 2026-08-08 | **Entity schemas (platform pack):** Expanded [`docs/schemas.md`](./schemas.md) to cover shared primitives, Admin ops backend DTOs, B2B portal entities, Ecom/retail DTOs, event catalog, and path-prefix ownership. §2.9 index updated. |
|
| 2026-08-08 | **Entity schemas (platform pack):** Expanded [`docs/schemas.md`](./schemas.md) to cover shared primitives, Admin ops backend DTOs, B2B portal entities, Ecom/retail DTOs, event catalog, and path-prefix ownership. §2.9 index updated. |
|
||||||
| 2026-08-08 | **Entity schemas:** Added shared schema summary (§2.9) + link to Admin [`docs/schemas.md`](./schemas.md) for full field tables on Money, Staff, Org/Commercial, Doctor, Rx, Order, Item/UOM, Stock, Customer, Settlement, Dispatch. |
|
| 2026-08-08 | **Entity schemas:** Added shared schema summary (§2.9) + link to Admin [`docs/schemas.md`](./schemas.md) for full field tables on Money, Staff, Org/Commercial, Doctor, Rx, Order, Item/UOM, Stock, Customer, Settlement, Dispatch. |
|
||||||
| 2026-08-08 | **Org/Money alignment:** Canonical org status `pending_activation`; Admin activate path + `CommercialTerms`/`Money`; B2B register `POST /v1/org/register` + `org.activated`/`org.suspended`. Resolved Money default (decimal major ETB). Refreshed divergence table + Sources stamp after sibling B2B/Ecom skim. |
|
| 2026-08-08 | **Org/Money alignment:** Canonical org status `pending_activation`; Admin activate path + `CommercialTerms`/`Money`; B2B register `POST /v1/org/register` + `org.activated`/`org.suspended`. Resolved Money default (decimal major ETB). Refreshed divergence table + Sources stamp after sibling B2B/Ecom skim. |
|
||||||
|
|
@ -43,12 +42,12 @@ Update these dates whenever you reconcile against a sibling.
|
||||||
|
|
||||||
| Source | Path (sibling area) | Found | Spec / README | Last skimmed |
|
| Source | Path (sibling area) | Found | Spec / README | Last skimmed |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| **Admin** | `Gishen-Admin/` | ✅ | [`docs/admin-backend-spec.md`](./admin-backend-spec.md) + [`docs/schemas.md`](./schemas.md) | 2026-08-10 |
|
| **Admin** | `Gishen-Admin/` | ✅ | [`docs/admin-backend-spec.md`](./admin-backend-spec.md) + [`docs/schemas.md`](./schemas.md) | 2026-08-08 |
|
||||||
| **Mob** | `Gishen-Mob/` | ✅ | [`docs/BACKEND_SPEC.md`](../../Gishen-Mob/docs/BACKEND_SPEC.md) (relative from sibling root) | 2026-08-10 |
|
| **Mob** | `Gishen-Mob/` | ✅ | [`docs/BACKEND_SPEC.md`](../../Gishen-Mob/docs/BACKEND_SPEC.md) (relative from sibling root) | 2026-08-07 |
|
||||||
| **Ecom** | `Gishen-Ecom/` | ✅ | [`docs/backend.md`](../../Gishen-Ecom/docs/backend.md) | 2026-08-10 |
|
| **Ecom** | `Gishen-Ecom/` | ✅ | [`docs/backend.md`](../../Gishen-Ecom/docs/backend.md) | 2026-08-08 |
|
||||||
| **B2B** | `Gishen-B2B/` | ✅ | [`docs/backend/OVERVIEW.md`](../../Gishen-B2B/docs/backend/OVERVIEW.md) + coordination | 2026-08-10 |
|
| **B2B** | `Gishen-B2B/` | ✅ | [`docs/backend/OVERVIEW.md`](../../Gishen-B2B/docs/backend/OVERVIEW.md) + coordination | 2026-08-08 |
|
||||||
| **Dispatch Bot** | `Gishen-Dispatch-Bot/` | ✅ (related) | `docs/BACKEND_SPEC.md` | 2026-08-10 |
|
| **Dispatch Bot** | `Gishen-Dispatch-Bot/` | ✅ (related) | `docs/BACKEND_SPEC.md` | 2026-08-07 |
|
||||||
| **Telegram Mini App** | `Gishen-Telegram-MiniApp/` | ⚠️ empty scaffold | — | 2026-08-10 |
|
| **Telegram Mini App** | `Gishen-Telegram-MiniApp/` | ⚠️ empty scaffold | — | 2026-08-07 |
|
||||||
|
|
||||||
### Sources not found
|
### Sources not found
|
||||||
|
|
||||||
|
|
@ -152,13 +151,12 @@ See [§3 Auth matrix](#3-auth-provider-matrix).
|
||||||
| Layer | Contract |
|
| Layer | Contract |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Admin line item | `frequency?`: `QD`\|`BID`\|`TID`\|`QID`\|`QXH`\|`custom`; `intervalHours?` (QXH); `times?`: `HH:mm`[] |
|
| Admin line item | `frequency?`: `QD`\|`BID`\|`TID`\|`QID`\|`QXH`\|`custom`; `intervalHours?` (QXH); `times?`: `HH:mm`[] |
|
||||||
| Admin review flags | Rx header `flags[]`: `code` (reason enum), `severity` info\|warning\|critical, `note`, open/resolved + resolution trail; queue filter + badges; add/resolve in Admin detail — **staff-only** |
|
|
||||||
| Mob reminders | `POST /health/reminders` with `times[]`, grace, escalate; calendar UI; confirm/snooze |
|
| Mob reminders | `POST /health/reminders` with `times[]`, grace, escalate; calendar UI; confirm/snooze |
|
||||||
| Sync intent | Persist pharmacist final `times` (+ interval) on approve/dispense so Mob can seed reminders without re-derive — **when** to push still open (Admin Q13) |
|
| Sync intent | Persist pharmacist final `times` (+ interval) on approve/dispense so Mob can seed reminders without re-derive — **when** to push still open (Admin Q13) |
|
||||||
| Customer Rx Care | Mob/Ecom upload + `prescriptions/check` stock match statuses — **no** pharmacist flagging API/UI in Mob yet (**TBD** if customers ever see staff flags) |
|
| Customer Rx Care | Mob/Ecom upload + `prescriptions/check` stock match statuses |
|
||||||
| B2B | Member submits Rx; HR/Finance never see clinical lines |
|
| B2B | Member submits Rx; HR/Finance never see clinical lines |
|
||||||
|
|
||||||
Admin helpers: `src/lib/dosingSchedule.ts`, `src/lib/prescriptionFlags.ts`.
|
Admin helper: `src/lib/dosingSchedule.ts`.
|
||||||
|
|
||||||
### 2.6 Catalog / Items + multi-UOM + storefront PDP
|
### 2.6 Catalog / Items + multi-UOM + storefront PDP
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
| Date | Change |
|
| Date | Change |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 2026-08-10 | **Rx prescription flags:** `flags[]` on Rx; add/resolve endpoints; list filters `flagged` / `flagCode`; Admin queue badges + detail Flags section; optional initial flag on intake |
|
|
||||||
| 2026-08-08 | **Entity schemas pack (platform):** [`schemas.md`](./schemas.md) covers shared + Admin + B2B + Ecom/backend DTOs; linked from Conventions + master |
|
| 2026-08-08 | **Entity schemas pack (platform):** [`schemas.md`](./schemas.md) covers shared + Admin + B2B + Ecom/backend DTOs; linked from Conventions + master |
|
||||||
| 2026-08-08 | **B2B org alignment:** `pending_activation`, `POST .../activate` + `CommercialTerms`/`Money`, events `org.activated`/`org.suspended`; B2B register path `POST /v1/org/register` |
|
| 2026-08-08 | **B2B org alignment:** `pending_activation`, `POST .../activate` + `CommercialTerms`/`Money`, events `org.activated`/`org.suspended`; B2B register path `POST /v1/org/register` |
|
||||||
| 2026-08-07 | Pointer to cross-app [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) + pre-push refresh rule |
|
| 2026-08-07 | Pointer to cross-app [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) + pre-push refresh rule |
|
||||||
|
|
@ -254,37 +253,15 @@ UI: `/orders/new` (Guest customer vs Registered customer toggle), `/orders/:id`
|
||||||
|
|
||||||
| Method | Path | Roles | Description |
|
| Method | Path | Roles | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| GET | `/pharmacy/prescriptions` | pharmacist, super_admin | Branch-scoped queue; filters: `status`, `controlled`, `flagged` (`open`\|`none`), `flagCode`, `q` |
|
| GET | `/pharmacy/prescriptions` | pharmacist, super_admin | Branch-scoped queue; filters: `status`, `controlled`, `q` |
|
||||||
| POST | `/pharmacy/prescriptions` | pharmacist, super_admin, **doctor** | Staff Rx intake or doctor-authored Rx; body includes `doctorId` when attributed; optional `flags[]` seed on create |
|
| POST | `/pharmacy/prescriptions` | pharmacist, super_admin, **doctor** | Staff Rx intake or doctor-authored Rx; body includes `doctorId` when attributed |
|
||||||
| POST | `/admin/prescriptions/:id/verify` | pharmacist, super_admin | `{ status: approved\|queried\|rejected, qtyAdjustments?, substitute?, notes? }` |
|
| POST | `/admin/prescriptions/:id/verify` | pharmacist, super_admin | `{ status: approved\|queried\|rejected, qtyAdjustments?, substitute?, notes? }` |
|
||||||
| POST | `/pharmacy/prescriptions/:id/dispense` | pharmacist | `{ batch, quantity }` stamped pharmacistId + time |
|
| POST | `/pharmacy/prescriptions/:id/dispense` | pharmacist | `{ batch, quantity }` stamped pharmacistId + time |
|
||||||
| GET | `/pharmacy/prescriptions/controlled-report` | pharmacist, super_admin | Controlled-substance report |
|
| GET | `/pharmacy/prescriptions/controlled-report` | pharmacist, super_admin | Controlled-substance report |
|
||||||
| GET | `/pharmacy/prescriptions/:id` | pharmacist, super_admin, doctor (own) | Detail: items (incl. dosing schedule), `flags[]`, controlled line flags, review trail, linked doctor |
|
| GET | `/pharmacy/prescriptions/:id` | pharmacist, super_admin, doctor (own) | Detail: items (incl. dosing schedule), controlled flags, review trail, linked doctor |
|
||||||
| POST | `/pharmacy/prescriptions/:id/flags` | pharmacist, super_admin | Add flag `{ code, severity?, note }` → opens review marker (defaults severity from code) |
|
|
||||||
| POST | `/pharmacy/prescriptions/:id/flags/:flagId/resolve` | pharmacist, super_admin | `{ resolutionNote }` → `status=resolved`, stamps `resolvedBy` / `resolvedAt` |
|
|
||||||
| GET | `/pharmacy/refills` | pharmacist, super_admin | Days-of-supply / script expiry oversight |
|
| GET | `/pharmacy/refills` | pharmacist, super_admin | Days-of-supply / script expiry oversight |
|
||||||
|
|
||||||
UI: `/prescriptions` (open-flag count badge + Filters: Flags / Flag type), `/prescriptions/new` (doctor select + per-line dosing + optional initial flag), `/prescriptions/:id` — Approve is the sole primary header action; Reject / Query live in an overflow menu and as an inline footer on the verification section; **Flags** section supports add + resolve with history. Medications table shows interval/frequency and dose times.
|
UI: `/prescriptions/new` (doctor select + per-line dosing schedule), `/prescriptions/:id` — Approve is the sole primary header action; Reject / Query live in an overflow menu and as an inline footer on the verification section. Medications table shows interval/frequency and dose times.
|
||||||
|
|
||||||
### Prescription flags (review markers)
|
|
||||||
|
|
||||||
Operational / clinical **review flags** on an Rx header — distinct from line-item `controlled` and workflow `status`. Staff add flags during intake or review; open flags surface on the queue until resolved. Resolved flags remain for audit.
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `id` | string | Flag id |
|
|
||||||
| `code` | enum | `controlled_substance` \| `interaction_concern` \| `incomplete_rx` \| `fraud_suspicion` \| `stock_shortage` \| `needs_clarification` \| `allergy_concern` \| `dosing_concern` \| `other` |
|
|
||||||
| `severity` | `info` \| `warning` \| `critical` | Defaults from `code` when omitted on create |
|
|
||||||
| `note` | string | Why this Rx needs attention |
|
|
||||||
| `createdBy` | string | Staff display name (or id + denorm) |
|
|
||||||
| `createdAt` | ISO datetime | |
|
|
||||||
| `status` | `open` \| `resolved` | |
|
|
||||||
| `resolvedBy?` | string | Set on resolve |
|
|
||||||
| `resolvedAt?` | ISO datetime | Set on resolve |
|
|
||||||
| `resolutionNote?` | string | How it was cleared |
|
|
||||||
|
|
||||||
Rx list may expose derived helpers: `openFlagCount`, highest open severity (for badge colour).
|
|
||||||
Helper (Admin): `src/lib/prescriptionFlags.ts`. Mock seeds: `rx-1001`–`1003`, `rx-1005`, `rx-1008`.
|
|
||||||
|
|
||||||
### Prescription medication line items (schedule)
|
### Prescription medication line items (schedule)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,34 +339,6 @@ interface PrescriptionItem {
|
||||||
instructions?: string
|
instructions?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Staff review markers — distinct from line `controlled` and workflow status. */
|
|
||||||
type PrescriptionFlagCode =
|
|
||||||
| 'controlled_substance'
|
|
||||||
| 'interaction_concern'
|
|
||||||
| 'incomplete_rx'
|
|
||||||
| 'fraud_suspicion'
|
|
||||||
| 'stock_shortage'
|
|
||||||
| 'needs_clarification'
|
|
||||||
| 'allergy_concern'
|
|
||||||
| 'dosing_concern'
|
|
||||||
| 'other'
|
|
||||||
|
|
||||||
type PrescriptionFlagSeverity = 'info' | 'warning' | 'critical'
|
|
||||||
type PrescriptionFlagStatus = 'open' | 'resolved'
|
|
||||||
|
|
||||||
interface PrescriptionFlag {
|
|
||||||
id: string
|
|
||||||
code: PrescriptionFlagCode
|
|
||||||
severity: PrescriptionFlagSeverity
|
|
||||||
note: string
|
|
||||||
createdBy: string
|
|
||||||
createdAt: Timestamp
|
|
||||||
status: PrescriptionFlagStatus
|
|
||||||
resolvedBy?: string
|
|
||||||
resolvedAt?: Timestamp
|
|
||||||
resolutionNote?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Prescription {
|
interface Prescription {
|
||||||
id: string
|
id: string
|
||||||
customerName: string
|
customerName: string
|
||||||
|
|
@ -387,7 +359,6 @@ interface Prescription {
|
||||||
rejection_reason?: string
|
rejection_reason?: string
|
||||||
days_supply?: number
|
days_supply?: number
|
||||||
refill_due_at?: DateOnly
|
refill_due_at?: DateOnly
|
||||||
flags?: PrescriptionFlag[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrescriptionVerifyBody = {
|
type PrescriptionVerifyBody = {
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import type {
|
|
||||||
Prescription,
|
|
||||||
PrescriptionFlag,
|
|
||||||
PrescriptionFlagCode,
|
|
||||||
PrescriptionFlagSeverity,
|
|
||||||
} from '@/types'
|
|
||||||
|
|
||||||
export const PRESCRIPTION_FLAG_CODES: {
|
|
||||||
value: PrescriptionFlagCode
|
|
||||||
label: string
|
|
||||||
defaultSeverity: PrescriptionFlagSeverity
|
|
||||||
}[] = [
|
|
||||||
{ value: 'controlled_substance', label: 'Controlled substance', defaultSeverity: 'warning' },
|
|
||||||
{ value: 'interaction_concern', label: 'Interaction concern', defaultSeverity: 'critical' },
|
|
||||||
{ value: 'incomplete_rx', label: 'Incomplete Rx', defaultSeverity: 'warning' },
|
|
||||||
{ value: 'fraud_suspicion', label: 'Fraud suspicion', defaultSeverity: 'critical' },
|
|
||||||
{ value: 'stock_shortage', label: 'Stock shortage', defaultSeverity: 'warning' },
|
|
||||||
{ value: 'needs_clarification', label: 'Needs doctor clarification', defaultSeverity: 'warning' },
|
|
||||||
{ value: 'allergy_concern', label: 'Allergy concern', defaultSeverity: 'critical' },
|
|
||||||
{ value: 'dosing_concern', label: 'Dosing concern', defaultSeverity: 'warning' },
|
|
||||||
{ value: 'other', label: 'Other', defaultSeverity: 'info' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const labelByCode = Object.fromEntries(
|
|
||||||
PRESCRIPTION_FLAG_CODES.map((c) => [c.value, c.label]),
|
|
||||||
) as Record<PrescriptionFlagCode, string>
|
|
||||||
|
|
||||||
const defaultSeverityByCode = Object.fromEntries(
|
|
||||||
PRESCRIPTION_FLAG_CODES.map((c) => [c.value, c.defaultSeverity]),
|
|
||||||
) as Record<PrescriptionFlagCode, PrescriptionFlagSeverity>
|
|
||||||
|
|
||||||
export function flagCodeLabel(code: PrescriptionFlagCode): string {
|
|
||||||
return labelByCode[code] ?? code
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultSeverityForFlag(code: PrescriptionFlagCode): PrescriptionFlagSeverity {
|
|
||||||
return defaultSeverityByCode[code] ?? 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function severityBadgeVariant(
|
|
||||||
severity: PrescriptionFlagSeverity,
|
|
||||||
): 'muted' | 'warning' | 'danger' {
|
|
||||||
if (severity === 'critical') return 'danger'
|
|
||||||
if (severity === 'warning') return 'warning'
|
|
||||||
return 'muted'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openFlags(rx: Prescription | undefined | null): PrescriptionFlag[] {
|
|
||||||
return (rx?.flags ?? []).filter((f) => f.status === 'open')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openFlagCount(rx: Prescription | undefined | null): number {
|
|
||||||
return openFlags(rx).length
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasOpenFlags(rx: Prescription | undefined | null): boolean {
|
|
||||||
return openFlagCount(rx) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export function highestOpenSeverity(
|
|
||||||
rx: Prescription | undefined | null,
|
|
||||||
): PrescriptionFlagSeverity | null {
|
|
||||||
const open = openFlags(rx)
|
|
||||||
if (open.some((f) => f.severity === 'critical')) return 'critical'
|
|
||||||
if (open.some((f) => f.severity === 'warning')) return 'warning'
|
|
||||||
if (open.length > 0) return 'info'
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Newest first; open flags before resolved when timestamps tie. */
|
|
||||||
export function sortFlags(flags: PrescriptionFlag[]): PrescriptionFlag[] {
|
|
||||||
return [...flags].sort((a, b) => {
|
|
||||||
if (a.status !== b.status) return a.status === 'open' ? -1 : 1
|
|
||||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -47,17 +47,6 @@ export let prescriptions: Prescription[] = [
|
||||||
],
|
],
|
||||||
submittedAt: '2026-08-06T08:10:00Z',
|
submittedAt: '2026-08-06T08:10:00Z',
|
||||||
prescriber: 'Dr. Selam',
|
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',
|
id: 'rx-1002',
|
||||||
|
|
@ -75,17 +64,6 @@ export let prescriptions: Prescription[] = [
|
||||||
],
|
],
|
||||||
submittedAt: '2026-08-06T09:20:00Z',
|
submittedAt: '2026-08-06T09:20:00Z',
|
||||||
prescriber: 'Dr. Yohannes',
|
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',
|
id: 'rx-1003',
|
||||||
|
|
@ -104,26 +82,6 @@ export let prescriptions: Prescription[] = [
|
||||||
submittedAt: '2026-08-06T07:45:00Z',
|
submittedAt: '2026-08-06T07:45:00Z',
|
||||||
prescriber: 'Dr. Helen',
|
prescriber: 'Dr. Helen',
|
||||||
reviewStartedAt: '2026-08-06T08:05:00Z',
|
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',
|
id: 'rx-1004',
|
||||||
|
|
@ -153,29 +111,6 @@ export let prescriptions: Prescription[] = [
|
||||||
reviewedBy: 'Hana Pharmacist',
|
reviewedBy: 'Hana Pharmacist',
|
||||||
reviewedAt: '2026-08-05T12:05:00Z',
|
reviewedAt: '2026-08-05T12:05:00Z',
|
||||||
decisionNote: 'Prescriber signature is illegible — a clearer scan was requested.',
|
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',
|
id: 'rx-1006',
|
||||||
|
|
@ -224,32 +159,6 @@ export let prescriptions: Prescription[] = [
|
||||||
reviewedBy: 'Hana Pharmacist',
|
reviewedBy: 'Hana Pharmacist',
|
||||||
reviewedAt: '2026-08-02T11:22:00Z',
|
reviewedAt: '2026-08-02T11:22:00Z',
|
||||||
decisionNote: 'Prescriber is not on the national register.',
|
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',
|
id: 'rx-1009',
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,8 @@ import {
|
||||||
DOSING_FREQUENCY_OPTIONS,
|
DOSING_FREQUENCY_OPTIONS,
|
||||||
defaultTimesForFrequency,
|
defaultTimesForFrequency,
|
||||||
} from '@/lib/dosingSchedule'
|
} from '@/lib/dosingSchedule'
|
||||||
import {
|
|
||||||
PRESCRIPTION_FLAG_CODES,
|
|
||||||
defaultSeverityForFlag,
|
|
||||||
} from '@/lib/prescriptionFlags'
|
|
||||||
import { branches, doctors, prescriptions } from '@/mocks/data'
|
import { branches, doctors, prescriptions } from '@/mocks/data'
|
||||||
import type { DosingFrequency, Prescription, PrescriptionFlagCode } from '@/types'
|
import type { DosingFrequency, Prescription } from '@/types'
|
||||||
|
|
||||||
export function CreatePrescriptionPage() {
|
export function CreatePrescriptionPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -42,9 +38,6 @@ export function CreatePrescriptionPage() {
|
||||||
frequency: 'BID' as DosingFrequency,
|
frequency: 'BID' as DosingFrequency,
|
||||||
intervalHours: '8',
|
intervalHours: '8',
|
||||||
times: defaultTimesForFrequency('BID'),
|
times: defaultTimesForFrequency('BID'),
|
||||||
attachFlag: false,
|
|
||||||
flagCode: 'needs_clarification' as PrescriptionFlagCode,
|
|
||||||
flagNote: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedDoctor = activeDoctors.find((d) => d.id === form.doctorId)
|
const selectedDoctor = activeDoctors.find((d) => d.id === form.doctorId)
|
||||||
|
|
@ -107,11 +100,6 @@ export function CreatePrescriptionPage() {
|
||||||
toast.error('Enter hours between doses')
|
toast.error('Enter hours between doses')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (form.attachFlag && !form.flagNote.trim()) {
|
|
||||||
toast.error('Add a note for the initial flag')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
const rx: Prescription = {
|
const rx: Prescription = {
|
||||||
id: `rx-${Date.now().toString().slice(-6)}`,
|
id: `rx-${Date.now().toString().slice(-6)}`,
|
||||||
customerName: form.customerName,
|
customerName: form.customerName,
|
||||||
|
|
@ -127,25 +115,10 @@ export function CreatePrescriptionPage() {
|
||||||
times: cleanedTimes,
|
times: cleanedTimes,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submittedAt: now,
|
submittedAt: new Date().toISOString(),
|
||||||
prescriber: selectedDoctor.name,
|
prescriber: selectedDoctor.name,
|
||||||
doctorId: selectedDoctor.id,
|
doctorId: selectedDoctor.id,
|
||||||
orgId: selectedDoctor.orgId,
|
orgId: selectedDoctor.orgId,
|
||||||
...(form.attachFlag
|
|
||||||
? {
|
|
||||||
flags: [
|
|
||||||
{
|
|
||||||
id: `rxf-${Date.now().toString(36)}`,
|
|
||||||
code: form.flagCode,
|
|
||||||
severity: defaultSeverityForFlag(form.flagCode),
|
|
||||||
note: form.flagNote.trim(),
|
|
||||||
createdBy: user?.name ?? 'Pharmacist',
|
|
||||||
createdAt: now,
|
|
||||||
status: 'open' as const,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
}
|
||||||
prescriptions.unshift(rx)
|
prescriptions.unshift(rx)
|
||||||
toast.success('Prescription intake created')
|
toast.success('Prescription intake created')
|
||||||
|
|
@ -317,44 +290,6 @@ export function CreatePrescriptionPage() {
|
||||||
/>
|
/>
|
||||||
Controlled substance
|
Controlled substance
|
||||||
</label>
|
</label>
|
||||||
<div className={cn('space-y-3 rounded-xl border border-dashed p-4', createFieldFull)}>
|
|
||||||
<label className="flex items-center gap-2 text-sm font-medium">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={form.attachFlag}
|
|
||||||
onChange={(e) => setForm({ ...form, attachFlag: e.target.checked })}
|
|
||||||
/>
|
|
||||||
Attach an initial review flag
|
|
||||||
</label>
|
|
||||||
{form.attachFlag ? (
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label>Flag reason</Label>
|
|
||||||
<SelectNative
|
|
||||||
value={form.flagCode}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm({ ...form, flagCode: e.target.value as PrescriptionFlagCode })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{PRESCRIPTION_FLAG_CODES.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</SelectNative>
|
|
||||||
</div>
|
|
||||||
<div className={cn('space-y-1.5', createFieldFull)}>
|
|
||||||
<Label>Flag note</Label>
|
|
||||||
<Input
|
|
||||||
required={form.attachFlag}
|
|
||||||
value={form.flagNote}
|
|
||||||
onChange={(e) => setForm({ ...form, flagNote: e.target.value })}
|
|
||||||
placeholder="Why this Rx needs attention"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</CreatePageLayout>
|
</CreatePageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,77 @@
|
||||||
import { useState, type FormEvent } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Check, ClipboardCopy, Flag, HelpCircle, Plus, X } from 'lucide-react'
|
import { Check, ClipboardCopy, HelpCircle, X } from 'lucide-react'
|
||||||
import { DetailPageLayout } from '@/layouts/DetailPageLayout'
|
import { DetailPageLayout } from '@/layouts/DetailPageLayout'
|
||||||
import { ActivityTimeline, DetailSection, type TimelineEvent } from '@/components/detail/ActivityTimeline'
|
import { ActivityTimeline, DetailSection } from '@/components/detail/ActivityTimeline'
|
||||||
import { ApprovalStepper, type ApprovalStep } from '@/components/detail/ApprovalStepper'
|
import { ApprovalStepper, type ApprovalStep } from '@/components/detail/ApprovalStepper'
|
||||||
import { KeyValueGrid } from '@/components/detail/KeyValueGrid'
|
import { KeyValueGrid } from '@/components/detail/KeyValueGrid'
|
||||||
import { RowActions, type RowAction } from '@/components/table/RowActions'
|
import { RowActions, type RowAction } from '@/components/table/RowActions'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { SelectNative } from '@/components/ui/select-native'
|
|
||||||
import { StatusBadge, statusLabel } from '@/components/ui/status-badge'
|
import { StatusBadge, statusLabel } from '@/components/ui/status-badge'
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { branches, doctors, prescriptions } from '@/mocks/data'
|
import { branches, doctors, prescriptions } from '@/mocks/data'
|
||||||
import { useAuthStore } from '@/zustand/authStore'
|
import { useAuthStore } from '@/zustand/authStore'
|
||||||
import { formatDoseTimes, formatFrequencyLabel } from '@/lib/dosingSchedule'
|
import { formatDoseTimes, formatFrequencyLabel } from '@/lib/dosingSchedule'
|
||||||
import {
|
|
||||||
PRESCRIPTION_FLAG_CODES,
|
|
||||||
defaultSeverityForFlag,
|
|
||||||
flagCodeLabel,
|
|
||||||
openFlagCount,
|
|
||||||
severityBadgeVariant,
|
|
||||||
sortFlags,
|
|
||||||
} from '@/lib/prescriptionFlags'
|
|
||||||
import { formatDateTime } from '@/lib/utils'
|
import { formatDateTime } from '@/lib/utils'
|
||||||
import type {
|
import type { Prescription } from '@/types'
|
||||||
Prescription,
|
|
||||||
PrescriptionFlag,
|
|
||||||
PrescriptionFlagCode,
|
|
||||||
PrescriptionFlagSeverity,
|
|
||||||
} from '@/types'
|
|
||||||
|
|
||||||
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
|
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
|
||||||
|
|
||||||
function buildSteps(rx: Prescription): ApprovalStep[] {
|
export function PrescriptionDetailPage() {
|
||||||
|
const { id } = useParams()
|
||||||
|
const user = useAuthStore((s) => s.user)
|
||||||
|
const seed = prescriptions.find((p) => p.id === id)
|
||||||
|
const [rx, setRx] = useState<Prescription | undefined>(seed ? { ...seed, items: [...seed.items] } : undefined)
|
||||||
|
|
||||||
|
if (!rx) {
|
||||||
|
return (
|
||||||
|
<DetailPageLayout backTo="/prescriptions" title="Prescription not found">
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/prescriptions">Back to prescriptions</Link>
|
||||||
|
</Button>
|
||||||
|
</DetailPageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = branches.find((b) => b.id === rx.branchId)
|
||||||
|
const doctor = rx.doctorId ? doctors.find((d) => d.id === rx.doctorId) : undefined
|
||||||
|
const controlled = rx.items.some((i) => i.controlled)
|
||||||
|
const isOpen = OPEN_STATUSES.includes(rx.status)
|
||||||
|
|
||||||
|
const act = (next: Prescription['status']) => {
|
||||||
|
const patch = {
|
||||||
|
status: next,
|
||||||
|
reviewedBy: user?.name ?? 'Pharmacist',
|
||||||
|
reviewedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
setRx((prev) => (prev ? { ...prev, ...patch } : prev))
|
||||||
|
const target = prescriptions.find((p) => p.id === rx.id)
|
||||||
|
if (target) Object.assign(target, patch)
|
||||||
|
toast.success(`Prescription ${next}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyId = () => {
|
||||||
|
void navigator.clipboard?.writeText(rx.id)
|
||||||
|
toast.message(`${rx.id} copied to clipboard`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const overflow: RowAction[] = [
|
||||||
|
...(isOpen
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Reject prescription',
|
||||||
|
icon: <X className="h-3.5 w-3.5" />,
|
||||||
|
variant: 'destructive' as const,
|
||||||
|
onClick: () => act('rejected'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ label: 'Copy Rx ID', icon: <ClipboardCopy className="h-3.5 w-3.5" />, onClick: copyId },
|
||||||
|
]
|
||||||
|
|
||||||
|
const steps = useMemo<ApprovalStep[]>(() => {
|
||||||
const decided = rx.status === 'approved' || rx.status === 'rejected'
|
const decided = rx.status === 'approved' || rx.status === 'rejected'
|
||||||
const reviewSeen = decided || rx.status === 'queried'
|
const reviewSeen = decided || rx.status === 'queried'
|
||||||
|
|
||||||
|
|
@ -81,141 +109,12 @@ function buildSteps(rx: Prescription): ApprovalStep[] {
|
||||||
: rx.status === 'queried'
|
: rx.status === 'queried'
|
||||||
? 'current'
|
? 'current'
|
||||||
: 'upcoming',
|
: 'upcoming',
|
||||||
actor:
|
actor: rx.status === 'queried' || rx.status === 'approved' || rx.status === 'rejected' ? rx.reviewedBy : undefined,
|
||||||
rx.status === 'queried' || rx.status === 'approved' || rx.status === 'rejected'
|
|
||||||
? rx.reviewedBy
|
|
||||||
: undefined,
|
|
||||||
at: rx.status === 'submitted' || rx.status === 'under_review' ? undefined : rx.reviewedAt,
|
at: rx.status === 'submitted' || rx.status === 'under_review' ? undefined : rx.reviewedAt,
|
||||||
hint: rx.status === 'queried' ? 'Awaiting a clearer scan from the customer' : undefined,
|
hint: rx.status === 'queried' ? 'Awaiting a clearer scan from the customer' : undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}, [rx])
|
||||||
|
|
||||||
export function PrescriptionDetailPage() {
|
|
||||||
const { id } = useParams()
|
|
||||||
const user = useAuthStore((s) => s.user)
|
|
||||||
const seed = prescriptions.find((p) => p.id === id)
|
|
||||||
const [rx, setRx] = useState<Prescription | undefined>(
|
|
||||||
seed ? { ...seed, items: [...seed.items], flags: seed.flags ? [...seed.flags] : [] } : undefined,
|
|
||||||
)
|
|
||||||
const [addOpen, setAddOpen] = useState(false)
|
|
||||||
const [resolveTarget, setResolveTarget] = useState<PrescriptionFlag | null>(null)
|
|
||||||
const [addForm, setAddForm] = useState({
|
|
||||||
code: 'needs_clarification' as PrescriptionFlagCode,
|
|
||||||
severity: 'warning' as PrescriptionFlagSeverity,
|
|
||||||
note: '',
|
|
||||||
})
|
|
||||||
const [resolutionNote, setResolutionNote] = useState('')
|
|
||||||
|
|
||||||
if (!rx) {
|
|
||||||
return (
|
|
||||||
<DetailPageLayout backTo="/prescriptions" title="Prescription not found">
|
|
||||||
<Button asChild>
|
|
||||||
<Link to="/prescriptions">Back to prescriptions</Link>
|
|
||||||
</Button>
|
|
||||||
</DetailPageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const branch = branches.find((b) => b.id === rx.branchId)
|
|
||||||
const doctor = rx.doctorId ? doctors.find((d) => d.id === rx.doctorId) : undefined
|
|
||||||
const controlled = rx.items.some((i) => i.controlled)
|
|
||||||
const isOpen = OPEN_STATUSES.includes(rx.status)
|
|
||||||
const flags = sortFlags(rx.flags ?? [])
|
|
||||||
const openCount = openFlagCount(rx)
|
|
||||||
const steps = buildSteps(rx)
|
|
||||||
|
|
||||||
const persistFlags = (nextFlags: PrescriptionFlag[]) => {
|
|
||||||
setRx((prev) => (prev ? { ...prev, flags: nextFlags } : prev))
|
|
||||||
const target = prescriptions.find((p) => p.id === rx.id)
|
|
||||||
if (target) target.flags = nextFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
const act = (next: Prescription['status']) => {
|
|
||||||
const patch = {
|
|
||||||
status: next,
|
|
||||||
reviewedBy: user?.name ?? 'Pharmacist',
|
|
||||||
reviewedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
setRx((prev) => (prev ? { ...prev, ...patch } : prev))
|
|
||||||
const target = prescriptions.find((p) => p.id === rx.id)
|
|
||||||
if (target) Object.assign(target, patch)
|
|
||||||
toast.success(`Prescription ${next}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyId = () => {
|
|
||||||
void navigator.clipboard?.writeText(rx.id)
|
|
||||||
toast.message(`${rx.id} copied to clipboard`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAddDialog = () => {
|
|
||||||
setAddForm({
|
|
||||||
code: 'needs_clarification',
|
|
||||||
severity: defaultSeverityForFlag('needs_clarification'),
|
|
||||||
note: '',
|
|
||||||
})
|
|
||||||
setAddOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onAddFlag = (e: FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const note = addForm.note.trim()
|
|
||||||
if (!note) {
|
|
||||||
toast.error('Add a short note for the flag')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const flag: PrescriptionFlag = {
|
|
||||||
id: `rxf-${Date.now().toString(36)}`,
|
|
||||||
code: addForm.code,
|
|
||||||
severity: addForm.severity,
|
|
||||||
note,
|
|
||||||
createdBy: user?.name ?? 'Pharmacist',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
status: 'open',
|
|
||||||
}
|
|
||||||
persistFlags([flag, ...(rx.flags ?? [])])
|
|
||||||
setAddOpen(false)
|
|
||||||
toast.success('Flag added')
|
|
||||||
}
|
|
||||||
|
|
||||||
const onResolveFlag = (e: FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!resolveTarget) return
|
|
||||||
const note = resolutionNote.trim()
|
|
||||||
if (!note) {
|
|
||||||
toast.error('Add a resolution note')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const nextFlags = (rx.flags ?? []).map((f) =>
|
|
||||||
f.id === resolveTarget.id
|
|
||||||
? {
|
|
||||||
...f,
|
|
||||||
status: 'resolved' as const,
|
|
||||||
resolvedBy: user?.name ?? 'Pharmacist',
|
|
||||||
resolvedAt: new Date().toISOString(),
|
|
||||||
resolutionNote: note,
|
|
||||||
}
|
|
||||||
: f,
|
|
||||||
)
|
|
||||||
persistFlags(nextFlags)
|
|
||||||
setResolveTarget(null)
|
|
||||||
setResolutionNote('')
|
|
||||||
toast.success('Flag resolved')
|
|
||||||
}
|
|
||||||
|
|
||||||
const overflow: RowAction[] = [
|
|
||||||
...(isOpen
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Reject prescription',
|
|
||||||
icon: <X className="h-3.5 w-3.5" />,
|
|
||||||
variant: 'destructive' as const,
|
|
||||||
onClick: () => act('rejected'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ label: 'Copy Rx ID', icon: <ClipboardCopy className="h-3.5 w-3.5" />, onClick: copyId },
|
|
||||||
]
|
|
||||||
|
|
||||||
const decisionCluster = (
|
const decisionCluster = (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
|
|
@ -247,74 +146,7 @@ export function PrescriptionDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const trailEvents: TimelineEvent[] = []
|
|
||||||
trailEvents.push({
|
|
||||||
id: '1',
|
|
||||||
at: rx.submittedAt,
|
|
||||||
title: 'Submitted by customer',
|
|
||||||
detail: branch?.name,
|
|
||||||
tone: 'default',
|
|
||||||
})
|
|
||||||
if (rx.reviewStartedAt) {
|
|
||||||
trailEvents.push({
|
|
||||||
id: '2',
|
|
||||||
at: rx.reviewStartedAt,
|
|
||||||
title: 'Picked up for pharmacist review',
|
|
||||||
detail: branch?.name,
|
|
||||||
tone: 'warning',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
trailEvents.push({
|
|
||||||
id: '2',
|
|
||||||
at: rx.submittedAt,
|
|
||||||
title: 'Queued for pharmacist review',
|
|
||||||
tone: 'warning',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (rx.status === 'approved') {
|
|
||||||
trailEvents.push({
|
|
||||||
id: '3',
|
|
||||||
at: rx.reviewedAt ?? rx.submittedAt,
|
|
||||||
title: 'Approved for dispensing',
|
|
||||||
detail: rx.reviewedBy,
|
|
||||||
tone: 'success',
|
|
||||||
})
|
|
||||||
} else if (rx.status === 'queried') {
|
|
||||||
trailEvents.push({
|
|
||||||
id: '3',
|
|
||||||
at: rx.reviewedAt ?? rx.submittedAt,
|
|
||||||
title: 'Query sent to customer',
|
|
||||||
detail: rx.reviewedBy,
|
|
||||||
tone: 'warning',
|
|
||||||
})
|
|
||||||
} else if (rx.status === 'rejected') {
|
|
||||||
trailEvents.push({
|
|
||||||
id: '3',
|
|
||||||
at: rx.reviewedAt ?? rx.submittedAt,
|
|
||||||
title: 'Rejected',
|
|
||||||
detail: rx.reviewedBy,
|
|
||||||
tone: 'danger',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for (const f of flags) {
|
|
||||||
trailEvents.push({
|
|
||||||
id: `flag-${f.id}`,
|
|
||||||
at: f.status === 'resolved' && f.resolvedAt ? f.resolvedAt : f.createdAt,
|
|
||||||
title:
|
|
||||||
f.status === 'resolved'
|
|
||||||
? `Resolved flag · ${flagCodeLabel(f.code)}`
|
|
||||||
: `Flagged · ${flagCodeLabel(f.code)}`,
|
|
||||||
detail:
|
|
||||||
f.status === 'resolved'
|
|
||||||
? `${f.resolvedBy ?? 'Staff'}: ${f.resolutionNote ?? f.note}`
|
|
||||||
: `${f.createdBy}: ${f.note}`,
|
|
||||||
tone: f.status === 'resolved' ? 'success' : f.severity === 'critical' ? 'danger' : 'warning',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
trailEvents.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<DetailPageLayout
|
<DetailPageLayout
|
||||||
backTo="/prescriptions"
|
backTo="/prescriptions"
|
||||||
backLabel="Back to prescriptions"
|
backLabel="Back to prescriptions"
|
||||||
|
|
@ -324,12 +156,6 @@ export function PrescriptionDetailPage() {
|
||||||
<>
|
<>
|
||||||
<StatusBadge domain="prescription" status={rx.status} />
|
<StatusBadge domain="prescription" status={rx.status} />
|
||||||
{controlled ? <Badge variant="danger">Controlled</Badge> : null}
|
{controlled ? <Badge variant="danger">Controlled</Badge> : null}
|
||||||
{openCount > 0 ? (
|
|
||||||
<Badge variant="warning" className="gap-1">
|
|
||||||
<Flag className="h-3 w-3" />
|
|
||||||
{openCount} open flag{openCount === 1 ? '' : 's'}
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
|
|
@ -351,7 +177,68 @@ export function PrescriptionDetailPage() {
|
||||||
}
|
}
|
||||||
sidebar={
|
sidebar={
|
||||||
<DetailSection title="Review trail">
|
<DetailSection title="Review trail">
|
||||||
<ActivityTimeline events={trailEvents} />
|
<ActivityTimeline
|
||||||
|
events={[
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
at: rx.submittedAt,
|
||||||
|
title: 'Submitted by customer',
|
||||||
|
detail: branch?.name,
|
||||||
|
tone: 'default',
|
||||||
|
},
|
||||||
|
...(rx.reviewStartedAt
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
at: rx.reviewStartedAt,
|
||||||
|
title: 'Picked up for pharmacist review',
|
||||||
|
detail: branch?.name,
|
||||||
|
tone: 'warning' as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
at: rx.submittedAt,
|
||||||
|
title: 'Queued for pharmacist review',
|
||||||
|
tone: 'warning' as const,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
...(rx.status === 'approved'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
at: rx.reviewedAt ?? rx.submittedAt,
|
||||||
|
title: 'Approved for dispensing',
|
||||||
|
detail: rx.reviewedBy,
|
||||||
|
tone: 'success' as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(rx.status === 'queried'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
at: rx.reviewedAt ?? rx.submittedAt,
|
||||||
|
title: 'Query sent to customer',
|
||||||
|
detail: rx.reviewedBy,
|
||||||
|
tone: 'warning' as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(rx.status === 'rejected'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
at: rx.reviewedAt ?? rx.submittedAt,
|
||||||
|
title: 'Rejected',
|
||||||
|
detail: rx.reviewedBy,
|
||||||
|
tone: 'danger' as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -392,74 +279,6 @@ export function PrescriptionDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
|
|
||||||
<DetailSection
|
|
||||||
title="Flags"
|
|
||||||
description="Operational and clinical review markers — open flags stay visible until resolved"
|
|
||||||
action={
|
|
||||||
<Button type="button" variant="outline" size="sm" className="h-8 gap-1.5" onClick={openAddDialog}>
|
|
||||||
<Plus className="h-3.5 w-3.5" />
|
|
||||||
Add flag
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{flags.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">No flags on this prescription yet.</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{flags.map((flag) => (
|
|
||||||
<li
|
|
||||||
key={flag.id}
|
|
||||||
className={
|
|
||||||
flag.status === 'open'
|
|
||||||
? 'rounded-xl border border-orange/25 bg-orange/5 px-4 py-3'
|
|
||||||
: 'rounded-xl border bg-muted/30 px-4 py-3'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<Badge variant={severityBadgeVariant(flag.severity)} className="gap-1 capitalize">
|
|
||||||
<Flag className="h-3 w-3" />
|
|
||||||
{flag.severity}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline">{flagCodeLabel(flag.code)}</Badge>
|
|
||||||
<Badge variant={flag.status === 'open' ? 'warning' : 'success'}>
|
|
||||||
{flag.status === 'open' ? 'Open' : 'Resolved'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-forest">{flag.note}</p>
|
|
||||||
<p className="text-[11px] text-muted-foreground">
|
|
||||||
Added by {flag.createdBy} · {formatDateTime(flag.createdAt)}
|
|
||||||
</p>
|
|
||||||
{flag.status === 'resolved' ? (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Resolved by {flag.resolvedBy}
|
|
||||||
{flag.resolvedAt ? ` · ${formatDateTime(flag.resolvedAt)}` : ''}
|
|
||||||
{flag.resolutionNote ? ` — ${flag.resolutionNote}` : ''}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{flag.status === 'open' ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 shrink-0"
|
|
||||||
onClick={() => {
|
|
||||||
setResolveTarget(flag)
|
|
||||||
setResolutionNote('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Resolve
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</DetailSection>
|
|
||||||
|
|
||||||
<DetailSection title="Details">
|
<DetailSection title="Details">
|
||||||
<KeyValueGrid
|
<KeyValueGrid
|
||||||
columns={3}
|
columns={3}
|
||||||
|
|
@ -480,7 +299,6 @@ export function PrescriptionDetailPage() {
|
||||||
{ label: 'Submitted', value: formatDateTime(rx.submittedAt) },
|
{ label: 'Submitted', value: formatDateTime(rx.submittedAt) },
|
||||||
{ label: 'Items', value: String(rx.items.length) },
|
{ label: 'Items', value: String(rx.items.length) },
|
||||||
{ label: 'Controlled items', value: controlled ? 'Yes — stamp required' : 'None' },
|
{ label: 'Controlled items', value: controlled ? 'Yes — stamp required' : 'None' },
|
||||||
{ label: 'Open flags', value: String(openCount) },
|
|
||||||
{ label: 'Reviewed by', value: rx.reviewedBy ?? 'Not reviewed yet' },
|
{ label: 'Reviewed by', value: rx.reviewedBy ?? 'Not reviewed yet' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
@ -516,7 +334,9 @@ export function PrescriptionDetailPage() {
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{formatDoseTimes(item.times)}</TableCell>
|
<TableCell className="font-mono text-sm">
|
||||||
|
{formatDoseTimes(item.times)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{item.controlled ? (
|
{item.controlled ? (
|
||||||
<Badge variant="danger">Controlled</Badge>
|
<Badge variant="danger">Controlled</Badge>
|
||||||
|
|
@ -541,121 +361,5 @@ export function PrescriptionDetailPage() {
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
</div>
|
</div>
|
||||||
</DetailPageLayout>
|
</DetailPageLayout>
|
||||||
|
|
||||||
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add prescription flag</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Mark a review, risk, or operational issue. Open flags show on the queue until someone resolves them.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form className="space-y-3" onSubmit={onAddFlag}>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="rx-flag-code">Reason</Label>
|
|
||||||
<SelectNative
|
|
||||||
id="rx-flag-code"
|
|
||||||
value={addForm.code}
|
|
||||||
onChange={(e) => {
|
|
||||||
const code = e.target.value as PrescriptionFlagCode
|
|
||||||
setAddForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
code,
|
|
||||||
severity: defaultSeverityForFlag(code),
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{PRESCRIPTION_FLAG_CODES.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</SelectNative>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="rx-flag-severity">Severity</Label>
|
|
||||||
<SelectNative
|
|
||||||
id="rx-flag-severity"
|
|
||||||
value={addForm.severity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAddForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
severity: e.target.value as PrescriptionFlagSeverity,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="info">Info</option>
|
|
||||||
<option value="warning">Warning</option>
|
|
||||||
<option value="critical">Critical</option>
|
|
||||||
</SelectNative>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="rx-flag-note">Note</Label>
|
|
||||||
<Textarea
|
|
||||||
id="rx-flag-note"
|
|
||||||
required
|
|
||||||
value={addForm.note}
|
|
||||||
onChange={(e) => setAddForm((prev) => ({ ...prev, note: e.target.value }))}
|
|
||||||
placeholder="What should the next pharmacist check?"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button type="button" variant="outline" onClick={() => setAddOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">Add flag</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={!!resolveTarget}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setResolveTarget(null)
|
|
||||||
setResolutionNote('')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Resolve flag</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{resolveTarget
|
|
||||||
? `${flagCodeLabel(resolveTarget.code)} — document how this was cleared.`
|
|
||||||
: 'Document how this was cleared.'}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form className="space-y-3" onSubmit={onResolveFlag}>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label htmlFor="rx-flag-resolution">Resolution note</Label>
|
|
||||||
<Input
|
|
||||||
id="rx-flag-resolution"
|
|
||||||
required
|
|
||||||
value={resolutionNote}
|
|
||||||
onChange={(e) => setResolutionNote(e.target.value)}
|
|
||||||
placeholder="Prescriber confirmed dose · stock transferred…"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setResolveTarget(null)
|
|
||||||
setResolutionNote('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">Mark resolved</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
Clock3,
|
Clock3,
|
||||||
Eye,
|
Eye,
|
||||||
FileHeart,
|
FileHeart,
|
||||||
Flag,
|
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Plus,
|
Plus,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
|
|
@ -24,15 +23,7 @@ import { TableToolbar, type TableFilter } from '@/components/table/TableToolbar'
|
||||||
import { RowActions, type RowAction } from '@/components/table/RowActions'
|
import { RowActions, type RowAction } from '@/components/table/RowActions'
|
||||||
import { useAuthStore } from '@/zustand/authStore'
|
import { useAuthStore } from '@/zustand/authStore'
|
||||||
import { branches, prescriptions as seed } from '@/mocks/data'
|
import { branches, prescriptions as seed } from '@/mocks/data'
|
||||||
import {
|
import type { Prescription } from '@/types'
|
||||||
PRESCRIPTION_FLAG_CODES,
|
|
||||||
flagCodeLabel,
|
|
||||||
hasOpenFlags,
|
|
||||||
highestOpenSeverity,
|
|
||||||
openFlagCount,
|
|
||||||
severityBadgeVariant,
|
|
||||||
} from '@/lib/prescriptionFlags'
|
|
||||||
import type { Prescription, PrescriptionFlagCode } from '@/types'
|
|
||||||
|
|
||||||
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
|
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
|
||||||
|
|
||||||
|
|
@ -53,8 +44,6 @@ export function PrescriptionsPage() {
|
||||||
const [status, setStatus] = useState('all')
|
const [status, setStatus] = useState('all')
|
||||||
const [branchFilter, setBranchFilter] = useState('all')
|
const [branchFilter, setBranchFilter] = useState('all')
|
||||||
const [controlledOnly, setControlledOnly] = useState('all')
|
const [controlledOnly, setControlledOnly] = useState('all')
|
||||||
const [flaggedFilter, setFlaggedFilter] = useState('all')
|
|
||||||
const [flagTypeFilter, setFlagTypeFilter] = useState('all')
|
|
||||||
|
|
||||||
const canPickBranch = !(user?.role === 'pharmacist' && user.branchId)
|
const canPickBranch = !(user?.role === 'pharmacist' && user.branchId)
|
||||||
|
|
||||||
|
|
@ -72,7 +61,6 @@ export function PrescriptionsPage() {
|
||||||
open: scoped.filter((p) => OPEN_STATUSES.includes(p.status)).length,
|
open: scoped.filter((p) => OPEN_STATUSES.includes(p.status)).length,
|
||||||
approved: scoped.filter((p) => p.status === 'approved').length,
|
approved: scoped.filter((p) => p.status === 'approved').length,
|
||||||
controlled: scoped.filter((p) => p.items.some((i) => i.controlled)).length,
|
controlled: scoped.filter((p) => p.items.some((i) => i.controlled)).length,
|
||||||
flagged: scoped.filter((p) => hasOpenFlags(p)).length,
|
|
||||||
}
|
}
|
||||||
}, [scoped])
|
}, [scoped])
|
||||||
|
|
||||||
|
|
@ -81,12 +69,6 @@ export function PrescriptionsPage() {
|
||||||
if (canPickBranch && branchFilter !== 'all') list = list.filter((p) => p.branchId === branchFilter)
|
if (canPickBranch && branchFilter !== 'all') list = list.filter((p) => p.branchId === branchFilter)
|
||||||
if (status !== 'all') list = list.filter((p) => p.status === status)
|
if (status !== 'all') list = list.filter((p) => p.status === status)
|
||||||
if (controlledOnly === 'yes') list = list.filter((p) => p.items.some((i) => i.controlled))
|
if (controlledOnly === 'yes') list = list.filter((p) => p.items.some((i) => i.controlled))
|
||||||
if (flaggedFilter === 'open') list = list.filter((p) => hasOpenFlags(p))
|
|
||||||
if (flaggedFilter === 'none') list = list.filter((p) => !hasOpenFlags(p))
|
|
||||||
if (flagTypeFilter !== 'all') {
|
|
||||||
const code = flagTypeFilter as PrescriptionFlagCode
|
|
||||||
list = list.filter((p) => (p.flags ?? []).some((f) => f.status === 'open' && f.code === code))
|
|
||||||
}
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const needle = q.toLowerCase()
|
const needle = q.toLowerCase()
|
||||||
list = list.filter(
|
list = list.filter(
|
||||||
|
|
@ -97,16 +79,7 @@ export function PrescriptionsPage() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}, [
|
}, [scoped, canPickBranch, q, status, branchFilter, controlledOnly])
|
||||||
scoped,
|
|
||||||
canPickBranch,
|
|
||||||
q,
|
|
||||||
status,
|
|
||||||
branchFilter,
|
|
||||||
controlledOnly,
|
|
||||||
flaggedFilter,
|
|
||||||
flagTypeFilter,
|
|
||||||
])
|
|
||||||
|
|
||||||
const act = (id: string, next: Prescription['status']) => {
|
const act = (id: string, next: Prescription['status']) => {
|
||||||
const patch = {
|
const patch = {
|
||||||
|
|
@ -192,37 +165,15 @@ export function PrescriptionsPage() {
|
||||||
{ value: 'yes', label: 'Controlled only' },
|
{ value: 'yes', label: 'Controlled only' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'flagged',
|
|
||||||
label: 'Flags',
|
|
||||||
value: flaggedFilter,
|
|
||||||
onChange: setFlaggedFilter,
|
|
||||||
options: [
|
|
||||||
{ value: 'all', label: 'All flag states' },
|
|
||||||
{ value: 'open', label: 'Open flags' },
|
|
||||||
{ value: 'none', label: 'No open flags' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'flagType',
|
|
||||||
label: 'Flag type',
|
|
||||||
value: flagTypeFilter,
|
|
||||||
onChange: setFlagTypeFilter,
|
|
||||||
options: [
|
|
||||||
{ value: 'all', label: 'All flag types' },
|
|
||||||
...PRESCRIPTION_FLAG_CODES.map((c) => ({ value: c.value, label: c.label })),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell title={t('prescriptions.title')} subtitle={t('prescriptions.subtitle')}>
|
<PageShell title={t('prescriptions.title')} subtitle={t('prescriptions.subtitle')}>
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<StatCard label="Submitted today" value={kpis.today} hint="Across the scoped queue" icon={FileHeart} tone="brand" />
|
<StatCard label="Submitted today" value={kpis.today} hint="Across the scoped queue" icon={FileHeart} tone="brand" />
|
||||||
<StatCard label="Awaiting decision" value={kpis.open} hint="Submitted, in review or queried" icon={Clock3} tone="orange" />
|
<StatCard label="Awaiting decision" value={kpis.open} hint="Submitted, in review or queried" icon={Clock3} tone="orange" />
|
||||||
<StatCard label="Approved" value={kpis.approved} hint="Ready to dispense" icon={Check} tone="forest" />
|
<StatCard label="Approved" value={kpis.approved} hint="Ready to dispense" icon={Check} tone="forest" />
|
||||||
<StatCard label="Controlled in queue" value={kpis.controlled} hint="Needs stamp / schedule check" icon={ShieldAlert} tone="muted" />
|
<StatCard label="Controlled in queue" value={kpis.controlled} hint="Needs stamp / schedule check" icon={ShieldAlert} tone="muted" />
|
||||||
<StatCard label="Open flags" value={kpis.flagged} hint="Needs review before dispense" icon={Flag} tone="orange" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
|
|
@ -237,8 +188,6 @@ export function PrescriptionsPage() {
|
||||||
setStatus('all')
|
setStatus('all')
|
||||||
setBranchFilter('all')
|
setBranchFilter('all')
|
||||||
setControlledOnly('all')
|
setControlledOnly('all')
|
||||||
setFlaggedFilter('all')
|
|
||||||
setFlagTypeFilter('all')
|
|
||||||
}}
|
}}
|
||||||
actions={
|
actions={
|
||||||
<Button asChild className="h-10 gap-2 shadow-none">
|
<Button asChild className="h-10 gap-2 shadow-none">
|
||||||
|
|
@ -257,7 +206,6 @@ export function PrescriptionsPage() {
|
||||||
<TableHead>Customer</TableHead>
|
<TableHead>Customer</TableHead>
|
||||||
<TableHead>Prescriber</TableHead>
|
<TableHead>Prescriber</TableHead>
|
||||||
<TableHead>Items</TableHead>
|
<TableHead>Items</TableHead>
|
||||||
<TableHead>Flags</TableHead>
|
|
||||||
<TableHead>{t('common.branch')}</TableHead>
|
<TableHead>{t('common.branch')}</TableHead>
|
||||||
<TableHead>{t('common.status')}</TableHead>
|
<TableHead>{t('common.status')}</TableHead>
|
||||||
<TableHead className="text-right">{t('common.actions')}</TableHead>
|
<TableHead className="text-right">{t('common.actions')}</TableHead>
|
||||||
|
|
@ -266,18 +214,13 @@ export function PrescriptionsPage() {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-24 text-center text-muted-foreground">
|
<TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
|
||||||
{t('common.noResults')}
|
{t('common.noResults')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : null}
|
) : null}
|
||||||
{filtered.map((p) => {
|
{filtered.map((p) => {
|
||||||
const actions = rowActions(p)
|
const actions = rowActions(p)
|
||||||
const count = openFlagCount(p)
|
|
||||||
const severity = highestOpenSeverity(p)
|
|
||||||
const openCodes = [
|
|
||||||
...new Set((p.flags ?? []).filter((f) => f.status === 'open').map((f) => f.code)),
|
|
||||||
]
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={p.id}>
|
<TableRow key={p.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
|
|
@ -296,26 +239,6 @@ export function PrescriptionsPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
{count > 0 && severity ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
|
||||||
<Badge variant={severityBadgeVariant(severity)} className="gap-1">
|
|
||||||
<Flag className="h-3 w-3" />
|
|
||||||
{count} open
|
|
||||||
</Badge>
|
|
||||||
{openCodes.slice(0, 2).map((code) => (
|
|
||||||
<Badge key={code} variant="outline" className="font-normal">
|
|
||||||
{flagCodeLabel(code)}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{openCodes.length > 2 ? (
|
|
||||||
<span className="text-[11px] text-muted-foreground">+{openCodes.length - 2}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-muted-foreground">—</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{branches.find((b) => b.id === p.branchId)?.name ?? p.branchId}</TableCell>
|
<TableCell>{branches.find((b) => b.id === p.branchId)?.name ?? p.branchId}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusBadge domain="prescription" status={p.status} />
|
<StatusBadge domain="prescription" status={p.status} />
|
||||||
|
|
|
||||||
|
|
@ -77,37 +77,6 @@ export interface PrescriptionItem {
|
||||||
times?: string[]
|
times?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pharmacist / staff operational review flags on a prescription
|
|
||||||
* (distinct from line-item `controlled` and Rx workflow `status`).
|
|
||||||
*/
|
|
||||||
export type PrescriptionFlagCode =
|
|
||||||
| 'controlled_substance'
|
|
||||||
| 'interaction_concern'
|
|
||||||
| 'incomplete_rx'
|
|
||||||
| 'fraud_suspicion'
|
|
||||||
| 'stock_shortage'
|
|
||||||
| 'needs_clarification'
|
|
||||||
| 'allergy_concern'
|
|
||||||
| 'dosing_concern'
|
|
||||||
| 'other'
|
|
||||||
|
|
||||||
export type PrescriptionFlagSeverity = 'info' | 'warning' | 'critical'
|
|
||||||
export type PrescriptionFlagStatus = 'open' | 'resolved'
|
|
||||||
|
|
||||||
export interface PrescriptionFlag {
|
|
||||||
id: string
|
|
||||||
code: PrescriptionFlagCode
|
|
||||||
severity: PrescriptionFlagSeverity
|
|
||||||
note: string
|
|
||||||
createdBy: string
|
|
||||||
createdAt: string
|
|
||||||
status: PrescriptionFlagStatus
|
|
||||||
resolvedBy?: string
|
|
||||||
resolvedAt?: string
|
|
||||||
resolutionNote?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Prescription {
|
export interface Prescription {
|
||||||
id: string
|
id: string
|
||||||
customerName: string
|
customerName: string
|
||||||
|
|
@ -127,8 +96,6 @@ export interface Prescription {
|
||||||
reviewedBy?: string
|
reviewedBy?: string
|
||||||
reviewedAt?: string
|
reviewedAt?: string
|
||||||
decisionNote?: string
|
decisionNote?: string
|
||||||
/** Operational / clinical review flags (open + resolved history). */
|
|
||||||
flags?: PrescriptionFlag[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** End-customer on an order — platform account vs walk-in / phone / guest. */
|
/** End-customer on an order — platform account vs walk-in / phone / guest. */
|
||||||
|
|
|
||||||
Reference in New Issue
Block a user