3 Commits

Author SHA1 Message Date
bdabcb2ea4 feat: consistent UI across all list pages — PhoneActionCell, custom pills, eye icon
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- PhoneActionCell: kebab always visible (SMS + WhatsApp), Call removed from menu,
  phone number always brand-colored regardless of telephony state
- LeadTable: replaced actions kebab column with eye icon (first column) for
  view activity, phone column now uses PhoneActionCell
- Worklist: React Aria Tabs replaced with custom pill buttons matching All Leads
  pattern (bg-brand-solid on selected), search lifted to call-desk.tsx header
- Appointments: underline tabs replaced with custom pills, phone in patient cell
  uses PhoneActionCell, group/row added to rows
- Patients: removed redundant HamburgerMenu column, group/row on rows
- Call Desk: search input in header row, cleaned up duplicate imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:05:32 +05:30
313842a922 feat: info icon on all PageHeader pages + Call Desk header restyled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:49:00 +05:30
dfcaa175ab feat: PageHeader component + refactor all 6 list pages
New reusable PageHeader component (src/components/layout/page-header.tsx)
with consistent layout: title + badge + subtitle on left, controls on
right, optional tabs below with no extra borders.

Refactored pages:
 - All Leads: inline header → PageHeader
 - Contacts: inline header → PageHeader
 - Appointments v2: inline header → PageHeader with tabs
 - Call History: removed p-7 wrapper + TableCard.Root → flat table
 - Patients: removed p-7 wrapper + TableCard.Root → flat table
 - Missed Calls: removed TopBar → PageHeader with tabs

All pages now share identical header spacing, font sizing, and
control alignment. No more double borders from tab + container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:31:30 +05:30
11 changed files with 388 additions and 323 deletions

View File

