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/doctors/DoctorsPage.tsx
kirukib dd471c5909 Add doctor actor, guest orders, and richer catalog/stock Admin flows.
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>
2026-08-07 11:25:45 +03:00

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>
)
}