mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38: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:
@@ -1,11 +1,17 @@
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faWifi, faWifiSlash } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { SipProvider } from '@/providers/sip-provider';
|
||||
import { useSip } from '@/providers/sip-provider';
|
||||
import { CallWidget } from '@/components/call-desk/call-widget';
|
||||
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
||||
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
||||
import { useNetworkStatus } from '@/hooks/use-network-status';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode;
|
||||
@@ -15,6 +21,9 @@ export const AppShell = ({ children }: AppShellProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const { isCCAgent } = useAuth();
|
||||
const { isOpen, activeAction, close } = useMaintShortcuts();
|
||||
const { connectionStatus, isRegistered } = useSip();
|
||||
const networkQuality = useNetworkStatus();
|
||||
const hasAgentConfig = !!localStorage.getItem('helix_agent_config');
|
||||
|
||||
// Heartbeat: keep agent session alive in Redis (CC agents only)
|
||||
useEffect(() => {
|
||||
@@ -39,7 +48,29 @@ export const AppShell = ({ children }: AppShellProps) => {
|
||||
<SipProvider>
|
||||
<div className="flex h-screen bg-primary">
|
||||
<Sidebar activeUrl={pathname} />
|
||||
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Persistent top bar — visible on all pages */}
|
||||
{hasAgentConfig && (
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-b border-secondary px-4 py-2">
|
||||
<div className={cx(
|
||||
'flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
|
||||
networkQuality === 'good'
|
||||
? 'bg-success-primary text-success-primary'
|
||||
: networkQuality === 'offline'
|
||||
? 'bg-error-secondary text-error-primary'
|
||||
: 'bg-warning-secondary text-warning-primary',
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
||||
className="size-3"
|
||||
/>
|
||||
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
|
||||
</div>
|
||||
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
||||
</div>
|
||||
)}
|
||||
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
|
||||
</div>
|
||||
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
||||
</div>
|
||||
<MaintOtpModal isOpen={isOpen} onOpenChange={(open) => !open && close()} action={activeAction} />
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
faCalendarCheck,
|
||||
faPhone,
|
||||
faUsers,
|
||||
faWifi,
|
||||
faWifiSlash,
|
||||
faArrowRightFromBracket,
|
||||
faTowerBroadcast,
|
||||
faChartLine,
|
||||
@@ -31,10 +29,8 @@ import { NavAccountCard } from "@/components/application/app-navigation/base-com
|
||||
import { NavItemBase } from "@/components/application/app-navigation/base-components/nav-item";
|
||||
import type { NavItemType } from "@/components/application/app-navigation/config";
|
||||
import { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import { notify } from "@/lib/toast";
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
import { useNetworkStatus } from "@/hooks/use-network-status";
|
||||
import { useAgentState } from "@/hooks/use-agent-state";
|
||||
import { sidebarCollapsedAtom } from "@/state/sidebar-state";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
@@ -126,7 +122,10 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
const { logout, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
|
||||
const networkQuality = useNetworkStatus();
|
||||
const agentConfig = typeof window !== 'undefined' ? localStorage.getItem('helix_agent_config') : null;
|
||||
const agentId = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
|
||||
const ozonetelState = useAgentState(agentId);
|
||||
const avatarStatus: 'online' | 'offline' = ozonetelState === 'ready' ? 'online' : 'offline';
|
||||
|
||||
const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH;
|
||||
|
||||
@@ -142,15 +141,6 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleForceReady = async () => {
|
||||
try {
|
||||
await apiClient.post('/api/ozonetel/agent-ready', {});
|
||||
notify.success('Agent Ready', 'Agent state has been reset to Ready');
|
||||
} catch {
|
||||
notify.error('Force Ready Failed', 'Could not reset agent state');
|
||||
}
|
||||
};
|
||||
|
||||
const navSections = getNavSections(user.role);
|
||||
|
||||
const content = (
|
||||
@@ -222,25 +212,6 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Network indicator — only shows when network is degraded */}
|
||||
{networkQuality !== 'good' && (
|
||||
<div className={cx(
|
||||
"mx-3 mb-2 flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-medium",
|
||||
networkQuality === 'offline'
|
||||
? "bg-error-secondary text-error-primary"
|
||||
: "bg-warning-secondary text-warning-primary",
|
||||
collapsed && "justify-center mx-2 px-2",
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
||||
className="size-3.5 shrink-0"
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span>{networkQuality === 'offline' ? 'No connection' : 'Unstable network'}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Account card */}
|
||||
<div className={cx("mt-auto py-4", collapsed ? "flex justify-center px-2" : "px-2 lg:px-4")}>
|
||||
{collapsed ? (
|
||||
@@ -249,7 +220,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
title={`${user.name}\nSign out`}
|
||||
className="rounded-lg p-1 hover:bg-primary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
<Avatar size="sm" initials={user.initials} status="online" />
|
||||
<Avatar size="sm" initials={user.initials} status={avatarStatus} />
|
||||
</button>
|
||||
) : (
|
||||
<NavAccountCard
|
||||
@@ -258,11 +229,13 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatar: '',
|
||||
status: 'online' as const,
|
||||
status: avatarStatus,
|
||||
}]}
|
||||
selectedAccountId="current"
|
||||
popoverPlacement="top"
|
||||
onSignOut={handleSignOut}
|
||||
onForceReady={handleForceReady}
|
||||
onViewProfile={() => navigate('/profile')}
|
||||
onAccountSettings={() => navigate('/account-settings')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user