import { useEffect, useState, useCallback, 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 { NotificationBell } from './notification-bell'; import { ResumeSetupBanner } from '@/components/setup/resume-setup-banner'; import { Badge } from '@/components/base/badges/badges'; import { useAuth } from '@/providers/auth-provider'; import { useData } from '@/providers/data-provider'; import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts'; import { useNetworkStatus } from '@/hooks/use-network-status'; import { apiClient } from '@/lib/api-client'; import { cx } from '@/utils/cx'; interface AppShellProps { children: ReactNode; } export const AppShell = ({ children }: AppShellProps) => { const { pathname } = useLocation(); const { isCCAgent, isAdmin } = useAuth(); const { isOpen, activeAction, close } = useMaintShortcuts(); const { connectionStatus, isRegistered } = useSip(); const networkQuality = useNetworkStatus(); const hasAgentConfig = !!localStorage.getItem('helix_agent_config'); // Pre-step state for actions that need user input before OTP const [preStepPayload, setPreStepPayload] = useState | undefined>(undefined); const { campaigns, leads, refresh } = useData(); const leadsPerCampaign = useCallback((campaignId: string) => leads.filter(l => l.campaignId === campaignId).length, [leads], ); // Client-side handler for clearing campaign leads const clearCampaignLeadsHandler = useCallback(async (payload: any) => { const campaignId = payload?.campaignId; if (!campaignId) return { status: 'error', message: 'No campaign selected' }; const campaignLeads = leads.filter(l => l.campaignId === campaignId); if (campaignLeads.length === 0) return { status: 'ok', message: 'No leads to clear' }; let deleted = 0; for (const lead of campaignLeads) { try { await apiClient.graphql(`mutation($id: UUID!) { deleteLead(id: $id) { id } }`, { id: lead.id }, { silent: true }); deleted++; } catch { /* continue */ } } refresh(); return { status: 'ok', message: `${deleted} leads deleted` }; }, [leads, refresh]); // Attach client-side handler to the action when it's clear-campaign-leads const enrichedAction = activeAction ? { ...activeAction, ...(activeAction.endpoint === 'clear-campaign-leads' ? { clientSideHandler: clearCampaignLeadsHandler } : {}), } : null; // Reset pre-step when modal closes useEffect(() => { if (!isOpen) setPreStepPayload(undefined); }, [isOpen]); // Pre-step content for campaign selection const campaignPreStep = activeAction?.needsPreStep && activeAction.endpoint === 'clear-campaign-leads' ? (
{campaigns.map(c => { const count = leadsPerCampaign(c.id); const isSelected = preStepPayload?.campaignId === c.id; return ( ); })}
) : undefined; // Heartbeat: keep agent session alive in Redis (CC agents only) useEffect(() => { if (!isCCAgent) return; const beat = () => { const token = localStorage.getItem('helix_access_token'); if (token) { const apiUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; fetch(`${apiUrl}/auth/heartbeat`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }).catch(() => {}); } }; const interval = setInterval(beat, 5 * 60 * 1000); return () => clearInterval(interval); }, [isCCAgent]); return (
{/* Persistent top bar — visible on all pages */} {(hasAgentConfig || isAdmin) && (
{isAdmin && } {hasAgentConfig && ( <>
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
)}
)}
{children}
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && }
!open && close()} action={enrichedAction} preStepContent={campaignPreStep} preStepPayload={preStepPayload} preStepReady={!activeAction?.needsPreStep || !!preStepPayload} />
); };