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:
2026-03-31 14:24:44 +05:30
parent 0295790c9a
commit 33fedf7082
5 changed files with 156 additions and 166 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
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';
@@ -19,15 +19,20 @@ type MaintAction = {
endpoint: string;
label: string;
description: string;
needsPreStep?: boolean;
clientSideHandler?: (payload: any) => Promise<{ status: string; message: string }>;
};
type MaintOtpModalProps = {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
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 [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -38,18 +43,40 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
setError(null);
try {
const res = await fetch(`${API_URL}/api/maint/${action.endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-maint-otp': otp },
});
const data = await res.json();
if (res.ok) {
console.log(`[MAINT] ${action.label}:`, data);
notify.success(action.label, data.message ?? 'Completed successfully');
if (action.clientSideHandler) {
// Client-side action — OTP verified by calling a dummy maint endpoint first
const otpRes = await fetch(`${API_URL}/api/maint/force-ready`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-maint-otp': otp },
});
if (!otpRes.ok) {
setError('Invalid maintenance code');
setLoading(false);
return;
}
const result = await action.clientSideHandler(preStepPayload);
notify.success(action.label, result.message ?? 'Completed');
onOpenChange(false);
setOtp('');
} 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 {
setError('Request failed');
@@ -71,6 +98,8 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
if (!action) return null;
const showOtp = !action.needsPreStep || preStepReady;
return (
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
<Modal className="sm:max-w-[400px]">
@@ -86,31 +115,40 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action }: MaintOtpModalPro
</div>
</div>
{/* Pin Input */}
<div className="flex flex-col items-center gap-2 px-6 pb-5">
<PinInput size="sm">
<PinInput.Label>Enter maintenance code</PinInput.Label>
<PinInput.Group
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
value={otp}
onChange={handleOtpChange}
onComplete={handleSubmit}
containerClassName="flex flex-row gap-2 h-14"
>
<PinInput.Slot index={0} className="!size-12 !text-display-sm" />
<PinInput.Slot index={1} className="!size-12 !text-display-sm" />
<PinInput.Slot index={2} className="!size-12 !text-display-sm" />
<PinInput.Separator />
<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>
{/* Pre-step content (e.g., campaign selection) */}
{action.needsPreStep && preStepContent && (
<div className="px-6 pb-4">
{preStepContent}
</div>
)}
{/* Pin Input — shown when pre-step is ready (or no pre-step needed) */}
{showOtp && (
<div className="flex flex-col items-center gap-2 px-6 pb-5">
<PinInput size="sm">
<PinInput.Label>Enter maintenance code</PinInput.Label>
<PinInput.Group
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
value={otp}
onChange={handleOtpChange}
onComplete={handleSubmit}
containerClassName="flex flex-row gap-2 h-14"
>
<PinInput.Slot index={0} className="!size-12 !text-display-sm" />
<PinInput.Slot index={1} className="!size-12 !text-display-sm" />
<PinInput.Slot index={2} className="!size-12 !text-display-sm" />
<PinInput.Separator />
<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 */}
<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
size="md"
color="primary"
isDisabled={otp.length < 6 || loading}
isDisabled={otp.length < 6 || loading || (action.needsPreStep && !preStepReady)}
isLoading={loading}
onClick={handleSubmit}
className="flex-1"