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>
662 lines
24 KiB
TypeScript
662 lines
24 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
||
import { Link, useParams } from 'react-router-dom'
|
||
import { toast } from 'sonner'
|
||
import { Check, ClipboardCopy, Flag, HelpCircle, Plus, X } from 'lucide-react'
|
||
import { DetailPageLayout } from '@/layouts/DetailPageLayout'
|
||
import { ActivityTimeline, DetailSection, type TimelineEvent } 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 {
|
||
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { branches, doctors, prescriptions } from '@/mocks/data'
|
||
import { useAuthStore } from '@/zustand/authStore'
|
||
import { formatDoseTimes, formatFrequencyLabel } from '@/lib/dosingSchedule'
|
||
import {
|
||
PRESCRIPTION_FLAG_CODES,
|
||
defaultSeverityForFlag,
|
||
flagCodeLabel,
|
||
openFlagCount,
|
||
severityBadgeVariant,
|
||
sortFlags,
|
||
} from '@/lib/prescriptionFlags'
|
||
import { formatDateTime } from '@/lib/utils'
|
||
import type {
|
||
Prescription,
|
||
PrescriptionFlag,
|
||
PrescriptionFlagCode,
|
||
PrescriptionFlagSeverity,
|
||
} from '@/types'
|
||
|
||
const OPEN_STATUSES: Prescription['status'][] = ['submitted', 'under_review', 'queried']
|
||
|
||
function buildSteps(rx: Prescription): 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,
|
||
},
|
||
]
|
||
}
|
||
|
||
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 = (
|
||
<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>
|
||
)
|
||
|
||
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 (
|
||
<>
|
||
<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}
|
||
{openCount > 0 ? (
|
||
<Badge variant="warning" className="gap-1">
|
||
<Flag className="h-3 w-3" />
|
||
{openCount} open flag{openCount === 1 ? '' : 's'}
|
||
</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={trailEvents} />
|
||
</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="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">
|
||
<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: 'Open flags', value: String(openCount) },
|
||
{ 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>
|
||
|
||
<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>
|
||
</>
|
||
)
|
||
}
|