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/pos/QuickPurchasePanel.tsx
kirukib fce0149b1a Ship full Admin console polish and update backend spec.
Add medication Items catalogue, counter POS/QR purchase, profile/login,
detail pages, loyalty ledger, Cmd+K nav, KPI strips/charts, avatars, and
i18n; document the new APIs and open questions for the shared backend team.

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

874 lines
35 KiB
TypeScript

/**
* Scan to purchase — the counter flow.
*
* Scan a customer's loyalty QR, confirm it is the right person, add what they
* are buying, clear any dispensing gate, confirm. Confirming writes a real
* order, a loyalty award and a stock movement into the mocks, then offers the
* receipt and a reset so the counter loops onto the next customer.
*/
import { useEffect, useMemo, useState, type FormEvent } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'sonner'
import {
AlertTriangle,
BadgeCheck,
CheckCircle2,
FileHeart,
Minus,
Package,
Plus,
Printer,
Receipt,
RotateCcw,
ScanLine,
ShieldAlert,
Trash2,
UserPlus,
UserRound,
X,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { SelectNative } from '@/components/ui/select-native'
import { Separator } from '@/components/ui/separator'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { EmptyState } from '@/components/ui/empty-state'
import { IconTile } from '@/components/ui/icon-tile'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { DetailSection } from '@/components/detail/ActivityTimeline'
import { useAuthStore } from '@/zustand/authStore'
import { formatDateTime, formatEtb, formatRelative } from '@/lib/utils'
import {
auditLog,
branches,
counterScanHistory,
customers,
type CounterScan,
type Customer,
type PurchaseTicket,
type ScanSource,
} from '@/mocks/data'
import type { Order } from '@/types'
import { QrImage } from '@/pages/pos/QrImage'
import { ScanPad } from '@/pages/pos/ScanPad'
import { commitPurchase, shortBranchName } from '@/pages/pos/commitPurchase'
import {
blockingIssues,
cartTotal,
customerQrPayload,
findStock,
gateFor,
isShort,
lineTotal,
pointsForPurchase,
resolveScannedCode,
searchBranchStock,
type CartLine,
} from '@/pages/pos/posEngine'
function tierVariant(tier: string) {
if (tier === 'Gold') return 'warning' as const
if (tier === 'Silver') return 'secondary' as const
return 'muted' as const
}
type CounterReceipt = {
order: Order
lines: CartLine[]
points: number
customer: Customer
}
export function QuickPurchasePanel({
handoff,
onHandoffConsumed,
}: {
/** A ticket sent over from the queue for an operator to clear and confirm. */
handoff?: PurchaseTicket | null
onHandoffConsumed?: () => void
}) {
const user = useAuthStore((s) => s.user)
const role = user?.role
const [branchId, setBranchId] = useState(user?.branchId ?? branches[0]?.id ?? '')
const [customer, setCustomer] = useState<Customer | null>(null)
const [unknownCode, setUnknownCode] = useState<string | null>(null)
const [scanSource, setScanSource] = useState<ScanSource>('manual')
const [lines, setLines] = useState<CartLine[]>([])
const [query, setQuery] = useState('')
const [receipt, setReceipt] = useState<CounterReceipt | null>(null)
const [overrideSku, setOverrideSku] = useState<string | null>(null)
const [overrideReason, setOverrideReason] = useState('')
const [createOpen, setCreateOpen] = useState(false)
const [draft, setDraft] = useState({ name: '', phone: '' })
const [history, setHistory] = useState<CounterScan[]>(() => counterScanHistory.slice(0, 6))
const suggestions = useMemo(() => searchBranchStock(branchId, query), [branchId, query])
const total = cartTotal(lines)
const points = customer ? pointsForPurchase(total, customer.tier) : 0
const issues = customer ? blockingIssues(lines, customer.name, role) : []
/** Ticket-queue accept lands here with a pre-filled customer + basket. */
useEffect(() => {
if (!handoff) return
const match =
(handoff.customerId ? customers.find((c) => c.id === handoff.customerId) : undefined) ??
(handoff.phone ? customers.find((c) => c.phone === handoff.phone) : undefined) ??
null
setBranchId(handoff.branchId || branchId)
setReceipt(null)
setUnknownCode(null)
setScanSource('manual')
if (match) {
setCustomer(match)
} else {
setCustomer(null)
setUnknownCode(handoff.phone ?? handoff.externalRef)
}
setLines(
handoff.lines.map((line) => {
const stock = findStock(handoff.branchId || branchId, line.sku)
return {
sku: line.sku,
name: line.name,
qty: line.qty,
unitEtb: line.unitEtb,
available: stock?.qty ?? 0,
}
}),
)
onHandoffConsumed?.()
toast.message(`Ticket ${handoff.externalRef} loaded`, {
description: match
? `${match.name} · confirm the basket`
: 'Resolve the customer, then confirm the basket',
})
// eslint-disable-next-line react-hooks/exhaustive-deps -- consume each handoff once when identity changes
}, [handoff?.id])
const logScan = (entry: CounterScan) => {
counterScanHistory.unshift(entry)
setHistory((prev) => [entry, ...prev].slice(0, 6))
}
const onCode = (code: string, source: ScanSource) => {
const { customer: match } = resolveScannedCode(code)
setScanSource(source)
if (!match) {
setUnknownCode(code)
setCustomer(null)
logScan({
id: `scn-${Date.now()}`,
at: new Date().toISOString(),
code,
customerName: 'Not recognised',
source,
outcome: 'unknown',
branchId,
staff: user?.name ?? 'Counter staff',
})
return
}
setUnknownCode(null)
setCustomer(match)
setLines([])
setReceipt(null)
toast.success(`${match.name} identified`, { description: `${match.tier} tier · ${match.points} points` })
}
const resetCounter = () => {
setCustomer(null)
setUnknownCode(null)
setLines([])
setQuery('')
setReceipt(null)
}
const addLine = (sku: string) => {
const row = findStock(branchId, sku)
if (!row) return
setQuery('')
setLines((prev) => {
const existing = prev.find((line) => line.sku === sku)
if (existing) {
return prev.map((line) => (line.sku === sku ? { ...line, qty: line.qty + 1 } : line))
}
return [
...prev,
{ sku: row.sku, name: row.name, qty: 1, unitEtb: row.unitEtb, available: row.qty },
]
})
}
const setQty = (sku: string, qty: number) => {
setLines((prev) =>
prev.map((line) => (line.sku === sku ? { ...line, qty: Math.max(1, qty) } : line)),
)
}
const removeLine = (sku: string) => setLines((prev) => prev.filter((line) => line.sku !== sku))
const saveOverride = (event: FormEvent) => {
event.preventDefault()
const reason = overrideReason.trim()
if (!overrideSku || !reason) return
setLines((prev) =>
prev.map((line) => (line.sku === overrideSku ? { ...line, overrideReason: reason } : line)),
)
auditLog.unshift({
id: `a-${Date.now().toString().slice(-4)}`,
at: new Date().toISOString(),
actor: user?.name ?? 'Counter staff',
actorRole: role ?? 'pharmacist',
action: 'pos.emergency_supply',
category: 'prescriptions',
target: overrideSku,
targetLabel: `${customer?.name ?? 'Walk-in'} · ${overrideSku}`,
ip: '196.189.44.12',
userAgent: navigator.userAgent,
outcome: 'success',
security: true,
note: reason,
})
toast.success('Emergency supply recorded', { description: 'Logged to the security review queue' })
setOverrideSku(null)
setOverrideReason('')
}
const createCustomer = (event: FormEvent) => {
event.preventDefault()
const name = draft.name.trim()
const phone = draft.phone.trim()
if (!name || !phone) return
const created: Customer = {
id: `c${customers.length + 1}`,
name,
phone,
tier: 'Bronze',
points: 0,
branch: shortBranchName(branchId),
joinedAt: new Date().toISOString(),
orders: 0,
spendEtb: 0,
lastOrderAt: null,
}
customers.push(created)
setCustomer(created)
setUnknownCode(null)
setLines([])
setCreateOpen(false)
setDraft({ name: '', phone: '' })
toast.success(`${name} enrolled`, { description: 'A loyalty QR code is ready in Customer codes' })
}
const confirmPurchase = () => {
if (!customer || lines.length === 0 || issues.length > 0) return
const { order, points: awarded, scan } = commitPurchase({
customer,
branchId,
lines,
role,
source: scanSource,
staffName: user?.name ?? 'Counter staff',
})
setHistory((prev) => [scan, ...prev].slice(0, 6))
setReceipt({ order, lines, points: awarded, customer })
setLines([])
toast.success(`${order.id} confirmed`, {
description: `${formatEtb(total)} · ${awarded} points awarded to ${customer.name}`,
})
}
// ── Receipt ───────────────────────────────────────────────────────────────
if (receipt) {
return (
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_360px]">
<Card>
<CardHeader className="flex-row items-center gap-3">
<IconTile icon={CheckCircle2} tone="brand" />
<div>
<CardTitle>Purchase confirmed</CardTitle>
<p className="text-sm text-muted-foreground">
{receipt.order.id} · {formatDateTime(receipt.order.createdAt)}
</p>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-xl border bg-cream/60 p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="font-display text-base font-semibold text-forest">{receipt.customer.name}</p>
<p className="text-xs text-muted-foreground">
{receipt.customer.phone} · {shortBranchName(receipt.order.branchId)}
</p>
</div>
<div className="text-right">
<p className="font-display text-xl font-bold text-forest">{formatEtb(receipt.order.totalEtb)}</p>
<p className="text-xs text-brand-700">+{receipt.points} loyalty points</p>
</div>
</div>
</div>
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>SKU</TableHead>
<TableHead>Item</TableHead>
<TableHead>Qty</TableHead>
<TableHead className="text-right">Line total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{receipt.lines.map((line) => (
<TableRow key={line.sku}>
<TableCell className="font-medium">{line.sku}</TableCell>
<TableCell>{line.name}</TableCell>
<TableCell>{line.qty}</TableCell>
<TableCell className="text-right">{formatEtb(lineTotal(line))}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex flex-wrap gap-2">
<Button className="h-10 gap-2 shadow-none" onClick={resetCounter}>
<ScanLine className="h-4 w-4" />
Next customer
</Button>
<Button variant="outline" className="h-10 gap-2" onClick={() => window.print()}>
<Printer className="h-4 w-4" />
Print receipt
</Button>
<Button asChild variant="secondary" className="h-10 gap-2">
<Link to={`/orders/${receipt.order.id}`}>
<Receipt className="h-4 w-4" />
Open order
</Link>
</Button>
</div>
</CardContent>
</Card>
<DetailSection title="What was written" description="Every mock this confirmation touched">
<ul className="space-y-2 text-sm">
<li className="flex gap-2">
<BadgeCheck className="mt-0.5 h-4 w-4 shrink-0 text-brand-700" aria-hidden="true" />
<span>
Order <strong>{receipt.order.id}</strong> appended, paid, channel <strong>counter</strong>
</span>
</li>
<li className="flex gap-2">
<BadgeCheck className="mt-0.5 h-4 w-4 shrink-0 text-brand-700" aria-hidden="true" />
<span>
Loyalty ledger credited <strong>+{receipt.points}</strong> points ({receipt.customer.tier} multiplier
applied)
</span>
</li>
<li className="flex gap-2">
<BadgeCheck className="mt-0.5 h-4 w-4 shrink-0 text-brand-700" aria-hidden="true" />
<span>
Stock decremented at {shortBranchName(receipt.order.branchId)} with a sale movement per line
</span>
</li>
<li className="flex gap-2">
<BadgeCheck className="mt-0.5 h-4 w-4 shrink-0 text-brand-700" aria-hidden="true" />
<span>Scan logged to the counter history below</span>
</li>
</ul>
</DetailSection>
</div>
)
}
// ── Scan / basket ─────────────────────────────────────────────────────────
return (
<div className="grid gap-4 xl:grid-cols-[minmax(0,420px)_minmax(0,1fr)]">
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Identify the customer</CardTitle>
<p className="text-sm text-muted-foreground">
Scan their loyalty QR. No camera? Type the code or phone number, or simulate a scan.
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="pos-branch">Selling branch</Label>
<SelectNative
id="pos-branch"
value={branchId}
onChange={(event) => {
setBranchId(event.target.value)
setLines([])
}}
disabled={role === 'pharmacist' && !!user?.branchId}
>
{branches.map((branch) => (
<option key={branch.id} value={branch.id}>
{branch.name}
</option>
))}
</SelectNative>
</div>
<Separator />
{customer ? (
<div className="space-y-3">
<div className="flex items-start gap-3 rounded-xl border border-brand-200 bg-brand-50 p-4">
<IconTile icon={UserRound} tone="brand" />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<p className="font-display text-base font-semibold text-forest">{customer.name}</p>
<Badge variant={tierVariant(customer.tier)}>{customer.tier}</Badge>
</div>
<p className="mt-0.5 text-xs text-muted-foreground">
{customer.phone} · {customer.id} · Home branch {customer.branch}
</p>
<dl className="mt-3 grid grid-cols-2 gap-2 text-xs">
<div>
<dt className="text-muted-foreground">Points balance</dt>
<dd className="font-semibold text-forest">{customer.points.toLocaleString()}</dd>
</div>
<div>
<dt className="text-muted-foreground">Last order</dt>
<dd className="font-semibold text-forest">
{customer.lastOrderAt ? formatRelative(customer.lastOrderAt) : 'First visit'}
</dd>
</div>
</dl>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={resetCounter}
aria-label="Clear customer and scan again"
>
<X className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
Resolved from a {scanSource} scan. Confirm the name and phone with the customer before taking payment.
</p>
</div>
) : unknownCode ? (
<div className="rounded-xl border border-orange/40 bg-orange/5 p-4">
<div className="flex items-start gap-3">
<IconTile icon={AlertTriangle} tone="orange" size="sm" />
<div className="min-w-0">
<p className="text-sm font-semibold text-forest">Code not recognised</p>
<p className="mt-0.5 break-all text-xs text-muted-foreground">
<code>{unknownCode}</code> matches no customer record.
</p>
<div className="mt-3 flex flex-wrap gap-2">
<Button
className="h-9 gap-1.5 px-3 text-xs shadow-none"
onClick={() => {
setDraft({
name: '',
phone: /^\+?\d[\d\s-]{7,}$/.test(unknownCode) ? unknownCode : '',
})
setCreateOpen(true)
}}
>
<UserPlus className="h-3.5 w-3.5" />
Enrol this customer
</Button>
<Button
variant="outline"
className="h-9 px-3 text-xs"
onClick={() => setUnknownCode(null)}
>
Scan again
</Button>
</div>
</div>
</div>
</div>
) : (
<ScanPad
onCode={onCode}
manualLabel="Keypad entry"
manualPlaceholder="c1, GISHEN-CUST:c1 or +2519…"
manualHint="Accepts the printed code, a loyalty URL, a customer id or a phone number."
simulateOptions={customers.slice(0, 4).map((c) => ({
label: c.name.split(' ')[0],
hint: `${c.tier} · ${c.points} points`,
value: customerQrPayload(c.id),
}))}
simulateHint="No camera needed — picks a demo customer as if their code had been read."
/>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Recent counter scans</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{history.map((entry) => (
<div key={entry.id} className="flex items-center justify-between gap-3 rounded-xl border p-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium text-forest">{entry.customerName}</p>
<p className="text-xs text-muted-foreground">
{formatRelative(entry.at)} · {entry.source}
{entry.orderId ? ` · ${entry.orderId}` : ''}
</p>
</div>
<Badge variant={entry.outcome === 'resolved' ? 'success' : 'warning'}>{entry.outcome}</Badge>
</div>
))}
</CardContent>
</Card>
</div>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>What are they buying?</CardTitle>
<p className="text-sm text-muted-foreground">
Search the {shortBranchName(branchId)} shelf by SKU or name.
</p>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="pos-sku">Add item</Label>
<Input
id="pos-sku"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="AMX-500, paracetamol…"
disabled={!customer}
autoComplete="off"
/>
</div>
{suggestions.length > 0 ? (
<ul className="space-y-1.5">
{suggestions.map((row) => (
<li key={`${row.sku}-${row.branchId}`}>
<button
type="button"
className="flex w-full items-center justify-between gap-3 rounded-xl border px-3 py-2 text-left transition-colors hover:bg-grayScale-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
onClick={() => addLine(row.sku)}
>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-forest">{row.name}</span>
<span className="block text-xs text-muted-foreground">
{row.sku} · {row.qty} on shelf · {formatEtb(row.unitEtb)}
</span>
</span>
<Plus className="h-4 w-4 shrink-0 text-brand-700" aria-hidden="true" />
</button>
</li>
))}
</ul>
) : null}
{lines.length === 0 ? (
<EmptyState
icon={Package}
variant="dashed"
title={customer ? 'Basket is empty' : 'Waiting for a scan'}
description={
customer
? 'Search above to add what the customer is buying.'
: 'Identify the customer first — the basket unlocks once a code resolves.'
}
/>
) : (
<div className="space-y-3">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Item</TableHead>
<TableHead>Qty</TableHead>
<TableHead>Unit</TableHead>
<TableHead>Line</TableHead>
<TableHead className="text-right">Remove</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{lines.map((line) => {
const gate = customer
? gateFor({
sku: line.sku,
productName: line.name,
customerName: customer.name,
role,
})
: null
const short = isShort(line)
return (
<TableRow key={line.sku} className="align-top">
<TableCell>
<p className="font-medium text-forest">{line.name}</p>
<p className="text-xs text-muted-foreground">
{line.sku} · {line.available} on shelf
</p>
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
{gate && gate.dispenseClass !== 'otc' ? (
<Badge
variant={
gate.state === 'rx_linked'
? 'success'
: gate.state === 'blocked'
? 'danger'
: 'warning'
}
>
{gate.dispenseClass === 'controlled' ? 'Controlled' : 'Rx only'}
</Badge>
) : null}
{short ? <Badge variant="danger">Insufficient stock</Badge> : null}
{line.overrideReason ? <Badge variant="warning">Emergency supply</Badge> : null}
</div>
{gate && gate.state !== 'clear' && !line.overrideReason ? (
<p
className={
gate.state === 'blocked'
? 'mt-1.5 text-xs text-red-700'
: 'mt-1.5 text-xs text-muted-foreground'
}
>
{gate.message}
</p>
) : null}
{short ? (
<p className="mt-1 text-xs text-red-700">
Only {line.available} available — reduce the quantity or transfer stock in.
</p>
) : null}
{gate?.state === 'needs_rx' && gate.overridable && !line.overrideReason ? (
<div className="mt-2 flex flex-wrap gap-2">
<Button
variant="outline"
className="h-8 gap-1.5 px-2.5 text-xs"
onClick={() => {
setOverrideSku(line.sku)
setOverrideReason('')
}}
>
<ShieldAlert className="h-3.5 w-3.5" />
Record emergency supply
</Button>
<Button asChild variant="ghost" className="h-8 gap-1.5 px-2.5 text-xs">
<Link to="/prescriptions/new">
<FileHeart className="h-3.5 w-3.5" />
Log an Rx
</Link>
</Button>
</div>
) : null}
{line.overrideReason ? (
<p className="mt-1.5 text-xs text-orange-deep">
Emergency supply: {line.overrideReason}
</p>
) : null}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setQty(line.sku, line.qty - 1)}
aria-label={`Decrease ${line.sku}`}
>
<Minus className="h-3.5 w-3.5" />
</Button>
<Input
className="h-8 w-14 text-center"
type="number"
min={1}
value={line.qty}
onChange={(event) => setQty(line.sku, Number(event.target.value))}
aria-label={`Quantity for ${line.sku}`}
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setQty(line.sku, line.qty + 1)}
aria-label={`Increase ${line.sku}`}
>
<Plus className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
<TableCell className="whitespace-nowrap">{formatEtb(line.unitEtb)}</TableCell>
<TableCell className="whitespace-nowrap font-medium">
{formatEtb(lineTotal(line))}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => removeLine(line.sku)}
aria-label={`Remove ${line.sku}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
<Card>
<CardContent className="space-y-4 p-5">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Total due</p>
<p className="font-display text-3xl font-bold text-forest">{formatEtb(total)}</p>
{customer ? (
<p className="mt-1 text-xs text-brand-700">
Earns {points} points · {customer.tier} multiplier on 1 point per 100 ETB
</p>
) : null}
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
className="h-11 gap-2"
onClick={() => setLines([])}
disabled={lines.length === 0}
>
<RotateCcw className="h-4 w-4" />
Clear basket
</Button>
<Button
className="h-11 gap-2 shadow-none"
onClick={confirmPurchase}
disabled={!customer || lines.length === 0 || issues.length > 0}
>
<CheckCircle2 className="h-4 w-4" />
Confirm purchase
</Button>
</div>
</div>
{issues.length > 0 ? (
<div className="rounded-xl border border-red-200 bg-red-50 p-3">
<p className="flex items-center gap-1.5 text-sm font-semibold text-red-700">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
{issues.length} thing{issues.length === 1 ? '' : 's'} to resolve before confirming
</p>
<ul className="mt-1.5 space-y-1 text-xs text-red-700">
{issues.map((issue) => (
<li key={`${issue.sku}-${issue.label}`}>{issue.label}</li>
))}
</ul>
</div>
) : null}
</CardContent>
</Card>
</div>
<Dialog
open={!!overrideSku}
onOpenChange={(open) => {
if (!open) setOverrideSku(null)
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Record an emergency supply</DialogTitle>
<DialogDescription>
Prescription-only item with no approved Rx on file. Your name, the reason and the SKU go to the security
review queue in the audit log.
</DialogDescription>
</DialogHeader>
<form className="space-y-3" onSubmit={saveOverride}>
<div className="space-y-1.5">
<Label htmlFor="pos-override">Clinical reason</Label>
<Input
id="pos-override"
value={overrideReason}
onChange={(event) => setOverrideReason(event.target.value)}
placeholder="Repeat inhaler, prescriber contacted by phone…"
autoFocus
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setOverrideSku(null)}>
Cancel
</Button>
<Button type="submit" disabled={!overrideReason.trim()}>
Record and continue
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Enrol a new customer</DialogTitle>
<DialogDescription>
Creates a loyalty record on the spot and generates a QR code this tool can scan next time.
</DialogDescription>
</DialogHeader>
<form className="space-y-3" onSubmit={createCustomer}>
<div className="space-y-1.5">
<Label htmlFor="pos-new-name">Full name</Label>
<Input
id="pos-new-name"
value={draft.name}
onChange={(event) => setDraft({ ...draft, name: event.target.value })}
autoFocus
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="pos-new-phone">Phone</Label>
<Input
id="pos-new-phone"
value={draft.phone}
onChange={(event) => setDraft({ ...draft, phone: event.target.value })}
placeholder="+2519…"
/>
</div>
<div className="flex items-center gap-3 rounded-xl border bg-cream/60 p-3">
<QrImage value={customerQrPayload(`c${customers.length + 1}`)} size={72} alt="Preview of the new loyalty code" />
<p className="text-xs text-muted-foreground">
Their code will be <code>{customerQrPayload(`c${customers.length + 1}`)}</code> — printable from the
Customer codes tab.
</p>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={!draft.name.trim() || !draft.phone.trim()}>
Enrol and continue
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}