import { useEffect, useState, type FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { extractApiError } from '@/api/client'; import type { EngagementInput, EngagementStatus } from '@/api/types'; import { useCreateEngagement, useEngagement, usePatchEngagement, } from '@/hooks/useEngagements'; import { useToast } from '@/hooks/useToast'; import { FormField, Select, TextArea, TextInput } from '@/components/FormField'; import { LoadingState } from '@/components/LoadingState'; import { ErrorState } from '@/components/ErrorState'; const STATUS_OPTIONS: { value: EngagementStatus; label: string }[] = [ { value: 'planned', label: 'Planned' }, { value: 'active', label: 'Active' }, { value: 'closed', label: 'Closed' }, ]; interface FormState { name: string; description: string; start_date: string; end_date: string; status: EngagementStatus; } const EMPTY: FormState = { name: '', description: '', start_date: '', end_date: '', status: 'planned', }; function validate(state: FormState): Partial> { const errors: Partial> = {}; if (!state.name.trim()) errors.name = 'Name is required'; if (!state.start_date) errors.start_date = 'Start date is required'; if (state.end_date && state.start_date && state.end_date < state.start_date) { errors.end_date = 'End date must be on or after start date'; } return errors; } export function EngagementFormPage(): JSX.Element { const { id } = useParams<{ id: string }>(); const editing = Boolean(id); const numericId = id ? Number(id) : undefined; const navigate = useNavigate(); const { push } = useToast(); const detail = useEngagement(editing ? numericId : undefined); const createMutation = useCreateEngagement(); const patchMutation = usePatchEngagement(numericId ?? 0); const [form, setForm] = useState(EMPTY); const [errors, setErrors] = useState>>({}); const [submitError, setSubmitError] = useState(null); // Hydrate edit form when data arrives. useEffect(() => { if (editing && detail.data) { setForm({ name: detail.data.name, description: detail.data.description ?? '', start_date: detail.data.start_date, end_date: detail.data.end_date ?? '', status: detail.data.status, }); } }, [editing, detail.data]); if (editing && detail.isLoading) return ; if (editing && detail.isError) { return ( detail.refetch()} /> ); } const onSubmit = async (e: FormEvent) => { e.preventDefault(); setSubmitError(null); const v = validate(form); setErrors(v); if (Object.keys(v).length > 0) return; const payload: EngagementInput = { name: form.name.trim(), start_date: form.start_date, status: form.status, }; if (form.description.trim()) payload.description = form.description.trim(); // PATCH with null clears end_date; POST with omitted leaves it null if (editing) { // Always include end_date for edit: '' → null to clear, otherwise value payload.end_date = form.end_date === '' ? null : form.end_date; } else if (form.end_date) { payload.end_date = form.end_date; } try { if (editing && numericId) { await patchMutation.mutateAsync(payload); push('Engagement updated', 'success'); navigate(`/engagements/${numericId}`); } else { const created = await createMutation.mutateAsync(payload); push('Engagement created', 'success'); navigate(`/engagements/${created.id}`); } } catch (err) { setSubmitError(extractApiError(err, 'Could not save engagement')); } }; const submitting = createMutation.isPending || patchMutation.isPending; return (

{editing ? 'Edit engagement' : 'New engagement'}

{editing ? 'Update the engagement metadata.' : 'Create a new red team mission to host simulations.'}

setForm({ ...form, name: e.target.value })} required />