import { useState, type ReactNode } from 'react'; import { REGEXP_ONLY_DIGITS } from 'input-otp'; import { Button } from '@/components/base/buttons/button'; import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal'; import { PinInput } from '@/components/base/pin-input/pin-input'; import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faShieldKeyhole, faLock, faLockOpen } from '@fortawesome/pro-duotone-svg-icons'; import type { FC } from 'react'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; const ShieldIcon: FC<{ className?: string }> = ({ className }) => ( ); const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; type MaintAction = { endpoint: string; label: string; description: string; needsPreStep?: boolean; agentPickerEndpoint?: string; clientSideHandler?: (payload: any) => Promise<{ status: string; message: string }>; }; type LockedRow = { agentId: string; displayName: string; heldByIp: string; lockedAt: string }; type FreeRow = { agentId: string; displayName: string }; type SessionStatus = { locked: LockedRow[]; free: FreeRow[] }; type MaintOtpModalProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; action: MaintAction | null; preStepContent?: ReactNode; preStepPayload?: Record; preStepReady?: boolean; }; export const MaintOtpModal = ({ isOpen, onOpenChange, action, preStepContent, preStepPayload, preStepReady = true }: MaintOtpModalProps) => { const [otp, setOtp] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Phase-2 state: once the OTP passes and the action uses an agent // picker, we swap the PIN input for a two-bucket list (Locked / Free) // fetched from `agentPickerEndpoint`. The operator picks a locked // agent, then Confirm posts to the real `endpoint`. const [sessionStatus, setSessionStatus] = useState(null); const [pickedAgentId, setPickedAgentId] = useState(null); // OTP is held across the two-phase flow so we don't force the user // to re-enter it after the picker loads. const [verifiedOtp, setVerifiedOtp] = useState(null); const reset = () => { setOtp(''); setError(null); setSessionStatus(null); setPickedAgentId(null); setVerifiedOtp(null); setLoading(false); }; const handleClose = () => { onOpenChange(false); reset(); }; const postMaint = async (endpoint: string, body: Record, otpHeader: string) => { const res = await fetch(`${API_URL}/api/maint/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-maint-otp': otpHeader }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { ok: res.ok, data }; }; const runPickerAction = async (pickedId: string, otpHeader: string) => { if (!action) return; setLoading(true); setError(null); const payload = { ...preStepPayload, agentId: pickedId }; const { ok, data } = await postMaint(action.endpoint, payload, otpHeader); setLoading(false); if (ok) { notify.success(action.label, data.message ?? 'Completed successfully'); onOpenChange(false); reset(); } else { setError(data.message ?? 'Failed'); } }; const handleSubmit = async () => { if (!action || otp.length < 6) return; setLoading(true); setError(null); try { // Two-phase agent-picker flow — OTP first, then fetch list, // then the operator picks which agent to act on. if (action.agentPickerEndpoint) { const { ok, data } = await postMaint(action.agentPickerEndpoint, {}, otp); if (!ok) { setError(data.message ?? 'Invalid maintenance code'); setLoading(false); return; } setSessionStatus(data as SessionStatus); setVerifiedOtp(otp); setLoading(false); return; } if (action.clientSideHandler) { const { ok, data } = await postMaint('force-ready', {}, otp); if (!ok) { setError(data.message ?? 'Invalid maintenance code'); setLoading(false); return; } const result = await action.clientSideHandler(preStepPayload); notify.success(action.label, result.message ?? 'Completed'); onOpenChange(false); reset(); return; } // Default: single-shot endpoint with agentId from the CC agent's // own local config (cc-agent context). Supervisors hitting this // path without agent config used to get 400 — the agent-picker // branch above is the fix. const agentCfg = localStorage.getItem('helix_agent_config'); const agentId = agentCfg ? JSON.parse(agentCfg).ozonetelAgentId : undefined; const payload = { ...preStepPayload, ...(agentId ? { agentId } : {}) }; const { ok, data } = await postMaint(action.endpoint, payload, otp); if (ok) { notify.success(action.label, data.message ?? 'Completed successfully'); onOpenChange(false); reset(); } else { setError(data.message ?? 'Failed'); } } catch { setError('Request failed'); } finally { setLoading(false); } }; const handleOtpChange = (value: string) => { setOtp(value); setError(null); }; if (!action) return null; const showPicker = Boolean(action.agentPickerEndpoint && sessionStatus && verifiedOtp); const showOtp = (!action.needsPreStep || preStepReady) && !showPicker; const confirmDisabled = showPicker ? !pickedAgentId || loading : otp.length < 6 || loading || (action.needsPreStep && !preStepReady); const handleConfirm = async () => { if (showPicker && pickedAgentId && verifiedOtp) { await runPickerAction(pickedAgentId, verifiedOtp); } else { await handleSubmit(); } }; return ( {() => (
{/* Header */}

{action.label}

{action.description}

{/* Pre-step content (e.g., campaign selection) */} {action.needsPreStep && preStepContent && !showPicker && (
{preStepContent}
)} {showOtp && (
Enter maintenance code {error && (

{error}

)}
)} {showPicker && sessionStatus && (

Locked ({sessionStatus.locked.length})

{sessionStatus.locked.length === 0 ? (

No active session locks.

) : (
{sessionStatus.locked.map((row) => { const selected = pickedAgentId === row.agentId; return ( ); })}
)}

Free ({sessionStatus.free.length})

{sessionStatus.free.length === 0 ? (

No free agents.

) : (
{sessionStatus.free.map((row) => (

{row.displayName}

{row.agentId}

Already free
))}
)}
{error && (

{error}

)}
)} {/* Footer */}
)}
); };