This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Admin/src/pages/prescriptions/PrescriptionDetailPage.tsx
kirukib e9a8e0ffc2 Add multi-provider staff login and Rx dosing schedules.
Google, phone OTP, and Telegram join email auth; prescription lines carry frequency, intervalHours, and times, with backend spec updated for this batch.

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

366 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { toast } from 'sonner'
import { Check, ClipboardCopy, HelpCircle, X } from 'lucide-react'
import { DetailPageLayout } from '@/layouts/DetailPageLayout'
import { ActivityTimeline, DetailSection } from '@/components/detail/ActivityTimeline'
import { ApprovalStepper, type ApprovalStep } from '@/components/detail/ApprovalStepper'
import { KeyValueGrid } from '@/components/detail/KeyValueGrid'
import { RowActions, type RowAction } from '@/components/table/RowActions'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { StatusBadge, statusLabel } from '@/components/ui/status-badge'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { branches, doctors, prescriptions } from '@/mocks/data'
import { useAuthStore } from '@/zustand/authStore'
import { formatDoseTimes, formatFrequencyLabel } from '@/lib/dosingSchedule'
import { formatDateTime } from '@/lib/utils'
import type { Prescription } from '@/types'
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
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 reviewSeen = decided || rx.status === 'queried'
return [
{
key: 'submitted',
label: 'Submitted',
state: 'done',
actor: rx.customerName,
at: rx.submittedAt,
},
{
key: 'review',
label: 'Pharmacist review',
state: reviewSeen ? 'done' : 'current',
actor: reviewSeen ? rx.reviewedBy : rx.reviewStartedAt ? 'Review in progress' : undefined,
at: rx.reviewStartedAt,
hint: rx.status === 'submitted' ? 'Waiting to be picked up from the queue' : undefined,
},
{
key: 'decision',
label:
rx.status === 'approved'
? 'Approved for dispensing'
: rx.status === 'rejected'
? 'Rejected'
: rx.status === 'queried'
? 'Queried with customer'
: 'Decision',
state:
rx.status === 'approved'
? 'done'
: rx.status === 'rejected'
? 'failed'
: rx.status === 'queried'
? 'current'
: 'upcoming',
actor: rx.status === 'queried' || rx.status === 'approved' || rx.status === 'rejected' ? rx.reviewedBy : undefined,
at: rx.status === 'submitted' || rx.status === 'under_review' ? undefined : rx.reviewedAt,
hint: rx.status === 'queried' ? 'Awaiting a clearer scan from the customer' : undefined,
},
]
}, [rx])
const decisionCluster = (
<div className="flex justify-end gap-2">
{isOpen ? (
<>
<Button
type="button"
variant="ghost"
className="h-10 gap-1.5 text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={() => act('rejected')}
>
<X className="h-4 w-4" />
Reject
</Button>
<Button type="button" variant="outline" className="h-10 gap-1.5" onClick={() => act('queried')}>
<HelpCircle className="h-4 w-4" />
Query
</Button>
<Button type="button" className="h-10 gap-1.5" onClick={() => act('approved')}>
<Check className="h-4 w-4" />
Approve
</Button>
</>
) : (
<Button asChild variant="outline" className="h-10">
<Link to="/prescriptions">Back to the review queue</Link>
</Button>
)}
</div>
)
return (
<DetailPageLayout
backTo="/prescriptions"
backLabel="Back to prescriptions"
title={rx.id}
subtitle={`${rx.customerName} · ${rx.prescriber ?? 'Unknown prescriber'}`}
badges={
<>
<StatusBadge domain="prescription" status={rx.status} />
{controlled ? <Badge variant="danger">Controlled</Badge> : null}
</>
}
actions={
<>
{isOpen ? (
<>
<Button className="h-10 gap-1.5" onClick={() => act('approved')}>
<Check className="h-4 w-4" />
Approve
</Button>
<Button variant="outline" className="h-10 gap-1.5" onClick={() => act('queried')}>
<HelpCircle className="h-4 w-4" />
Query
</Button>
</>
) : null}
<RowActions items={overflow} />
</>
}
sidebar={
<DetailSection title="Review trail">
<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>
}
>
<div className="space-y-5">
<DetailSection
title="Review status"
description="Where this prescription sits in the pharmacist workflow"
>
<ApprovalStepper steps={steps} />
<div
className={
rx.status === 'rejected'
? 'mt-5 rounded-xl border border-destructive/25 bg-destructive/5 px-4 py-3'
: rx.status === 'approved'
? 'mt-5 rounded-xl border border-brand-200 bg-brand-50 px-4 py-3'
: 'mt-5 rounded-xl border border-orange/30 bg-orange/5 px-4 py-3'
}
>
<p className="text-sm font-semibold text-forest">
{rx.status === 'approved'
? 'Approved and ready to dispense'
: rx.status === 'rejected'
? 'Rejected — nothing will be dispensed'
: rx.status === 'queried'
? 'Waiting on the customer'
: rx.status === 'under_review'
? 'Open with a pharmacist'
: 'Waiting for a pharmacist'}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{rx.reviewedBy && rx.reviewedAt
? `${statusLabel('prescription', rx.status)} by ${rx.reviewedBy} · ${formatDateTime(rx.reviewedAt)}`
: `Submitted ${formatDateTime(rx.submittedAt)} · no decision recorded yet`}
</p>
{rx.decisionNote ? (
<p className="mt-2 text-xs text-grayScale-600">{rx.decisionNote}</p>
) : null}
</div>
</DetailSection>
<DetailSection title="Details">
<KeyValueGrid
columns={3}
items={[
{ label: 'Branch', value: branch?.name },
{
label: 'Prescriber',
value: doctor ? (
<Link to={`/doctors/${doctor.id}`} className="text-brand-700 hover:underline">
{doctor.name}
</Link>
) : (
rx.prescriber ?? '—'
),
},
{ label: 'Hospital', value: doctor?.orgName ?? '—' },
{ label: 'License', value: doctor?.licenseNumber ?? '—' },
{ label: 'Submitted', value: formatDateTime(rx.submittedAt) },
{ label: 'Items', value: String(rx.items.length) },
{ label: 'Controlled items', value: controlled ? 'Yes — stamp required' : 'None' },
{ label: 'Reviewed by', value: rx.reviewedBy ?? 'Not reviewed yet' },
]}
/>
</DetailSection>
<DetailSection
title="Medications"
description="Verify strength, qty, dosing schedule, and controlled flags"
>
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Medication</TableHead>
<TableHead>Qty</TableHead>
<TableHead>Interval</TableHead>
<TableHead>Dose times</TableHead>
<TableHead>Flags</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rx.items.map((item) => (
<TableRow key={item.name}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.qty}</TableCell>
<TableCell>
{formatFrequencyLabel(item.frequency, item.intervalHours)}
{item.frequency ? (
<span className="mt-0.5 block font-mono text-[11px] text-muted-foreground">
{item.frequency}
{item.frequency === 'QXH' && item.intervalHours
? ` · ${item.intervalHours}h`
: ''}
</span>
) : null}
</TableCell>
<TableCell className="font-mono text-sm">
{formatDoseTimes(item.times)}
</TableCell>
<TableCell>
{item.controlled ? (
<Badge variant="danger">Controlled</Badge>
) : (
<Badge variant="secondary">OTC/Rx</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="mt-5 flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-muted-foreground">
{isOpen
? controlled
? 'Approving stamps a dispensing record and logs the controlled item against your licence.'
: 'Approving stamps a dispensing record against this branch’s stock.'
: `Decision recorded by ${rx.reviewedBy ?? 'a pharmacist'}.`}
</p>
{decisionCluster}
</div>
</DetailSection>
</div>
</DetailPageLayout>
)
}