Wire hospital-affiliated doctors and unauthenticated customer checkout into mocks, UI, and the backend spec so clinical and walk-in paths match the product model. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
6.8 KiB
TypeScript
179 lines
6.8 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { Link, useSearchParams } from 'react-router-dom'
|
|
import { Eye, Plus, Stethoscope, UserCheck, UserX, Building2 } from 'lucide-react'
|
|
import { PageShell } from '@/layouts/PageShell'
|
|
import { StatCard } from '@/components/dashboard/StatCard'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { StatusBadge } from '@/components/ui/status-badge'
|
|
import { PersonAvatar } from '@/components/ui/avatar'
|
|
import { TableToolbar, type TableFilter } from '@/components/table/TableToolbar'
|
|
import { RowActions } from '@/components/table/RowActions'
|
|
import { doctors as seed, organisations } from '@/mocks/data'
|
|
import type { Doctor } from '@/types'
|
|
|
|
const HOSPITAL_TYPES = new Set(['hospital', 'clinic'])
|
|
|
|
export function DoctorsPage() {
|
|
const [params] = useSearchParams()
|
|
const orgFilterParam = params.get('orgId') ?? 'all'
|
|
const [rows] = useState<Doctor[]>([...seed])
|
|
const [q, setQ] = useState('')
|
|
const [status, setStatus] = useState('all')
|
|
const [orgId, setOrgId] = useState(orgFilterParam)
|
|
|
|
const hospitalOrgs = useMemo(
|
|
() => organisations.filter((o) => HOSPITAL_TYPES.has(o.orgType) && o.status !== 'rejected'),
|
|
[],
|
|
)
|
|
|
|
const kpis = useMemo(() => {
|
|
const scoped = orgId === 'all' ? rows : rows.filter((d) => d.orgId === orgId)
|
|
return {
|
|
total: scoped.length,
|
|
active: scoped.filter((d) => d.status === 'active').length,
|
|
inactive: scoped.filter((d) => d.status === 'inactive').length,
|
|
linked: scoped.filter((d) => d.hasLinkedAccount).length,
|
|
hospitals: hospitalOrgs.length,
|
|
}
|
|
}, [rows, orgId, hospitalOrgs.length])
|
|
|
|
const filtered = useMemo(() => {
|
|
return rows.filter((d) => {
|
|
if (orgId !== 'all' && d.orgId !== orgId) return false
|
|
if (status !== 'all' && d.status !== status) return false
|
|
if (q) {
|
|
const needle = q.toLowerCase()
|
|
if (
|
|
!d.name.toLowerCase().includes(needle) &&
|
|
!d.specialty.toLowerCase().includes(needle) &&
|
|
!d.licenseNumber.toLowerCase().includes(needle) &&
|
|
!d.orgName.toLowerCase().includes(needle) &&
|
|
!d.email.toLowerCase().includes(needle)
|
|
) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}, [rows, q, status, orgId])
|
|
|
|
const filters: TableFilter[] = [
|
|
{
|
|
id: 'status',
|
|
label: 'Status',
|
|
value: status,
|
|
onChange: setStatus,
|
|
options: [
|
|
{ value: 'all', label: 'All statuses' },
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'inactive', label: 'Inactive' },
|
|
],
|
|
},
|
|
{
|
|
id: 'org',
|
|
label: 'Hospital',
|
|
value: orgId,
|
|
onChange: setOrgId,
|
|
options: [
|
|
{ value: 'all', label: 'All hospitals' },
|
|
...hospitalOrgs.map((o) => ({ value: o.id, label: o.name })),
|
|
],
|
|
},
|
|
]
|
|
|
|
const newHref = orgId !== 'all' ? `/doctors/new?orgId=${orgId}` : '/doctors/new'
|
|
|
|
return (
|
|
<PageShell title="Doctors" subtitle="Clinicians affiliated with hospital organisations — invite, review, attribute Rx & orders">
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<StatCard label="Doctors" value={kpis.total} hint={`${kpis.active} active`} icon={Stethoscope} tone="brand" />
|
|
<StatCard label="Active" value={kpis.active} hint="Can create Rx & orders" icon={UserCheck} tone="forest" />
|
|
<StatCard label="Linked accounts" value={kpis.linked} hint="Login provisioned" icon={Building2} tone="muted" />
|
|
<StatCard label="Inactive" value={kpis.inactive} hint="Suspended or left org" icon={UserX} tone="orange" />
|
|
</div>
|
|
|
|
<Card className="overflow-hidden">
|
|
<TableToolbar
|
|
search={q}
|
|
onSearchChange={setQ}
|
|
searchPlaceholder="Search name, specialty, license, hospital…"
|
|
resultCount={filtered.length}
|
|
filters={filters}
|
|
onClearFilters={() => {
|
|
setQ('')
|
|
setStatus('all')
|
|
setOrgId('all')
|
|
}}
|
|
actions={
|
|
<Button asChild className="h-10 gap-2 shadow-none">
|
|
<Link to={newHref}>
|
|
<Plus className="h-4 w-4" />
|
|
Add doctor
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead>Doctor</TableHead>
|
|
<TableHead>Specialty</TableHead>
|
|
<TableHead>Hospital</TableHead>
|
|
<TableHead>License</TableHead>
|
|
<TableHead>Account</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filtered.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
|
|
No doctors match these filters
|
|
</TableCell>
|
|
</TableRow>
|
|
) : null}
|
|
{filtered.map((d) => (
|
|
<TableRow key={d.id}>
|
|
<TableCell className="font-medium">
|
|
<Link to={`/doctors/${d.id}`} className="flex items-center gap-2.5 text-brand-700 hover:underline">
|
|
<PersonAvatar name={d.name} avatarUrl={d.avatarUrl} size="sm" />
|
|
{d.name}
|
|
</Link>
|
|
</TableCell>
|
|
<TableCell>{d.specialty}</TableCell>
|
|
<TableCell>
|
|
<Link to={`/organisations/${d.orgId}`} className="text-brand-700 hover:underline">
|
|
{d.orgName}
|
|
</Link>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-xs">{d.licenseNumber}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={d.hasLinkedAccount ? 'success' : 'outline'}>
|
|
{d.hasLinkedAccount ? 'Linked' : 'Invite pending'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<StatusBadge domain="doctor" status={d.status} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<RowActions
|
|
primary={[
|
|
{ label: 'View', icon: <Eye className="h-3.5 w-3.5" />, to: `/doctors/${d.id}` },
|
|
]}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</PageShell>
|
|
)
|
|
}
|