mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
- 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>
176 lines
7.9 KiB
TypeScript
176 lines
7.9 KiB
TypeScript
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 } from '@fortawesome/pro-duotone-svg-icons';
|
|
import type { FC } from 'react';
|
|
import { notify } from '@/lib/toast';
|
|
|
|
const ShieldIcon: FC<{ className?: string }> = ({ className }) => (
|
|
<FontAwesomeIcon icon={faShieldKeyhole} className={className} />
|
|
);
|
|
|
|
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
|
|
|
|
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, preStepContent, preStepPayload, preStepReady = true }: MaintOtpModalProps) => {
|
|
const [otp, setOtp] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async () => {
|
|
if (!action || otp.length < 6) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
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 {
|
|
// 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');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOtpChange = (value: string) => {
|
|
setOtp(value);
|
|
setError(null);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
onOpenChange(false);
|
|
setOtp('');
|
|
setError(null);
|
|
};
|
|
|
|
if (!action) return null;
|
|
|
|
const showOtp = !action.needsPreStep || preStepReady;
|
|
|
|
return (
|
|
<ModalOverlay isOpen={isOpen} onOpenChange={handleClose} isDismissable>
|
|
<Modal className="sm:max-w-[400px]">
|
|
<Dialog>
|
|
{() => (
|
|
<div className="flex w-full flex-col rounded-xl bg-primary shadow-xl ring-1 ring-secondary overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex flex-col items-center gap-4 px-6 pt-6 pb-5">
|
|
<FeaturedIcon icon={ShieldIcon} color="brand" theme="light" size="md" />
|
|
<div className="flex flex-col items-center gap-1 text-center">
|
|
<h2 className="text-lg font-semibold text-primary">{action.label}</h2>
|
|
<p className="text-sm text-tertiary">{action.description}</p>
|
|
</div>
|
|
</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">
|
|
<Button size="md" color="secondary" onClick={handleClose} className="flex-1">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="md"
|
|
color="primary"
|
|
isDisabled={otp.length < 6 || loading || (action.needsPreStep && !preStepReady)}
|
|
isLoading={loading}
|
|
onClick={handleSubmit}
|
|
className="flex-1"
|
|
>
|
|
Confirm
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
</Modal>
|
|
</ModalOverlay>
|
|
);
|
|
};
|