mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: disposition modal, persistent top bar, pagination, QA fixes
- DispositionModal: single modal for all call endings. Dismissable (agent can resume call). Agent clicks End → modal → select reason → hangup + dispose. Caller disconnects → same modal. - One call screen: CallWidget stripped to ringing notification + auto-redirect to Call Desk. - Persistent top bar in AppShell: agent status toggle + network indicator on all pages. - Network indicator always visible (Connected/Unstable/No connection). - Pagination: Untitled UI PaginationCardDefault on Call History + Appointments (20/page). - Pinned table headers/footers: sticky column headers, scrollable body, pinned pagination. Applied to Call Desk worklist, Call History, Appointments, Call Recordings, Missed Calls. - "Patient" → "Caller" column label in Call History. - Offline → Ready toggle enabled. - Profile status dot reflects Ozonetel state. - NavAccountCard: popover placement top, View Profile + Account Settings restored. - WIP pages for /profile and /account-settings. - Enquiry form PHONE_INQUIRY → PHONE enum fix. - Force Ready / View Profile / Account Settings removed then restored properly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
src/pages/account-settings.tsx
Normal file
14
src/pages/account-settings.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
export const AccountSettingsPage = () => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TopBar title="Account Settings" />
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<div className="rounded-lg bg-secondary px-4 py-3 text-sm text-tertiary">
|
||||
Account settings are coming soon.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const SearchLg = faIcon(faMagnifyingGlass);
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { Table } from '@/components/application/table/table';
|
||||
import { PaginationCardDefault } from '@/components/application/pagination/pagination';
|
||||
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||
@@ -69,6 +70,8 @@ export const AppointmentsPage = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tab, setTab] = useState<StatusTab>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
useEffect(() => {
|
||||
apiClient.graphql<{ appointments: { edges: Array<{ node: AppointmentRecord }> } }>(QUERY, undefined, { silent: true })
|
||||
@@ -108,6 +111,12 @@ export const AppointmentsPage = () => {
|
||||
return rows;
|
||||
}, [appointments, tab, search]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||
const pagedRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
|
||||
// Reset page on filter/search change
|
||||
useEffect(() => { setPage(1); }, [tab, search]);
|
||||
|
||||
const tabItems = [
|
||||
{ id: 'all' as const, label: 'All', badge: appointments.length > 0 ? String(appointments.length) : undefined },
|
||||
{ id: 'SCHEDULED' as const, label: 'Booked', badge: statusCounts.SCHEDULED ? String(statusCounts.SCHEDULED) : undefined },
|
||||
@@ -122,7 +131,7 @@ export const AppointmentsPage = () => {
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Tabs + search */}
|
||||
<div className="flex items-end justify-between border-b border-secondary px-6 pt-3 pb-0.5">
|
||||
<div className="flex shrink-0 items-end justify-between border-b border-secondary px-6 pt-3 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} />}
|
||||
@@ -141,7 +150,7 @@ export const AppointmentsPage = () => {
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-y-auto px-4 pt-3">
|
||||
<div className="flex flex-1 flex-col overflow-hidden px-4 pt-3">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p className="text-sm text-tertiary">Loading appointments...</p>
|
||||
@@ -162,7 +171,7 @@ export const AppointmentsPage = () => {
|
||||
<Table.Head label="Status" className="w-28" />
|
||||
<Table.Head label="Chief Complaint" />
|
||||
</Table.Header>
|
||||
<Table.Body items={filtered}>
|
||||
<Table.Body items={pagedRows}>
|
||||
{(appt) => {
|
||||
const patientName = appt.patient
|
||||
? `${appt.patient.fullName?.firstName ?? ''} ${appt.patient.fullName?.lastName ?? ''}`.trim() || 'Unknown'
|
||||
@@ -222,6 +231,13 @@ export const AppointmentsPage = () => {
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
<div className="shrink-0">
|
||||
<PaginationCardDefault
|
||||
page={page}
|
||||
total={totalPages}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,14 +11,13 @@ import { ContextPanel } from '@/components/call-desk/context-panel';
|
||||
import { ActiveCallCard } from '@/components/call-desk/active-call-card';
|
||||
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||
import { notify } from '@/lib/toast';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
export const CallDeskPage = () => {
|
||||
const { user } = useAuth();
|
||||
const { leadActivities } = useData();
|
||||
const { connectionStatus, isRegistered, callState, callerNumber, callUcid, dialOutbound } = useSip();
|
||||
const { callState, callerNumber, callUcid, dialOutbound } = useSip();
|
||||
const { missedCalls, followUps, marketingLeads, totalPending, loading } = useWorklist();
|
||||
const [selectedLead, setSelectedLead] = useState<WorklistLead | null>(null);
|
||||
const [contextOpen, setContextOpen] = useState(true);
|
||||
@@ -132,7 +131,6 @@ export const CallDeskPage = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
||||
{totalPending > 0 && (
|
||||
<Badge size="sm" color="brand" type="pill-color">{totalPending} pending</Badge>
|
||||
)}
|
||||
@@ -149,7 +147,7 @@ export const CallDeskPage = () => {
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Main panel */}
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
<div className="flex flex-1 flex-col min-h-0">
|
||||
{/* Active call */}
|
||||
{isInCall && (
|
||||
<div className="p-5">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import { TopBar } from '@/components/layout/top-bar';
|
||||
import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
|
||||
import { formatShortDate, formatPhone } from '@/lib/format';
|
||||
import { useData } from '@/providers/data-provider';
|
||||
import { PaginationCardDefault } from '@/components/application/pagination/pagination';
|
||||
import type { Call, CallDirection, CallDisposition } from '@/types/entities';
|
||||
|
||||
type FilterKey = 'all' | 'inbound' | 'outbound' | 'missed';
|
||||
@@ -103,17 +104,13 @@ const RecordingPlayer: FC<{ url: string }> = ({ url }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export const CallHistoryPage = () => {
|
||||
const { calls, leads } = useData();
|
||||
const [search, setSearch] = useState('');
|
||||
const [filter, setFilter] = useState<FilterKey>('all');
|
||||
|
||||
// Debug: log first call's raw timestamp to diagnose timezone issue
|
||||
if (calls.length > 0 && !(window as any).__callTimestampLogged) {
|
||||
const c = calls[0];
|
||||
console.log(`[DEBUG-TIME] Raw startedAt="${c.startedAt}" → parsed=${new Date(c.startedAt!)} → formatted="${c.startedAt ? new Intl.DateTimeFormat('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).format(new Date(c.startedAt)) : 'n/a'}" | direction=${c.callDirection} status=${c.callStatus}`);
|
||||
(window as any).__callTimestampLogged = true;
|
||||
}
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// Build a map of lead names by ID for enrichment
|
||||
const leadNameMap = useMemo(() => {
|
||||
@@ -161,12 +158,18 @@ export const CallHistoryPage = () => {
|
||||
const completedCount = filteredCalls.filter((c) => c.callStatus !== 'MISSED').length;
|
||||
const missedCount = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredCalls.length / PAGE_SIZE));
|
||||
const pagedCalls = filteredCalls.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
|
||||
// Reset page when filter/search changes
|
||||
useEffect(() => { setPage(1); }, [filter, search]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TopBar title="Call History" subtitle={`${filteredCalls.length} total calls`} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-7">
|
||||
<TableCard.Root size="md">
|
||||
<div className="flex flex-1 flex-col overflow-hidden p-7">
|
||||
<TableCard.Root size="md" className="flex-1 min-h-0">
|
||||
<TableCard.Header
|
||||
title="Call History"
|
||||
badge={String(filteredCalls.length)}
|
||||
@@ -214,7 +217,7 @@ export const CallHistoryPage = () => {
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Head label="TYPE" className="w-14" isRowHeader />
|
||||
<Table.Head label="PATIENT" />
|
||||
<Table.Head label="CALLER" />
|
||||
<Table.Head label="PHONE" />
|
||||
<Table.Head label="DURATION" className="w-24" />
|
||||
<Table.Head label="OUTCOME" />
|
||||
@@ -223,7 +226,7 @@ export const CallHistoryPage = () => {
|
||||
<Table.Head label="TIME" />
|
||||
<Table.Head label="ACTIONS" className="w-24" />
|
||||
</Table.Header>
|
||||
<Table.Body items={filteredCalls}>
|
||||
<Table.Body items={pagedCalls}>
|
||||
{(call) => {
|
||||
const patientName = call.leadName ?? leadNameMap.get(call.leadId ?? '') ?? 'Unknown';
|
||||
const phoneDisplay = formatPhoneDisplay(call);
|
||||
@@ -294,6 +297,13 @@ export const CallHistoryPage = () => {
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
<div className="shrink-0">
|
||||
<PaginationCardDefault
|
||||
page={page}
|
||||
total={totalPages}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</TableCard.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,14 +91,14 @@ export const CallRecordingsPage = () => {
|
||||
<>
|
||||
<TopBar title="Call Recordings" />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-secondary px-6 py-3">
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3">
|
||||
<span className="text-sm text-tertiary">{filtered.length} recordings</span>
|
||||
<div className="w-56">
|
||||
<Input placeholder="Search agent or phone..." icon={SearchLg} size="sm" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 pt-3">
|
||||
<div className="flex flex-1 flex-col overflow-hidden px-4 pt-3">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p className="text-sm text-tertiary">Loading recordings...</p>
|
||||
|
||||
@@ -109,7 +109,7 @@ export const MissedCallsPage = () => {
|
||||
<>
|
||||
<TopBar title="Missed Calls" />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-end justify-between border-b border-secondary px-6 pt-3 pb-0.5">
|
||||
<div className="flex shrink-0 items-end justify-between border-b border-secondary px-6 pt-3 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} />}
|
||||
@@ -120,7 +120,7 @@ export const MissedCallsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 pt-3">
|
||||
<div className="flex flex-1 flex-col overflow-hidden px-4 pt-3">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p className="text-sm text-tertiary">Loading missed calls...</p>
|
||||
|
||||
24
src/pages/profile.tsx
Normal file
24
src/pages/profile.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { Avatar } from '@/components/base/avatar/avatar';
|
||||
|
||||
export const ProfilePage = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TopBar title="Profile" />
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
||||
<Avatar size="2xl" initials={user.initials} />
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-primary">{user.name}</h2>
|
||||
<p className="text-sm text-tertiary">{user.email}</p>
|
||||
<p className="mt-1 text-xs text-quaternary capitalize">{user.role}</p>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-secondary px-4 py-3 text-sm text-tertiary">
|
||||
Profile management is coming soon.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user