@@ -74,43 +74,33 @@ export const PhoneActionCell = ({ phoneNumber, displayNumber, leadId: _leadId, o
{/* Clickable phone number — calls directly */} {/* Clickable phone number — calls directly */}
<button <button
type="button" type="button"
onClick={handleCall} onClick={canCall ? handleCall : undefined}
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd} onTouchEnd={onTouchEnd}
onContextMenu={(e) => { e.preventDefault(); setMenuOpen(true); }} onContextMenu={(e) => { e.preventDefault(); setMenuOpen(true); }}
disabled={!canCall}
className={cx( className={cx(
'flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm transition duration-100 ease-linear', 'flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm text-brand-secondary transition duration-100 ease-linear',
canCall canCall
? 'cursor-pointer text-brand-secondary hover:bg-brand-primary hover:text-brand-secondary' ? 'cursor-pointer hover:bg-brand-primary'
: 'cursor-default text-tertiary', : 'cursor-default',
)} )}
> >
<FontAwesomeIcon icon={faPhone} className="size-3" /> <FontAwesomeIcon icon={faPhone} className="size-3" />
<span className="whitespace-nowrap">{displayNumber}</span> <span className="whitespace-nowrap">{displayNumber}</span>
</button> </button>
{/* Kebab menu trigger — desktop */} {/* Kebab menu trigger — SMS + WhatsApp */}
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); setMenuOpen(!menuOpen); }} onClick={(e) => { e.stopPropagation(); setMenuOpen(!menuOpen); }}
className="flex size-6 items-center justify-center rounded-md text-fg-quaternary opacity-0 group-hover/row:opacity-100 hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear" className="flex size-6 items-center justify-center rounded-md text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
> >
<FontAwesomeIcon icon={faEllipsisVertical} className="size-3" /> <FontAwesomeIcon icon={faEllipsisVertical} className="size-3" />
</button> </button>
{/* Context menu */} {/* Context menu — SMS + WhatsApp only (dial is the primary click) */}
{menuOpen && ( {menuOpen && (
<div className="absolute left-0 top-full z-50 mt-1 w-40 rounded-lg bg-primary shadow-lg ring-1 ring-secondary py-1"> <div className="absolute left-0 top-full z-50 mt-1 w-40 rounded-lg bg-primary shadow-lg ring-1 ring-secondary py-1">
<button
type="button"
onClick={handleCall}
disabled={!canCall}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-secondary hover:bg-primary_hover disabled:text-disabled"
>
<FontAwesomeIcon icon={faPhone} className="size-3.5 text-fg-success-secondary" />
Call
</button>
<button <button
type="button" type="button"
onClick={handleSms} onClick={handleSms}

View File

@@ -1,13 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { faPhoneArrowDown, faPhoneArrowUp, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; import { faPhoneArrowDown, faPhoneArrowUp } from '@fortawesome/pro-duotone-svg-icons';
import type { SortDescriptor } from 'react-aria-components'; import type { SortDescriptor } from 'react-aria-components';
import { faIcon } from '@/lib/icon-wrapper'; import { faIcon } from '@/lib/icon-wrapper';
import { Table } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
const SearchLg = faIcon(faMagnifyingGlass);
import { Badge } from '@/components/base/badges/badges'; import { Badge } from '@/components/base/badges/badges';
import { Input } from '@/components/base/input/input';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { PhoneActionCell } from './phone-action-cell'; import { PhoneActionCell } from './phone-action-cell';
import { formatPhone, formatTimeOnly, formatShortDate } from '@/lib/format'; import { formatPhone, formatTimeOnly, formatShortDate } from '@/lib/format';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
@@ -79,6 +75,9 @@ interface WorklistPanelProps {
onSelectItem: (selection: WorklistSelection) => void; onSelectItem: (selection: WorklistSelection) => void;
selectedItemId: string | null; selectedItemId: string | null;
onDialMissedCall?: (missedCallId: string) => void; onDialMissedCall?: (missedCallId: string) => void;
// Lifted from internal state — owned by call-desk.tsx so the search
// input can live in the PageHeader row alongside other controls.
search: string;
} }
type TabKey = 'all' | 'missed' | 'leads' | 'follow-ups'; type TabKey = 'all' | 'missed' | 'leads' | 'follow-ups';
@@ -299,9 +298,8 @@ const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], lea
return actionableRows; return actionableRows;
}; };
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectItem, selectedItemId, onDialMissedCall }: WorklistPanelProps) => { export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectItem, selectedItemId, onDialMissedCall, search }: WorklistPanelProps) => {
const [tab, setTab] = useState<TabKey>('all'); const [tab, setTab] = useState<TabKey>('all');
const [search, setSearch] = useState('');
// Missed tab always shows PENDING callbacks. Attempted/Completed/Invalid // Missed tab always shows PENDING callbacks. Attempted/Completed/Invalid
// sub-tabs were removed per QA feedback — pending callbacks are the only // sub-tabs were removed per QA feedback — pending callbacks are the only
// ones agents need to act on from the worklist. // ones agents need to act on from the worklist.
@@ -402,8 +400,10 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
const PAGE_SIZE = 15; const PAGE_SIZE = 15;
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
// Reset page when search changes from parent
useEffect(() => { setPage(1); }, [search]);
const handleTabChange = useCallback((key: TabKey) => { setTab(key); setPage(1); }, []); const handleTabChange = useCallback((key: TabKey) => { setTab(key); setPage(1); }, []);
const handleSearch = useCallback((value: string) => { setSearch(value); setPage(1); }, []);
const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE));
const pagedRows = filteredRows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); const pagedRows = filteredRows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
@@ -436,23 +436,22 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
return ( return (
<div className="flex flex-1 flex-col min-h-0 overflow-hidden"> <div className="flex flex-1 flex-col min-h-0 overflow-hidden">
{/* Filter tabs + search */} {/* Filter pills — custom buttons matching All Leads pattern */}
<div className="flex shrink-0 items-end justify-between border-b border-secondary px-5 pt-3 pb-0.5"> <div className="flex shrink-0 items-center gap-1.5 px-5 py-2">
<Tabs selectedKey={tab} onSelectionChange={(key) => handleTabChange(key as TabKey)}> {tabItems.map((item) => (
<TabList items={tabItems} type="underline" size="sm"> <button
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />} key={item.id}
</TabList> onClick={() => handleTabChange(item.id)}
</Tabs> className={cx(
<div className="w-44 shrink-0"> 'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
<Input tab === item.id
placeholder="Search..." ? 'bg-brand-solid text-white'
icon={SearchLg} : 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
size="sm" )}
value={search} >
onChange={handleSearch} {item.label}{item.badge ? ` (${item.badge})` : ''}
aria-label="Search worklist" </button>
/> ))}
</div>
</div> </div>
{/* Missed-call sub-tabs removed per QA feedback — the Missed tab {/* Missed-call sub-tabs removed per QA feedback — the Missed tab

View File

@@ -0,0 +1,98 @@
// PageHeader — consistent header layout for all list pages.
//
// Row 1: Title (+ optional badge + info icon) on the left,
// controls (search, columns, export, etc.) on the right.
// Row 2: Optional tabs (underline style) — no extra borders.
//
// The `infoText` prop renders as a hoverable info icon (ⓘ) next to
// the title. Long descriptive text goes here instead of inline
// subtitle — keeps the header compact.
//
// Usage:
// <PageHeader
// title="Contacts"
// badge={16}
// infoText="People who reached out directly — phone, walk-in, referral."
// controls={<><Input .../> <Button .../></>}
// tabs={<Tabs ...><TabList ...>{...}</TabList></Tabs>}
// />
import { useState, useRef, useEffect, type ReactNode } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
interface PageHeaderProps {
title: string;
badge?: number | string;
/** Short inline text next to badge — use sparingly (e.g. "17 total") */
subtitle?: string;
/** Longer descriptive text shown on info icon hover/click */
infoText?: string;
controls?: ReactNode;
tabs?: ReactNode;
}
const InfoTooltip = ({ text }: { text: string }) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen(!open)}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
className="flex size-5 items-center justify-center rounded-full text-fg-quaternary hover:text-fg-secondary transition duration-100 ease-linear"
title={text}
>
<FontAwesomeIcon icon={faCircleInfo} className="size-4" />
</button>
{open && (
<div className="absolute left-0 top-full mt-1 z-50 w-72 rounded-lg bg-primary px-3 py-2 text-xs text-tertiary shadow-lg ring-1 ring-secondary">
{text}
</div>
)}
</div>
);
};
export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }: PageHeaderProps) => (
<div className="shrink-0">
{/* Row 1: title + controls */}
<div className="flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-2">
<h1 className="text-lg font-bold text-primary">{title}</h1>
{badge != null && (
<span className="inline-flex items-center justify-center rounded-full bg-brand-secondary px-2 py-0.5 text-xs font-semibold text-white">
{badge}
</span>
)}
{subtitle && (
<span className="text-sm text-tertiary ml-1">{subtitle}</span>
)}
{infoText && <InfoTooltip text={infoText} />}
</div>
{controls && (
<div className="flex items-center gap-2">
{controls}
</div>
)}
</div>
{/* Row 2: optional tabs — no container border, tab underline is the separator */}
{tabs && (
<div className="px-6">
{tabs}
</div>
)}
</div>
);

