'use client'; import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, Save } from 'lucide-react'; import { toast } from 'sonner'; import { PageHeader } from '@/components/shared/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; type Mode = 'auto' | 'suggest' | 'off'; const TRIGGERS: Array<{ key: string; label: string; description: string; defaultMode: Mode; }> = [ { key: 'eoi_sent', label: 'EOI sent', description: 'Rep generates an EOI for signing - moves the deal to "EOI" stage.', defaultMode: 'auto', }, { key: 'eoi_signed', label: 'EOI signed (all parties)', description: 'All signatories complete the EOI - moves the deal to "Reservation" stage. Conventional CRM behaviour.', defaultMode: 'auto', }, { key: 'reservation_signed', label: 'Reservation agreement signed', description: 'Reservation paperwork signed by all parties - keeps the deal at "Reservation" with sub-status signed.', defaultMode: 'auto', }, { key: 'deposit_received', label: 'Deposit received in full', description: 'Deposit total reaches the expected amount - moves the deal to "Deposit Paid" stage.', defaultMode: 'auto', }, { key: 'contract_signed', label: 'Sales contract signed', description: 'Final contract signed by all parties - moves the deal to "Contract" stage.', defaultMode: 'auto', }, ]; const PRESETS = { aggressive: 'auto', conservative: 'suggest', } as const; type PresetName = keyof typeof PRESETS; export default function PipelineRulesPage() { const queryClient = useQueryClient(); const [rules, setRules] = useState>(() => Object.fromEntries(TRIGGERS.map((t) => [t.key, t.defaultMode])), ); const { data, isLoading } = useQuery<{ data: { values: Record | null }> }; }>({ queryKey: ['admin', 'settings', 'pipeline.auto_advance'], queryFn: () => apiFetch<{ data: { values: Record | null }> }; }>('/api/v1/admin/settings/resolved?sections=pipeline.auto_advance'), }); // Hydrate the local form once the server-side state arrives. We treat // missing keys as the registered default — the page's persisted JSON // doesn't have to enumerate every trigger, just the overrides. useEffect(() => { const persisted = data?.data?.values?.stage_advance_rules?.value; if (!persisted || typeof persisted !== 'object') return; // eslint-disable-next-line react-hooks/set-state-in-effect setRules((prev) => { const next = { ...prev }; for (const t of TRIGGERS) { const v = persisted[t.key]; if (v === 'auto' || v === 'suggest' || v === 'off') next[t.key] = v; } return next; }); }, [data]); const saveMutation = useMutation({ mutationFn: () => apiFetch('/api/v1/admin/settings/stage_advance_rules', { method: 'PUT', body: { value: rules }, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] }); toast.success('Pipeline rules saved.'); }, onError: (err) => toastError(err), }); const applyPreset = (preset: PresetName) => { const target = PRESETS[preset]; setRules(Object.fromEntries(TRIGGERS.map((t) => [t.key, target]))); }; const setMode = (key: string, mode: Mode) => { setRules((prev) => ({ ...prev, [key]: mode })); }; const allMatch = (mode: Mode) => TRIGGERS.every((t) => rules[t.key] === mode); const currentPreset: PresetName | 'custom' = allMatch('auto') ? 'aggressive' : allMatch('suggest') ? 'conservative' : 'custom'; return (
Preset
applyPreset('aggressive')} /> applyPreset('conservative')} />

Custom

Mix and match - the per-trigger toggles below override the preset.

Per-trigger settings {isLoading ? (
Loading…
) : ( TRIGGERS.map((t) => (

{t.label}

{t.description}

)) )}
); } function PresetButton({ name, label, description, active, onClick, }: { name: PresetName; label: string; description: string; active: boolean; onClick: () => void; }) { return ( ); }