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>
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
/**
|
|
* Printable loyalty QR codes — the payloads the counter scanner actually reads.
|
|
*/
|
|
import { useMemo, useState } from 'react'
|
|
import { Printer } 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 { PersonAvatar } from '@/components/ui/avatar'
|
|
import { customers } from '@/mocks/data'
|
|
import { customerQrPayload } from '@/pages/pos/posEngine'
|
|
import { QrImage } from '@/pages/pos/QrImage'
|
|
import { useAuthStore } from '@/zustand/authStore'
|
|
import { branches } from '@/mocks/data'
|
|
|
|
export function CustomerCodesPanel() {
|
|
const user = useAuthStore((s) => s.user)
|
|
const [q, setQ] = useState('')
|
|
|
|
const scopedZone = user?.branchId ? branches.find((b) => b.id === user.branchId)?.zone : undefined
|
|
|
|
const list = useMemo(() => {
|
|
let rows = scopedZone ? customers.filter((c) => c.branch === scopedZone) : customers
|
|
if (q) {
|
|
const needle = q.toLowerCase()
|
|
rows = rows.filter(
|
|
(c) =>
|
|
c.name.toLowerCase().includes(needle) ||
|
|
c.phone.includes(needle) ||
|
|
c.id.toLowerCase().includes(needle),
|
|
)
|
|
}
|
|
return rows
|
|
}, [scopedZone, q])
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-sm text-muted-foreground">
|
|
Each code encodes <code className="rounded bg-muted px-1">GISHEN-CUST:<id></code>. Scan it
|
|
on Quick purchase, or type the code when the camera is unavailable.
|
|
</p>
|
|
<Input
|
|
className="h-10 max-w-sm"
|
|
placeholder="Search customer…"
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
{list.map((customer) => {
|
|
const payload = customerQrPayload(customer.id)
|
|
return (
|
|
<Card key={customer.id}>
|
|
<CardHeader className="flex-row items-center gap-3 space-y-0 pb-2">
|
|
<PersonAvatar name={customer.name} avatarUrl={customer.avatarUrl} size="md" />
|
|
<div className="min-w-0 flex-1">
|
|
<CardTitle className="truncate text-base">{customer.name}</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{customer.phone}</p>
|
|
</div>
|
|
<Badge variant="secondary">{customer.tier}</Badge>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col items-center gap-3 pt-2">
|
|
<QrImage value={payload} size={160} alt={`Loyalty QR for ${customer.name}`} />
|
|
<code className="break-all text-center text-[11px] text-muted-foreground">{payload}</code>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 gap-1.5"
|
|
onClick={() => window.print()}
|
|
>
|
|
<Printer className="h-3.5 w-3.5" />
|
|
Print
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|