mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
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>
This commit is contained in:
55
src/components/layout/page-header.tsx
Normal file
55
src/components/layout/page-header.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// PageHeader — consistent header layout for all list pages.
|
||||||
|
//
|
||||||
|
// Row 1: Title (+ optional badge + subtitle) on the left,
|
||||||
|
// controls (search, columns, export, etc.) on the right.
|
||||||
|
// Row 2: Optional tabs (underline style) — no extra borders.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// <PageHeader
|
||||||
|
// title="Appointments"
|
||||||
|
// badge={37}
|
||||||
|
// subtitle="Manage appointments"
|
||||||
|
// controls={<><Input .../> <Button .../></>}
|
||||||
|
// tabs={<Tabs ...><TabList ...>{...}</TabList></Tabs>}
|
||||||
|
// />
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
title: string;
|
||||||
|
badge?: number | string;
|
||||||
|
subtitle?: string;
|
||||||
|
controls?: ReactNode;
|
||||||
|
tabs?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageHeader = ({ title, badge, subtitle, 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>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
@@ -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,36 @@ 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>
|
controls={
|
||||||
<p className="text-xs text-tertiary">{total} total</p>
|
<>
|
||||||
</div>
|
<div className="w-56">
|
||||||
<div className="flex items-center gap-2">
|
<Input
|
||||||
<div className="w-56">
|
placeholder="Search leads..."
|
||||||
<Input
|
icon={SearchLg}
|
||||||
placeholder="Search leads..."
|
size="sm"
|
||||||
icon={SearchLg}
|
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">
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,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 +552,32 @@ 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>
|
controls={
|
||||||
</div>
|
<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={
|
||||||
|
<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 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 ? (
|
||||||
|
|||||||
@@ -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,43 @@ 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)}
|
controls={
|
||||||
description={isAdmin ? `${completedCount} completed \u00B7 ${missedCount} missed` : `${completedCount} completed`}
|
<>
|
||||||
contentTrailing={
|
<div className="w-44">
|
||||||
<div className="flex items-center gap-2">
|
<Select
|
||||||
<div className="w-44">
|
size="sm"
|
||||||
<Select
|
placeholder="All Calls"
|
||||||
size="sm"
|
selectedKey={filter}
|
||||||
placeholder="All Calls"
|
onSelectionChange={(key) => setFilter(key as FilterKey)}
|
||||||
selectedKey={filter}
|
items={isAdmin ? allFilterItems : agentFilterItems}
|
||||||
onSelectionChange={(key) => setFilter(key as FilterKey)}
|
aria-label="Filter calls"
|
||||||
items={isAdmin ? allFilterItems : agentFilterItems}
|
>
|
||||||
aria-label="Filter calls"
|
{(item) => (
|
||||||
>
|
<Select.Item id={item.id} label={item.label}>
|
||||||
{(item) => (
|
{item.label}
|
||||||
<Select.Item id={item.id} label={item.label}>
|
</Select.Item>
|
||||||
{item.label}
|
)}
|
||||||
</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 +314,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
subtitle="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}
|
||||||
|
|||||||
@@ -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,57 @@ 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">
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ 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';
|
||||||
@@ -127,37 +128,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}
|
||||||
|
subtitle="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>
|
||||||
@@ -267,13 +267,12 @@ export const PatientsPage = () => {
|
|||||||
</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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user