"use client"; import { useEffect, useState } from "react"; import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { createClient } from "@/lib/supabase/client"; import type { Pipeline, PipelineStage } from "@/types"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Trash2, Plus, GripVertical, AlertTriangle, } from "lucide-react"; import { toast } from "sonner"; const STAGE_COLORS = [ "#3b82f6", "#6366f1", "#8b5cf6", "#ec4899", "#f43f5e", "#f97316", "#eab308", "#22c55e", "#14b8a6", "#06b6d4", ]; interface PipelineSettingsProps { open: boolean; onOpenChange: (open: boolean) => void; pipeline: Pipeline; stages: PipelineStage[]; onPipelinesChanged: () => void; onStagesChanged: () => void; onCreateNewPipeline: () => void; } export function PipelineSettings({ open, onOpenChange, pipeline, stages, onPipelinesChanged, onStagesChanged, onCreateNewPipeline, }: PipelineSettingsProps) { const supabase = createClient(); const [name, setName] = useState(pipeline.name); const [localStages, setLocalStages] = useState(stages); const [newStageName, setNewStageName] = useState(""); const [newStageColor, setNewStageColor] = useState(STAGE_COLORS[0]); const [saving, setSaving] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleting, setDeleting] = useState(false); // Reset form state when the dialog opens or its prop inputs change // — legitimate prop-driven sync. /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!open) return; setName(pipeline.name); setLocalStages([...stages].sort((a, b) => a.position - b.position)); setShowDeleteConfirm(false); }, [open, pipeline, stages]); /* eslint-enable react-hooks/set-state-in-effect */ const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), ); function handleReorder(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = localStages.findIndex((s) => s.id === active.id); const newIndex = localStages.findIndex((s) => s.id === over.id); if (oldIndex < 0 || newIndex < 0) return; setLocalStages(arrayMove(localStages, oldIndex, newIndex)); } async function handleSave() { setSaving(true); // One upsert for all stages — batches N stage writes into a single // round-trip. Previous implementation did N sequential UPDATEs which // latency-scaled linearly with stage count. const stageRows = localStages.map((s, i) => ({ id: s.id, pipeline_id: s.pipeline_id, name: s.name, color: s.color, position: i, })); const [renameRes, stagesRes] = await Promise.all([ supabase .from("pipelines") .update({ name: name.trim() }) .eq("id", pipeline.id), supabase.from("pipeline_stages").upsert(stageRows, { onConflict: "id" }), ]); setSaving(false); if (renameRes.error || stagesRes.error) { toast.error("Failed to save pipeline"); return; } onOpenChange(false); onPipelinesChanged(); onStagesChanged(); toast.success("Pipeline saved"); } async function handleAddStage() { const trimmed = newStageName.trim(); if (!trimmed) return; const { data, error } = await supabase .from("pipeline_stages") .insert({ pipeline_id: pipeline.id, name: trimmed, color: newStageColor, position: localStages.length, }) .select() .single(); if (error || !data) { toast.error("Failed to add stage"); return; } setLocalStages([...localStages, data as PipelineStage]); setNewStageName(""); setNewStageColor(STAGE_COLORS[(localStages.length + 1) % STAGE_COLORS.length]); } async function handleRemoveStage(stageId: string) { // Refuse to delete if deals still reference the stage (FK would fail). const { count } = await supabase .from("deals") .select("id", { count: "exact", head: true }) .eq("stage_id", stageId); if (count && count > 0) { toast.error("Move or delete deals in this stage first"); return; } const { error } = await supabase .from("pipeline_stages") .delete() .eq("id", stageId); if (error) { toast.error("Failed to delete stage"); return; } setLocalStages(localStages.filter((s) => s.id !== stageId)); } async function handleDeletePipeline() { setDeleting(true); // ON DELETE CASCADE handles deals + stages. const { error } = await supabase .from("pipelines") .delete() .eq("id", pipeline.id); setDeleting(false); if (error) { toast.error("Failed to delete pipeline"); return; } onOpenChange(false); onPipelinesChanged(); toast.success("Pipeline deleted"); } return ( Manage Pipeline {showDeleteConfirm ? (

Delete Pipeline

This will archive all deals in this pipeline. This cannot be undone.

) : ( <>
setName(e.target.value)} className="border-slate-700 bg-slate-800 text-white" />
s.id)} strategy={verticalListSortingStrategy} >
{localStages.map((stage, index) => ( { const updated = [...localStages]; updated[index] = { ...updated[index], name: v }; setLocalStages(updated); }} onColorChange={(v) => { const updated = [...localStages]; updated[index] = { ...updated[index], color: v }; setLocalStages(updated); }} onRemove={() => handleRemoveStage(stage.id)} colors={STAGE_COLORS} /> ))}
{/* Add new stage */}
{STAGE_COLORS.map((color) => (
setNewStageName(e.target.value)} placeholder="New stage name" className="border-slate-700 bg-slate-800 text-sm text-white" onKeyDown={(e) => { if (e.key === "Enter") handleAddStage(); }} />
)}
); } function SortableStageRow({ stage, onNameChange, onColorChange, onRemove, colors, }: { stage: PipelineStage; onNameChange: (v: string) => void; onColorChange: (v: string) => void; onRemove: () => void; colors: string[]; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: stage.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
onNameChange(e.target.value)} className="h-7 flex-1 border-transparent bg-transparent text-sm text-white focus:border-slate-600" />
); } function ColorSwatch({ value, onChange, colors, }: { value: string; onChange: (v: string) => void; colors: string[]; }) { const [open, setOpen] = useState(false); return (
)} ); }