View File

@@ -1,17 +1,14 @@
import type { FC } from 'react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { TableBody as AriaTableBody } from 'react-aria-components'; import { TableBody as AriaTableBody } from 'react-aria-components';
import type { SortDescriptor, Selection } from 'react-aria-components'; import type { SortDescriptor, Selection } from 'react-aria-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEllipsisVertical } from '@fortawesome/pro-duotone-svg-icons'; import { faEye } from '@fortawesome/pro-duotone-svg-icons';
const DotsVertical: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faEllipsisVertical} className={className} />;
import { Badge } from '@/components/base/badges/badges'; import { Badge } from '@/components/base/badges/badges';
import { Button } from '@/components/base/buttons/button';
import { Table } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
import { LeadStatusBadge } from '@/components/shared/status-badge'; import { LeadStatusBadge } from '@/components/shared/status-badge';
import { SourceTag } from '@/components/shared/source-tag'; import { SourceTag } from '@/components/shared/source-tag';
import { AgeIndicator } from '@/components/shared/age-indicator'; import { AgeIndicator } from '@/components/shared/age-indicator';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { formatPhone, formatShortDate } from '@/lib/format'; import { formatPhone, formatShortDate } from '@/lib/format';
import { cx } from '@/utils/cx'; import { cx } from '@/utils/cx';
import type { Lead } from '@/types/entities'; import type { Lead } from '@/types/entities';
@@ -97,6 +94,7 @@ export const LeadTable = ({
}, [leads, expandedDupId]); }, [leads, expandedDupId]);
const allColumns = [ const allColumns = [
{ id: 'view', label: '', allowsSorting: false, defaultWidth: 40 },
{ id: 'phone', label: 'Phone', allowsSorting: true, defaultWidth: 150 }, { id: 'phone', label: 'Phone', allowsSorting: true, defaultWidth: 150 },
{ id: 'name', label: 'Name', allowsSorting: true, defaultWidth: 160 }, { id: 'name', label: 'Name', allowsSorting: true, defaultWidth: 160 },
{ id: 'email', label: 'Email', allowsSorting: false, defaultWidth: 180 }, { id: 'email', label: 'Email', allowsSorting: false, defaultWidth: 180 },
@@ -109,11 +107,10 @@ export const LeadTable = ({
{ id: 'createdAt', label: 'Age', allowsSorting: true, defaultWidth: 80 }, { id: 'createdAt', label: 'Age', allowsSorting: true, defaultWidth: 80 },
{ id: 'spamScore', label: 'Spam', allowsSorting: true, defaultWidth: 70 }, { id: 'spamScore', label: 'Spam', allowsSorting: true, defaultWidth: 70 },
{ id: 'dups', label: 'Dups', allowsSorting: false, defaultWidth: 60 }, { id: 'dups', label: 'Dups', allowsSorting: false, defaultWidth: 60 },
{ id: 'actions', label: '', allowsSorting: false, defaultWidth: 50 },
]; ];
const columns = visibleColumns const columns = visibleColumns
? allColumns.filter(c => visibleColumns.has(c.id) || c.id === 'actions') ? allColumns.filter(c => visibleColumns.has(c.id) || c.id === 'view')
: allColumns; : allColumns;
return ( return (
@@ -145,6 +142,7 @@ export const LeadTable = ({
const firstName = lead.contactName?.firstName ?? ''; const firstName = lead.contactName?.firstName ?? '';
const lastName = lead.contactName?.lastName ?? ''; const lastName = lead.contactName?.lastName ?? '';
const name = `${firstName} ${lastName}`.trim() || '\u2014'; const name = `${firstName} ${lastName}`.trim() || '\u2014';
const phoneRaw = lead.contactPhone?.[0]?.number ?? '';
const phone = lead.contactPhone?.[0] const phone = lead.contactPhone?.[0]
? formatPhone(lead.contactPhone[0]) ? formatPhone(lead.contactPhone[0])
: '\u2014'; : '\u2014';
@@ -158,6 +156,7 @@ export const LeadTable = ({
id={row.id} id={row.id}
className="bg-warning-primary" className="bg-warning-primary"
> >
<Table.Cell />
<Table.Cell className="pl-10"> <Table.Cell className="pl-10">
<span className="text-xs text-tertiary">{phone}</span> <span className="text-xs text-tertiary">{phone}</span>
</Table.Cell> </Table.Cell>
@@ -191,17 +190,6 @@ export const LeadTable = ({
<Table.Cell /> <Table.Cell />
<Table.Cell /> <Table.Cell />
<Table.Cell /> <Table.Cell />
<Table.Cell />
<Table.Cell>
<div className="flex gap-2">
<Button size="sm" color="primary">
Merge
</Button>
<Button size="sm" color="secondary">
Keep Separate
</Button>
</div>
</Table.Cell>
</Table.Row> </Table.Row>
); );
} }
@@ -219,12 +207,26 @@ export const LeadTable = ({
key={row.id} key={row.id}
id={row.id} id={row.id}
className={cx( className={cx(
'group/row',
isSpamRow && !isSelected && 'bg-warning-primary', isSpamRow && !isSelected && 'bg-warning-primary',
isSelected && 'bg-brand-primary', isSelected && 'bg-brand-primary',
)} )}
> >
<Table.Cell>
<button
onClick={(e) => { e.stopPropagation(); onViewActivity?.(lead); }}
className="flex size-7 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
title="View details"
>
<FontAwesomeIcon icon={faEye} className="size-3.5" />
</button>
</Table.Cell>
{isCol('phone') && <Table.Cell> {isCol('phone') && <Table.Cell>
<span className="font-semibold text-primary">{phone}</span> {phoneRaw ? (
<PhoneActionCell phoneNumber={phoneRaw} displayNumber={phone} />
) : (
<span className="text-sm text-quaternary">{'\u2014'}</span>
)}
</Table.Cell>} </Table.Cell>}
{isCol('name') && <Table.Cell> {isCol('name') && <Table.Cell>
<span className="text-secondary">{name}</span> <span className="text-secondary">{name}</span>
@@ -308,15 +310,6 @@ export const LeadTable = ({
<span className="text-tertiary">0</span> <span className="text-tertiary">0</span>
)} )}
</Table.Cell>} </Table.Cell>}
<Table.Cell>
<Button
size="sm"
color="tertiary"
iconLeading={DotsVertical}
aria-label="Row actions"
onClick={() => onViewActivity?.(lead)}
/>
</Table.Cell>
</Table.Row> </Table.Row>
); );
}} }}

