mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
refactor: unified maint modal with pre-step support, OTP-gated campaign clear
- Extended MaintAction with needsPreStep + clientSideHandler - MaintOtpModal supports pre-step content before OTP (campaign selection) - Removed standalone ClearCampaignLeadsModal — all maint actions go through one modal - 4-step import wizard with Untitled UI Select for mapping - DynamicTable className passthrough Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
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';
|
||||
@@ -7,12 +7,14 @@ 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 { ClearCampaignLeadsModal } from '@/components/modals/clear-campaign-leads-modal';
|
||||
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||
import { NotificationBell } from './notification-bell';
|
||||
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 {
|
||||
@@ -27,6 +29,69 @@ export const AppShell = ({ children }: AppShellProps) => {
|
||||
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<Record<string, any> | 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' ? (
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{campaigns.map(c => {
|
||||
const count = leadsPerCampaign(c.id);
|
||||
const isSelected = preStepPayload?.campaignId === c.id;
|
||||
return (
|
||||
<button
|
||||
key={c.id}
|
||||
onClick={() => setPreStepPayload({ campaignId: c.id })}
|
||||
className={cx(
|
||||
'flex w-full items-center justify-between rounded-lg border-2 px-3 py-2.5 text-left transition duration-100 ease-linear',
|
||||
isSelected ? 'border-error bg-error-primary' : 'border-secondary hover:border-error',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium text-primary">{c.campaignName ?? 'Untitled'}</span>
|
||||
<Badge size="sm" color={count > 0 ? 'error' : 'gray'} type="pill-color">{count} leads</Badge>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
// Heartbeat: keep agent session alive in Redis (CC agents only)
|
||||
useEffect(() => {
|
||||
if (!isCCAgent) return;
|
||||
@@ -80,8 +145,14 @@ export const AppShell = ({ children }: AppShellProps) => {
|
||||
</div>
|
||||
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
||||
</div>
|
||||
<MaintOtpModal isOpen={isOpen && activeAction?.endpoint !== '__client__clear-campaign-leads'} onOpenChange={(open) => !open && close()} action={activeAction} />
|
||||
<ClearCampaignLeadsModal isOpen={isOpen && activeAction?.endpoint === '__client__clear-campaign-leads'} onOpenChange={(open) => !open && close()} />
|
||||
<MaintOtpModal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => !open && close()}
|
||||
action={enrichedAction}
|
||||
preStepContent={campaignPreStep}
|
||||
preStepPayload={preStepPayload}
|
||||
preStepReady={!activeAction?.needsPreStep || !!preStepPayload}
|
||||
/>
|
||||
</SipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user