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.
|
// 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.
|
// controls (search, columns, export, etc.) on the right.
|
||||||
// Row 2: Optional tabs (underline style) — no extra borders.
|
// 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:
|
// Usage:
|
||||||
// <PageHeader
|
// <PageHeader
|
||||||
// title="Appointments"
|
// title="Contacts"
|
||||||
// badge={37}
|
// badge={16}
|
||||||
// subtitle="Manage appointments"
|
// infoText="People who reached out directly — phone, walk-in, referral."
|
||||||
// controls={<><Input .../> <Button .../></>}
|
// controls={<><Input .../> <Button .../></>}
|
||||||
// tabs={<Tabs ...><TabList ...>{...}</TabList></Tabs>}
|
// 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 {
|
interface PageHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
badge?: number | string;
|
badge?: number | string;
|
||||||
|
/** Short inline text next to badge — use sparingly (e.g. "17 total") */
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
/** Longer descriptive text shown on info icon hover/click */
|
||||||
|
infoText?: string;
|
||||||
controls?: ReactNode;
|
controls?: ReactNode;
|
||||||
tabs?: 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">
|
<div className="shrink-0">
|
||||||
{/* Row 1: title + controls */}
|
{/* Row 1: title + controls */}
|
||||||
<div className="flex items-center justify-between px-6 py-3">
|
<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 && (
|
{subtitle && (
|
||||||
<span className="text-sm text-tertiary ml-1">{subtitle}</span>
|
<span className="text-sm text-tertiary ml-1">{subtitle}</span>
|
||||||
)}
|
)}
|
||||||
|
{infoText && <InfoTooltip text={infoText} />}
|
||||||
</div>
|
</div>
|
||||||
{controls && (
|
{controls && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const AllLeadsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="All Leads"
|
title="All Leads"
|
||||||
subtitle={`${total} total`}
|
subtitle={`${total} total`}
|
||||||
|
infoText="Campaign-sourced marketing leads. Organic callers are on the Contacts page."
|
||||||
controls={
|
controls={
|
||||||
<>
|
<>
|
||||||
<div className="w-56">
|
<div className="w-56">
|
||||||
|
|||||||
@@ -555,6 +555,7 @@ export const AppointmentsPageV2 = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Appointments"
|
title="Appointments"
|
||||||
badge={filtered.length}
|
badge={filtered.length}
|
||||||
|
infoText="All scheduled, completed, cancelled, and rescheduled appointments. Click the eye icon to view details or reschedule."
|
||||||
controls={
|
controls={
|
||||||
<div className="w-56">
|
<div className="w-56">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { ActiveCallCard } from '@/components/call-desk/active-call-card';
|
|||||||
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';
|
||||||
|
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
|
||||||
|
|
||||||
export const CallDeskPage = () => {
|
export const CallDeskPage = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -166,11 +168,14 @@ 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.">
|
||||||
|
<FAIcon icon={faCircleInfo} className="size-4" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const CallHistoryPage = () => {
|
|||||||
title={isAdmin ? 'Call History' : 'My Call History'}
|
title={isAdmin ? 'Call History' : 'My Call History'}
|
||||||
badge={filteredCalls.length}
|
badge={filteredCalls.length}
|
||||||
subtitle={isAdmin ? `${completedCount} completed \u00B7 ${missedCount} missed` : `${completedCount} completed`}
|
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={
|
controls={
|
||||||
<>
|
<>
|
||||||
<div className="w-44">
|
<div className="w-44">
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const ContactsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Contacts"
|
title="Contacts"
|
||||||
badge={contacts.length}
|
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={
|
controls={
|
||||||
<>
|
<>
|
||||||
<div className="w-56">
|
<div className="w-56">
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export const MissedCallsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Missed Calls"
|
title="Missed Calls"
|
||||||
badge={calls.length}
|
badge={calls.length}
|
||||||
|
infoText="Inbound calls that were not answered. Agents can call back from here."
|
||||||
controls={
|
controls={
|
||||||
<>
|
<>
|
||||||
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
|
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const PatientsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="All Patients"
|
title="All Patients"
|
||||||
badge={filteredPatients.length}
|
badge={filteredPatients.length}
|
||||||
subtitle="Manage and view patient records"
|
infoText="Manage and view patient records"
|
||||||
controls={
|
controls={
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user