mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: info icon on all PageHeader pages + Call Desk header restyled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,29 +1,71 @@
|
||||
// PageHeader — consistent header layout for all list pages.
|
||||
//
|
||||
// Row 1: Title (+ optional badge + subtitle) on the left,
|
||||
// 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="Appointments"
|
||||
// badge={37}
|
||||
// subtitle="Manage appointments"
|
||||
// title="Contacts"
|
||||
// badge={16}
|
||||
// infoText="People who reached out directly — phone, walk-in, referral."
|
||||
// controls={<><Input .../> <Button .../></>}
|
||||
// tabs={<Tabs ...><TabList ...>{...}</TabList></Tabs>}
|
||||
// />
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
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;
|
||||
}
|
||||
|
||||
export const PageHeader = ({ title, badge, subtitle, controls, tabs }: PageHeaderProps) => (
|
||||
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">
|
||||
@@ -37,6 +79,7 @@ export const PageHeader = ({ title, badge, subtitle, controls, tabs }: PageHeade
|
||||
{subtitle && (
|
||||
<span className="text-sm text-tertiary ml-1">{subtitle}</span>
|
||||
)}
|
||||
{infoText && <InfoTooltip text={infoText} />}
|
||||
</div>
|
||||
{controls && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -246,6 +246,7 @@ export const AllLeadsPage = () => {
|
||||
<PageHeader
|
||||
title="All Leads"
|
||||
subtitle={`${total} total`}
|
||||
infoText="Campaign-sourced marketing leads. Organic callers are on the Contacts page."
|
||||
controls={
|
||||
<>
|
||||
<div className="w-56">
|
||||
|
||||
@@ -555,6 +555,7 @@ export const AppointmentsPageV2 = () => {
|
||||
<PageHeader
|
||||
title="Appointments"
|
||||
badge={filtered.length}
|
||||
infoText="All scheduled, completed, cancelled, and rescheduled appointments. Click the eye icon to view details or reschedule."
|
||||
controls={
|
||||
<div className="w-56">
|
||||
<Input
|
||||
|
||||
@@ -16,6 +16,8 @@ import { ActiveCallCard } from '@/components/call-desk/active-call-card';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { notify } from '@/lib/toast';
|
||||
import { cx } from '@/utils/cx';
|
||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
|
||||
|
||||
export const CallDeskPage = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -166,11 +168,14 @@ export const CallDeskPage = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Compact header: title + name on left, status + toggle on right */}
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Header — matches PageHeader visual pattern */}
|
||||
<div className="flex shrink-0 items-center justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<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.">
|
||||
<FAIcon icon={faCircleInfo} className="size-4" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -194,6 +194,7 @@ export const CallHistoryPage = () => {
|
||||
title={isAdmin ? 'Call History' : 'My Call History'}
|
||||
badge={filteredCalls.length}
|
||||
subtitle={isAdmin ? `${completedCount} completed \u00B7 ${missedCount} missed` : `${completedCount} completed`}
|
||||
infoText={isAdmin ? 'All calls across all agents with recordings and dispositions.' : 'Your answered inbound and outbound calls.'}
|
||||
controls={
|
||||
<>
|
||||
<div className="w-44">
|
||||
|
||||
@@ -116,7 +116,7 @@ export const ContactsPage = () => {
|
||||
<PageHeader
|
||||
title="Contacts"
|
||||
badge={contacts.length}
|
||||
subtitle="People who reached out directly — phone, walk-in, referral. Not sourced from campaigns."
|
||||
infoText="People who reached out directly — phone, walk-in, referral. Not sourced from campaigns."
|
||||
controls={
|
||||
<>
|
||||
<div className="w-56">
|
||||
|
||||
@@ -242,6 +242,7 @@ export const MissedCallsPage = () => {
|
||||
<PageHeader
|
||||
title="Missed Calls"
|
||||
badge={calls.length}
|
||||
infoText="Inbound calls that were not answered. Agents can call back from here."
|
||||
controls={
|
||||
<>
|
||||
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
|
||||
|
||||
@@ -131,7 +131,7 @@ export const PatientsPage = () => {
|
||||
<PageHeader
|
||||
title="All Patients"
|
||||
badge={filteredPatients.length}
|
||||
subtitle="Manage and view patient records"
|
||||
infoText="Manage and view patient records"
|
||||
controls={
|
||||
<>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user