View File

@@ -11,8 +11,7 @@ import { Input } from '@/components/base/input/input';
// Tabs removed — campaign pills handle all filtering now // Tabs removed — campaign pills handle all filtering now
// import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; // import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PaginationPageDefault } from '@/components/application/pagination/pagination';
// TopBar replaced by inline header import { PageHeader } from '@/components/layout/page-header';
// import { TopBar } from '@/components/layout/top-bar';
import { LeadTable } from '@/components/leads/lead-table'; import { LeadTable } from '@/components/leads/lead-table';
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
// Bulk actions removed — checkboxes hidden // Bulk actions removed — checkboxes hidden
@@ -244,37 +243,37 @@ export const AllLeadsPage = () => {
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
{/* Header with controls inline */} <PageHeader
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3"> title="All Leads"
<div> subtitle={`${total} total`}
<h1 className="text-lg font-bold text-primary">All Leads</h1> infoText="Campaign-sourced marketing leads. Organic callers are on the Contacts page."
<p className="text-xs text-tertiary">{total} total</p> controls={
</div> <>
<div className="flex items-center gap-2"> <div className="w-56">
<div className="w-56"> <Input
<Input placeholder="Search leads..."
placeholder="Search leads..." icon={SearchLg}
icon={SearchLg} size="sm"
value={searchQuery}
onChange={(value) => {
setSearchQuery(value);
setCurrentPage(1);
}}
aria-label="Search leads"
/>
</div>
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
<Button
size="sm" size="sm"
value={searchQuery} color="secondary"
onChange={(value) => { iconLeading={Download01}
setSearchQuery(value); onClick={handleExportCsv}
setCurrentPage(1); >
}} Export CSV
aria-label="Search leads" </Button>
/> </>
</div> }
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} /> />
<Button
size="sm"
color="secondary"
iconLeading={Download01}
onClick={handleExportCsv}
>
Export CSV
</Button>
</div>
</div>
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">

View File

