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:
@@ -314,8 +314,8 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
|
|||||||
{noPhoneCount > 0 && <span className="text-error-primary">{noPhoneCount} no phone</span>}
|
{noPhoneCount > 0 && <span className="text-error-primary">{noPhoneCount} no phone</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table — fills remaining space, body scrolls */}
|
{/* Table — fills remaining space, header pinned, body scrolls */}
|
||||||
<div className="flex-1 min-h-0 overflow-auto px-4 pt-2">
|
<div className="flex flex-1 flex-col min-h-0 px-4 pt-2">
|
||||||
<DynamicTable<DynamicRow>
|
<DynamicTable<DynamicRow>
|
||||||
columns={[
|
columns={[
|
||||||
...mapping.filter(m => m.leadField).map(m => ({
|
...mapping.filter(m => m.leadField).map(m => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, type ReactNode } from 'react';
|
import { useEffect, useState, useCallback, type ReactNode } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faWifi, faWifiSlash } from '@fortawesome/pro-duotone-svg-icons';
|
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 { useSip } from '@/providers/sip-provider';
|
||||||
import { CallWidget } from '@/components/call-desk/call-widget';
|
import { CallWidget } from '@/components/call-desk/call-widget';
|
||||||
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
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 { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||||
import { NotificationBell } from './notification-bell';
|
import { NotificationBell } from './notification-bell';
|
||||||
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { useAuth } from '@/providers/auth-provider';
|
import { useAuth } from '@/providers/auth-provider';
|
||||||
|
import { useData } from '@/providers/data-provider';
|
||||||
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
||||||
import { useNetworkStatus } from '@/hooks/use-network-status';
|
import { useNetworkStatus } from '@/hooks/use-network-status';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
import { cx } from '@/utils/cx';
|
import { cx } from '@/utils/cx';
|
||||||
|
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
@@ -27,6 +29,69 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
const networkQuality = useNetworkStatus();
|
const networkQuality = useNetworkStatus();
|
||||||
const hasAgentConfig = !!localStorage.getItem('helix_agent_config');
|
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)
|
// Heartbeat: keep agent session alive in Redis (CC agents only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCCAgent) return;
|
if (!isCCAgent) return;
|
||||||
@@ -80,8 +145,14 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
{isCCAgent && pathname !== '/' && pathname !== '/call-desk' && <CallWidget />}
|
||||||
</div>
|
</div>
|
||||||
<MaintOtpModal isOpen={isOpen && activeAction?.endpoint !== '__client__clear-campaign-leads'} onOpenChange={(open) => !open && close()} action={activeAction} />
|
<MaintOtpModal
|
||||||
<ClearCampaignLeadsModal isOpen={isOpen && activeAction?.endpoint === '__client__clear-campaign-leads'} onOpenChange={(open) => !open && close()} />
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(open) => !open && close()}
|
||||||
|
action={enrichedAction}
|
||||||
|
preStepContent={campaignPreStep}
|
||||||
|
preStepPayload={preStepPayload}
|
||||||
|
preStepReady={!activeAction?.needsPreStep || !!preStepPayload}
|
||||||
|
/>
|
||||||
</SipProvider>
|
</SipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faTrash } from '@fortawesome/pro-duotone-svg-icons';
|
|
||||||
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
|
||||||
import { Button } from '@/components/base/buttons/button';
|
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
|
||||||
import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon';
|
|
||||||
import { useData } from '@/providers/data-provider';
|
|
||||||
import { apiClient } from '@/lib/api-client';
|
|
||||||
import { notify } from '@/lib/toast';
|
|
||||||
import { cx } from '@/utils/cx';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
const TrashIcon: FC<{ className?: string }> = ({ className }) => (
|
|
||||||
<FontAwesomeIcon icon={faTrash} className={className} />
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ClearCampaignLeadsModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClearCampaignLeadsModal = ({ isOpen, onOpenChange }: ClearCampaignLeadsModalProps) => {
|
|
||||||
const { campaigns, leads, refresh } = useData();
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
||||||
const [clearing, setClearing] = useState(false);
|
|
||||||
|
|
||||||
const leadsPerCampaign = (campaignId: string) =>
|
|
||||||
leads.filter(l => l.campaignId === campaignId).length;
|
|
||||||
|
|
||||||
const handleClear = async () => {
|
|
||||||
if (!selectedId) return;
|
|
||||||
const campaignLeads = leads.filter(l => l.campaignId === selectedId);
|
|
||||||
if (campaignLeads.length === 0) {
|
|
||||||
notify.info('No Leads', 'No leads to clear for this campaign');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClearing(true);
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notify.success('Leads Cleared', `${deleted} leads deleted from campaign`);
|
|
||||||
setClearing(false);
|
|
||||||
setSelectedId(null);
|
|
||||||
onOpenChange(false);
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (clearing) return;
|
|
||||||
onOpenChange(false);
|
|
||||||
setSelectedId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedLeadCount = selectedId ? leadsPerCampaign(selectedId) : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
|
|
||||||
<Modal className="sm:max-w-md">
|
|
||||||
<Dialog>
|
|
||||||
{() => (
|
|
||||||
<div className="flex w-full flex-col rounded-xl bg-primary shadow-xl ring-1 ring-secondary overflow-hidden">
|
|
||||||
<div className="flex flex-col items-center gap-3 px-6 pt-6 pb-4">
|
|
||||||
<FeaturedIcon icon={TrashIcon} color="error" theme="light" size="md" />
|
|
||||||
<h2 className="text-lg font-semibold text-primary">Clear Campaign Leads</h2>
|
|
||||||
<p className="text-xs text-tertiary text-center">Select a campaign to delete all its imported leads. This cannot be undone.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-6 pb-4 space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
{campaigns.map(c => {
|
|
||||||
const count = leadsPerCampaign(c.id);
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={c.id}
|
|
||||||
onClick={() => setSelectedId(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',
|
|
||||||
selectedId === c.id ? '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>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 border-t border-secondary px-6 py-4">
|
|
||||||
<Button size="md" color="secondary" onClick={handleClose} className="flex-1" isDisabled={clearing}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="md"
|
|
||||||
color="primary-destructive"
|
|
||||||
onClick={handleClear}
|
|
||||||
isDisabled={!selectedId || selectedLeadCount === 0 || clearing}
|
|
||||||
isLoading={clearing}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
{clearing ? 'Clearing...' : `Delete ${selectedLeadCount} Leads`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
</Modal>
|
|
||||||
</ModalOverlay>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
||||||
import { Button } from '@/components/base/buttons/button';
|
import { Button } from '@/components/base/buttons/button';
|
||||||
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
|
||||||
@@ -19,15 +19,20 @@ type MaintAction = {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
needsPreStep?: boolean;
|
||||||
|
clientSideHandler?: (payload: any) => Promise<{ status: string; message: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MaintOtpModalProps = {
|
type MaintOtpModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
action: MaintAction | null;
|
action: MaintAction | null;
|
||||||
|
preStepContent?: ReactNode;
|
||||||
|
preStepPayload?: Record<string, any>;
|
||||||
|
preStepReady?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalProps) => {
|
export const MaintOtpModal = ({ isOpen, onOpenChange, action, preStepContent, preStepPayload, preStepReady = true }: MaintOtpModalProps) => {
|
||||||
const [otp, setOtp] = useState('');
|
const [otp, setOtp] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -38,18 +43,40 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/api/maint/${action.endpoint}`, {
|
if (action.clientSideHandler) {
|
||||||
method: 'POST',
|
// Client-side action — OTP verified by calling a dummy maint endpoint first
|
||||||
headers: { 'Content-Type': 'application/json', 'x-maint-otp': otp },
|
const otpRes = await fetch(`${API_URL}/api/maint/force-ready`, {
|
||||||
});
|
method: 'POST',
|
||||||
const data = await res.json();
|
headers: { 'Content-Type': 'application/json', 'x-maint-otp': otp },
|
||||||
if (res.ok) {
|
});
|
||||||
console.log(`[MAINT] ${action.label}:`, data);
|
if (!otpRes.ok) {
|
||||||
notify.success(action.label, data.message ?? 'Completed successfully');
|
setError('Invalid maintenance code');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await action.clientSideHandler(preStepPayload);
|
||||||
|
notify.success(action.label, result.message ?? 'Completed');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setOtp('');
|
setOtp('');
|
||||||
} else {
|
} else {
|
||||||
setError(data.message ?? 'Failed');
|
// Standard sidecar endpoint
|
||||||
|
const res = await fetch(`${API_URL}/api/maint/${action.endpoint}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-maint-otp': otp,
|
||||||
|
},
|
||||||
|
...(preStepPayload ? { body: JSON.stringify(preStepPayload) } : {}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
console.log(`[MAINT] ${action.label}:`, data);
|
||||||
|
notify.success(action.label, data.message ?? 'Completed successfully');
|
||||||
|
onOpenChange(false);
|
||||||
|
setOtp('');
|
||||||
|
} else {
|
||||||
|
setError(data.message ?? 'Failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('Request failed');
|
setError('Request failed');
|
||||||
@@ -71,6 +98,8 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
|
|||||||
|
|
||||||
if (!action) return null;
|
if (!action) return null;
|
||||||
|
|
||||||
|
const showOtp = !action.needsPreStep || preStepReady;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
|
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
|
||||||
<Modal className="sm:max-w-[400px]">
|
<Modal className="sm:max-w-[400px]">
|
||||||
@@ -86,31 +115,40 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pin Input */}
|
{/* Pre-step content (e.g., campaign selection) */}
|
||||||
<div className="flex flex-col items-center gap-2 px-6 pb-5">
|
{action.needsPreStep && preStepContent && (
|
||||||
<PinInput size="sm">
|
<div className="px-6 pb-4">
|
||||||
<PinInput.Label>Enter maintenance code</PinInput.Label>
|
{preStepContent}
|
||||||
<PinInput.Group
|
</div>
|
||||||
maxLength={6}
|
)}
|
||||||
pattern={REGEXP_ONLY_DIGITS}
|
|
||||||
value={otp}
|
{/* Pin Input — shown when pre-step is ready (or no pre-step needed) */}
|
||||||
onChange={handleOtpChange}
|
{showOtp && (
|
||||||
onComplete={handleSubmit}
|
<div className="flex flex-col items-center gap-2 px-6 pb-5">
|
||||||
containerClassName="flex flex-row gap-2 h-14"
|
<PinInput size="sm">
|
||||||
>
|
<PinInput.Label>Enter maintenance code</PinInput.Label>
|
||||||
<PinInput.Slot index={0} className="!size-12 !text-display-sm" />
|
<PinInput.Group
|
||||||
<PinInput.Slot index={1} className="!size-12 !text-display-sm" />
|
maxLength={6}
|
||||||
<PinInput.Slot index={2} className="!size-12 !text-display-sm" />
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
<PinInput.Separator />
|
value={otp}
|
||||||
<PinInput.Slot index={3} className="!size-12 !text-display-sm" />
|
onChange={handleOtpChange}
|
||||||
<PinInput.Slot index={4} className="!size-12 !text-display-sm" />
|
onComplete={handleSubmit}
|
||||||
<PinInput.Slot index={5} className="!size-12 !text-display-sm" />
|
containerClassName="flex flex-row gap-2 h-14"
|
||||||
</PinInput.Group>
|
>
|
||||||
</PinInput>
|
<PinInput.Slot index={0} className="!size-12 !text-display-sm" />
|
||||||
{error && (
|
<PinInput.Slot index={1} className="!size-12 !text-display-sm" />
|
||||||
<p className="text-sm text-error-primary mt-1">{error}</p>
|
<PinInput.Slot index={2} className="!size-12 !text-display-sm" />
|
||||||
)}
|
<PinInput.Separator />
|
||||||
</div>
|
<PinInput.Slot index={3} className="!size-12 !text-display-sm" />
|
||||||
|
<PinInput.Slot index={4} className="!size-12 !text-display-sm" />
|
||||||
|
<PinInput.Slot index={5} className="!size-12 !text-display-sm" />
|
||||||
|
</PinInput.Group>
|
||||||
|
</PinInput>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-error-primary mt-1">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center gap-3 border-t border-secondary px-6 py-4">
|
<div className="flex items-center gap-3 border-t border-secondary px-6 py-4">
|
||||||
@@ -120,7 +158,7 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
|
|||||||
<Button
|
<Button
|
||||||
size="md"
|
size="md"
|
||||||
color="primary"
|
color="primary"
|
||||||
isDisabled={otp.length < 6 || loading}
|
isDisabled={otp.length < 6 || loading || (action.needsPreStep && !preStepReady)}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type MaintAction = {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
needsPreStep?: boolean;
|
||||||
|
clientSideHandler?: (payload: any) => Promise<{ status: string; message: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAINT_ACTIONS: Record<string, MaintAction> = {
|
const MAINT_ACTIONS: Record<string, MaintAction> = {
|
||||||
@@ -28,9 +30,10 @@ const MAINT_ACTIONS: Record<string, MaintAction> = {
|
|||||||
description: 'Correct call timestamps that were stored with IST double-offset.',
|
description: 'Correct call timestamps that were stored with IST double-offset.',
|
||||||
},
|
},
|
||||||
clearCampaignLeads: {
|
clearCampaignLeads: {
|
||||||
endpoint: '__client__clear-campaign-leads',
|
endpoint: 'clear-campaign-leads',
|
||||||
label: 'Clear Campaign Leads',
|
label: 'Clear Campaign Leads',
|
||||||
description: 'Delete all imported leads from a selected campaign. For testing only.',
|
description: 'Delete all imported leads from a selected campaign. For testing only.',
|
||||||
|
needsPreStep: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user