@@ -12,7 +12,6 @@ import { Badge } from '@/components/base/badges/badges';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Table } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
import { PaginationCardDefault } from '@/components/application/pagination/pagination'; import { PaginationCardDefault } from '@/components/application/pagination/pagination';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
// TopBar replaced by inline header // TopBar replaced by inline header
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal'; import { ModalOverlay, Modal, Dialog } from '@/components/application/modals/modal';
@@ -20,6 +19,7 @@ import { Select } from '@/components/base/select/select';
import { DatePicker } from '@/components/application/date-picker/date-picker'; import { DatePicker } from '@/components/application/date-picker/date-picker';
import { parseDate, today, getLocalTimeZone } from '@internationalized/date'; import { parseDate, today, getLocalTimeZone } from '@internationalized/date';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { PageHeader } from '@/components/layout/page-header';
import { formatPhone, formatDateOnly, formatTimeOnly } from '@/lib/format'; import { formatPhone, formatDateOnly, formatTimeOnly } from '@/lib/format';
import { apiClient } from '@/lib/api-client'; import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
@@ -551,33 +551,44 @@ export const AppointmentsPageV2 = () => {
return ( return (
<> <>
{/* Header with search inline */} <PageHeader
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3"> title="Appointments"
<div> badge={filtered.length}
<h1 className="text-lg font-bold text-primary">Appointments</h1> infoText="All scheduled, completed, cancelled, and rescheduled appointments. Click the eye icon to view details or reschedule."
</div> controls={
<div className="w-56"> <div className="w-56">
<Input <Input
placeholder="Search patient, doctor..." placeholder="Search patient, doctor..."
icon={SearchLg} icon={SearchLg}
size="sm" size="sm"
value={search} value={search}
onChange={setSearch} onChange={setSearch}
aria-label="Search appointments" aria-label="Search appointments"
/> />
</div> </div>
</div> }
tabs={
<div className="flex items-center gap-1.5">
{tabItems.map((item) => (
<button
key={item.id}
onClick={() => setTab(item.id)}
className={cx(
'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
tab === item.id
? 'bg-brand-solid text-white'
: 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
)}
>
{item.label}{item.badge ? ` ${item.badge}` : ''}
</button>
))}
</div>
}
/>
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
{/* Tabs */}
<div className="flex shrink-0 items-end px-6 pt-2 pb-0.5">
<Tabs selectedKey={tab} onSelectionChange={(key) => setTab(key as StatusTab)}>
<TabList items={tabItems} type="underline" size="sm">
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />}
</TabList>
</Tabs>
</div>
<div className="flex flex-1 flex-col overflow-hidden px-4 pt-3"> <div className="flex flex-1 flex-col overflow-hidden px-4 pt-3">
{loading ? ( {loading ? (
@@ -610,7 +621,7 @@ export const AppointmentsPageV2 = () => {
return ( return (
<Table.Row <Table.Row
id={appt.id} id={appt.id}
className={cx(isSelected && 'bg-brand-primary')} className={cx('group/row', isSelected && 'bg-brand-primary')}
> >
{/* Eye icon — first column */} {/* Eye icon — first column */}
<Table.Cell> <Table.Cell>
@@ -627,7 +638,7 @@ export const AppointmentsPageV2 = () => {
<Table.Cell> <Table.Cell>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium text-primary truncate">{name}</p> <p className="text-sm font-medium text-primary truncate">{name}</p>
{phone && <p className="text-xs text-tertiary">{formatPhone({ number: phone, callingCode: '+91' })}</p>} {phone && <PhoneActionCell phoneNumber={phone} displayNumber={formatPhone({ number: phone, callingCode: '+91' })} />}
</div> </div>
</Table.Cell> </Table.Cell>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft, faFlask } from '@fortawesome/pro-duotone-svg-icons'; import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft, faFlask, faMagnifyingGlass, faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom, sipCallDurationAtom } from '@/state/sip-state'; import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom, sipCallDurationAtom } from '@/state/sip-state';
import { useAuth } from '@/providers/auth-provider'; import { useAuth } from '@/providers/auth-provider';
@@ -12,11 +12,15 @@ import type { WorklistSelection } from '@/components/call-desk/worklist-panel';
import { ContextPanel } from '@/components/call-desk/context-panel'; import { ContextPanel } from '@/components/call-desk/context-panel';
import type { ContextPanelSubject } from '@/components/call-desk/context-panel'; import type { ContextPanelSubject } from '@/components/call-desk/context-panel';
import { ActiveCallCard } from '@/components/call-desk/active-call-card'; import { ActiveCallCard } from '@/components/call-desk/active-call-card';
import { Input } from '@/components/base/input/input';
import { faIcon } from '@/lib/icon-wrapper';
import { apiClient } from '@/lib/api-client'; import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
import { cx } from '@/utils/cx'; import { cx } from '@/utils/cx';
const SearchLg = faIcon(faMagnifyingGlass);
export const CallDeskPage = () => { export const CallDeskPage = () => {
const { user } = useAuth(); const { user } = useAuth();
const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData(); const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData();
@@ -30,6 +34,7 @@ export const CallDeskPage = () => {
const [diallerOpen, setDiallerOpen] = useState(false); const [diallerOpen, setDiallerOpen] = useState(false);
const [dialNumber, setDialNumber] = useState(''); const [dialNumber, setDialNumber] = useState('');
const [dialling, setDialling] = useState(false); const [dialling, setDialling] = useState(false);
const [search, setSearch] = useState('');
// DEV: simulate incoming call // DEV: simulate incoming call
const setSimCallState = useSetAtom(sipCallStateAtom); const setSimCallState = useSetAtom(sipCallStateAtom);
@@ -166,14 +171,29 @@ export const CallDeskPage = () => {
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
{/* Compact header: title + name on left, status + toggle on right */} {/* Header — matches PageHeader visual pattern */}
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3"> <div className="flex shrink-0 items-center justify-between px-6 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<h1 className="text-lg font-bold text-primary">Call Desk</h1> <h1 className="text-lg font-bold text-primary">Call Desk</h1>
<span className="text-sm text-tertiary">{user.name}</span> <span className="text-sm text-tertiary ml-1">{user.name}</span>
<span className="flex size-5 items-center justify-center text-fg-quaternary" title="Your active worklist — missed calls, leads, and follow-ups prioritised by SLA.">
<FontAwesomeIcon icon={faCircleInfo} className="size-4" />
</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isInCall && (
<div className="w-52">
<Input
placeholder="Search worklist..."
icon={SearchLg}
size="sm"
value={search}
onChange={setSearch}
aria-label="Search worklist"
/>
</div>
)}
{import.meta.env.DEV && (!isInCall ? ( {import.meta.env.DEV && (!isInCall ? (
<button <button
onClick={startSimCall} onClick={startSimCall}
@@ -283,6 +303,7 @@ export const CallDeskPage = () => {
onSelectItem={handleSelectItem} onSelectItem={handleSelectItem}
selectedItemId={selectedItemId} selectedItemId={selectedItemId}
onDialMissedCall={(id) => setActiveMissedCallId(id)} onDialMissedCall={(id) => setActiveMissedCallId(id)}
search={search}
/> />
)} )}
</div> </div>

View File

@@ -11,11 +11,12 @@ import {
} from '@fortawesome/pro-duotone-svg-icons'; } from '@fortawesome/pro-duotone-svg-icons';
const SearchLg: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faMagnifyingGlass} className={className} />; const SearchLg: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faMagnifyingGlass} className={className} />;
import { Table, TableCard } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
import { Badge } from '@/components/base/badges/badges'; import { Badge } from '@/components/base/badges/badges';
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Select } from '@/components/base/select/select'; import { Select } from '@/components/base/select/select';
import { PageHeader } from '@/components/layout/page-header';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { formatShortDate, formatPhone } from '@/lib/format'; import { formatShortDate, formatPhone } from '@/lib/format';
// cx removed — no longer used after SLA column removal // cx removed — no longer used after SLA column removal
@@ -189,44 +190,44 @@ export const CallHistoryPage = () => {
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<div className="flex flex-1 flex-col overflow-hidden p-7"> <PageHeader
<TableCard.Root size="md" className="flex-1 min-h-0"> title={isAdmin ? 'Call History' : 'My Call History'}
<TableCard.Header badge={filteredCalls.length}
title={isAdmin ? 'Call History' : 'My Call History'} subtitle={isAdmin ? `${completedCount} completed \u00B7 ${missedCount} missed` : `${completedCount} completed`}
badge={String(filteredCalls.length)} infoText={isAdmin ? 'All calls across all agents with recordings and dispositions.' : 'Your answered inbound and outbound calls.'}
description={isAdmin ? `${completedCount} completed \u00B7 ${missedCount} missed` : `${completedCount} completed`} controls={
contentTrailing={ <>
<div className="flex items-center gap-2"> <div className="w-44">
<div className="w-44"> <Select
<Select size="sm"
size="sm" placeholder="All Calls"
placeholder="All Calls" selectedKey={filter}
selectedKey={filter} onSelectionChange={(key) => setFilter(key as FilterKey)}
onSelectionChange={(key) => setFilter(key as FilterKey)} items={isAdmin ? allFilterItems : agentFilterItems}
items={isAdmin ? allFilterItems : agentFilterItems} aria-label="Filter calls"
aria-label="Filter calls" >
> {(item) => (
{(item) => ( <Select.Item id={item.id} label={item.label}>
<Select.Item id={item.id} label={item.label}> {item.label}
{item.label} </Select.Item>
</Select.Item> )}
)} </Select>
</Select>
</div>
<div className="w-56">
<Input
placeholder="Search calls..."
icon={SearchLg}
size="sm"
value={search}
onChange={(value) => setSearch(value)}
aria-label="Search calls"
/>
</div>
</div> </div>
} <div className="w-56">
/> <Input
placeholder="Search calls..."
icon={SearchLg}
size="sm"
value={search}
onChange={(value) => setSearch(value)}
aria-label="Search calls"
/>
</div>
</>
}
/>
<div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3">
{filteredCalls.length === 0 ? ( {filteredCalls.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16"> <div className="flex flex-col items-center justify-center py-16">
<h3 className="text-sm font-semibold text-primary">No calls found</h3> <h3 className="text-sm font-semibold text-primary">No calls found</h3>
@@ -314,15 +315,16 @@ export const CallHistoryPage = () => {
</Table.Body> </Table.Body>
</Table> </Table>
)} )}
<div className="shrink-0">
<PaginationCardDefault
page={page}
total={totalPages}
onPageChange={setPage}
/>
</div>
</TableCard.Root>
</div> </div>
{totalPages > 1 && (
<div className="shrink-0 border-t border-secondary px-6 py-3">
<PaginationCardDefault
page={page}
total={totalPages}
onPageChange={setPage}
/>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -17,7 +17,7 @@ const SearchLg: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PaginationPageDefault } from '@/components/application/pagination/pagination';
import { TopBar } from '@/components/layout/top-bar'; import { PageHeader } from '@/components/layout/page-header';
import { LeadTable } from '@/components/leads/lead-table'; import { LeadTable } from '@/components/leads/lead-table';
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
import { LeadActivitySlideout } from '@/components/leads/lead-activity-slideout'; import { LeadActivitySlideout } from '@/components/leads/lead-activity-slideout';
@@ -113,14 +113,12 @@ export const ContactsPage = () => {
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<TopBar title="Contacts" subtitle={`${contacts.length} organic callers`} /> <PageHeader
title="Contacts"
<div className="flex flex-1 flex-col overflow-hidden"> badge={contacts.length}
<div className="flex shrink-0 items-center justify-between px-6 py-3 border-b border-secondary"> infoText="People who reached out directly — phone, walk-in, referral. Not sourced from campaigns."
<p className="text-xs text-tertiary"> controls={
People who reached out directly phone, walk-in, referral. Not sourced from campaigns. <>
</p>
<div className="flex items-center gap-2">
<div className="w-56"> <div className="w-56">
<Input <Input
placeholder="Search contacts..." placeholder="Search contacts..."
@@ -135,9 +133,11 @@ export const ContactsPage = () => {
<Button size="sm" color="secondary" iconLeading={Download01} onClick={handleExportCsv}> <Button size="sm" color="secondary" iconLeading={Download01} onClick={handleExportCsv}>
Export CSV Export CSV
</Button> </Button>
</div> </>
</div> }
/>
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto px-4 pt-3"> <div className="flex-1 overflow-y-auto px-4 pt-3">
<LeadTable <LeadTable
leads={paged} leads={paged}

View File

@@ -9,7 +9,7 @@ import { Input } from '@/components/base/input/input';
import { Table } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PaginationPageDefault } from '@/components/application/pagination/pagination';
import { TopBar } from '@/components/layout/top-bar'; import { PageHeader } from '@/components/layout/page-header';
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { apiClient } from '@/lib/api-client'; import { apiClient } from '@/lib/api-client';
@@ -238,55 +238,58 @@ export const MissedCallsPage = () => {
]; ];
return ( return (
<> <div className="flex flex-1 flex-col overflow-hidden">
<TopBar title="Missed Calls" /> <PageHeader
<div className="flex flex-1 flex-col overflow-hidden"> title="Missed Calls"
{/* Tabs + toolbar */} badge={calls.length}
<div className="flex shrink-0 items-end justify-between border-b border-secondary px-6 pt-3 pb-0.5"> infoText="Inbound calls that were not answered. Agents can call back from here."
controls={
<>
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
<div className="w-56">
<Input placeholder="Search phone, agent, branch..." icon={SearchLg} size="sm" value={search} onChange={handleSearch} />
</div>
</>
}
tabs={
<Tabs selectedKey={tab} onSelectionChange={(key) => handleTab(key as StatusTab)}> <Tabs selectedKey={tab} onSelectionChange={(key) => handleTab(key as StatusTab)}>
<TabList items={tabItems} type="underline" size="sm"> <TabList items={tabItems} type="underline" size="sm">
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />} {(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />}
</TabList> </TabList>
</Tabs> </Tabs>
<div className="flex items-center gap-3 pb-1"> }
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} /> />
<div className="w-56">
<Input placeholder="Search phone, agent, branch..." icon={SearchLg} size="sm" value={search} onChange={handleSearch} />
</div>
</div>
</div>
{/* Table */} {/* Table */}
<div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3"> <div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<p className="text-sm text-tertiary">Loading missed calls...</p> <p className="text-sm text-tertiary">Loading missed calls...</p>
</div>
) : filtered.length === 0 ? (
<div className="flex items-center justify-center py-12">
<p className="text-sm text-quaternary">{search ? 'No matching calls' : 'No missed calls'}</p>
</div>
) : (
<DynamicMissedCallTable
calls={pagedRows}
columns={columnDefs.filter(c => visibleColumns.has(c.id))}
sortDescriptor={sortDescriptor}
onSortChange={setSortDescriptor}
/>
)}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="shrink-0 border-t border-secondary px-6 py-3">
<PaginationPageDefault
page={currentPage}
total={totalPages}
onPageChange={setCurrentPage}
/>
</div> </div>
) : filtered.length === 0 ? (
<div className="flex items-center justify-center py-12">
<p className="text-sm text-quaternary">{search ? 'No matching calls' : 'No missed calls'}</p>
</div>
) : (
<DynamicMissedCallTable
calls={pagedRows}
columns={columnDefs.filter(c => visibleColumns.has(c.id))}
sortDescriptor={sortDescriptor}
onSortChange={setSortDescriptor}
/>
)} )}
</div> </div>
</>
{/* Pagination */}
{totalPages > 1 && (
<div className="shrink-0 border-t border-secondary px-6 py-3">
<PaginationPageDefault
page={currentPage}
total={totalPages}
onPageChange={setCurrentPage}
/>
</div>
)}
</div>
); );
}; };

View File

@@ -1,13 +1,14 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useMemo, useState } from 'react';
// useNavigate removed — row click opens profile panel // useNavigate removed — row click opens profile panel
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser, faMagnifyingGlass, faCommentDots, faMessageDots, faEllipsisVertical, faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons'; import { faUser, faMagnifyingGlass, faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons';
import { faIcon } from '@/lib/icon-wrapper'; import { faIcon } from '@/lib/icon-wrapper';
const SearchLg = faIcon(faMagnifyingGlass); const SearchLg = faIcon(faMagnifyingGlass);
import { Avatar } from '@/components/base/avatar/avatar'; import { Avatar } from '@/components/base/avatar/avatar';
import { Input } from '@/components/base/input/input'; import { Input } from '@/components/base/input/input';
import { Table, TableCard } from '@/components/application/table/table'; import { Table } from '@/components/application/table/table';
import { PageHeader } from '@/components/layout/page-header';
import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PaginationPageDefault } from '@/components/application/pagination/pagination';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell'; import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
@@ -54,49 +55,6 @@ const getPatientEmail = (patient: Patient): string => {
return patient.emails?.primaryEmail ?? ''; return patient.emails?.primaryEmail ?? '';
}; };
const HamburgerMenu = ({ phone }: { phone: string }) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
return (
<div className="relative" ref={ref}>
<button
onClick={(e) => { e.stopPropagation(); setOpen(!open); }}
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
title="More actions"
>
<FontAwesomeIcon icon={faEllipsisVertical} className="size-4" />
</button>
{open && (
<div className="absolute top-full right-0 mt-1 w-40 rounded-xl bg-primary shadow-xl ring-1 ring-secondary z-50 overflow-hidden py-1">
<button
onClick={(e) => { e.stopPropagation(); window.open(`sms:+91${phone}`, '_self'); setOpen(false); }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-primary_hover text-left"
>
<FontAwesomeIcon icon={faCommentDots} className="size-3.5 text-fg-quaternary" />
Send SMS
</button>
<button
onClick={(e) => { e.stopPropagation(); window.open(`https://wa.me/91${phone}`, '_blank'); setOpen(false); }}
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-primary hover:bg-primary_hover text-left"
>
<FontAwesomeIcon icon={faMessageDots} className="size-3.5 text-fg-quaternary" />
WhatsApp
</button>
</div>
)}
</div>
);
};
export const PatientsPage = () => { export const PatientsPage = () => {
const { patients, loading } = useData(); const { patients, loading } = useData();
@@ -127,37 +85,36 @@ export const PatientsPage = () => {
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<PageHeader
title="All Patients"
badge={filteredPatients.length}
infoText="Manage and view patient records"
controls={
<>
<button
onClick={() => setPanelOpen(!panelOpen)}
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
title={panelOpen ? 'Hide patient profile' : 'Show patient profile'}
>
<FontAwesomeIcon icon={panelOpen ? faSidebarFlip : faSidebar} className="size-4" />
</button>
<div className="w-56">
<Input
placeholder="Search by name or phone..."
icon={SearchLg}
size="sm"
value={searchQuery}
onChange={handleSearch}
aria-label="Search patients"
/>
</div>
</>
}
/>
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 flex-col overflow-y-auto p-7"> <div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3">
<TableCard.Root size="sm">
<TableCard.Header
title="All Patients"
badge={filteredPatients.length}
description="Manage and view patient records"
contentTrailing={
<div className="flex items-center gap-2">
<button
onClick={() => setPanelOpen(!panelOpen)}
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
title={panelOpen ? 'Hide patient profile' : 'Show patient profile'}
>
<FontAwesomeIcon icon={panelOpen ? faSidebarFlip : faSidebar} className="size-4" />
</button>
<div className="w-56">
<Input
placeholder="Search by name or phone..."
icon={SearchLg}
size="sm"
value={searchQuery}
onChange={handleSearch}
aria-label="Search patients"
/>
</div>
</div>
}
/>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<p className="text-sm text-tertiary">Loading patients...</p> <p className="text-sm text-tertiary">Loading patients...</p>
@@ -178,7 +135,6 @@ export const PatientsPage = () => {
<Table.Head label="EMAIL" /> <Table.Head label="EMAIL" />
<Table.Head label="GENDER" /> <Table.Head label="GENDER" />
<Table.Head label="AGE" /> <Table.Head label="AGE" />
<Table.Head label="" className="w-12" />
</Table.Header> </Table.Header>
<Table.Body items={pagedPatients} dependencies={[selectedPatient?.id]}> <Table.Body items={pagedPatients} dependencies={[selectedPatient?.id]}>
{(patient) => { {(patient) => {
@@ -192,10 +148,10 @@ export const PatientsPage = () => {
: '?'; : '?';
return ( return (
<Table.Row <Table.Row
id={patient.id} id={patient.id}
className={cx( className={cx(
'cursor-pointer', 'cursor-pointer group/row',
selectedPatient?.id === patient.id && 'bg-brand-primary' selectedPatient?.id === patient.id && 'bg-brand-primary'
)} )}
onAction={() => { onAction={() => {
@@ -255,25 +211,18 @@ export const PatientsPage = () => {
</span> </span>
</Table.Cell> </Table.Cell>
{/* Hamburger — SMS + WhatsApp */}
<Table.Cell>
{phone ? (
<HamburgerMenu phone={phone} />
) : null}
</Table.Cell>
</Table.Row> </Table.Row>
); );
}} }}
</Table.Body> </Table.Body>
</Table> </Table>
)} )}
</TableCard.Root>
{totalPages > 1 && ( {totalPages > 1 && (
<div className="shrink-0 border-t border-secondary px-6 py-3"> <div className="shrink-0 border-t border-secondary px-6 py-3">
<PaginationPageDefault page={currentPage} total={totalPages} onPageChange={setCurrentPage} /> <PaginationPageDefault page={currentPage} total={totalPages} onPageChange={setCurrentPage} />
</div> </div>
)} )}
</div> </div>
{/* Patient Profile Panel - collapsible with smooth transition */} {/* Patient Profile Panel - collapsible with smooth transition */}