"use client"; import { useState, useEffect } from "react"; import Link from "next/link"; import { createClient } from "@/lib/supabase/client"; import type { Contact, Conversation, Deal, DealStatus, PipelineStage, Profile, } from "@/types"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Check, X, Trash2, MessageSquare, DollarSign, Loader2, } from "lucide-react"; import { toast } from "sonner"; interface DealFormProps { open: boolean; onOpenChange: (open: boolean) => void; deal?: Deal | null; pipelineId: string; stages: PipelineStage[]; defaultStageId?: string; onSaved: () => void; } export function DealForm({ open, onOpenChange, deal, pipelineId, stages, defaultStageId, onSaved, }: DealFormProps) { const supabase = createClient(); const [title, setTitle] = useState(""); const [value, setValue] = useState(""); const [currency, setCurrency] = useState("USD"); const [contactId, setContactId] = useState(""); const [stageId, setStageId] = useState(""); const [assignedTo, setAssignedTo] = useState(""); const [expectedCloseDate, setExpectedCloseDate] = useState(""); const [notes, setNotes] = useState(""); const [contacts, setContacts] = useState([]); const [profiles, setProfiles] = useState([]); const [linkedConversation, setLinkedConversation] = useState(null); const [saving, setSaving] = useState(false); const [statusAction, setStatusAction] = useState(null); const [deleting, setDeleting] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); // Reset the form fields every time the sheet opens or its input // props change. This is a legitimate prop-driven sync; the rule is // over-cautious here, hence the block-level disable. /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!open) return; setConfirmDelete(false); if (deal) { setTitle(deal.title); setValue(String(deal.value ?? "")); setCurrency(deal.currency || "USD"); // contact_id is nullable when the contact has been deleted // (migration 004: ON DELETE SET NULL). "" means "no selection". setContactId(deal.contact_id ?? ""); setStageId(deal.stage_id); setAssignedTo(deal.assigned_to ?? ""); setExpectedCloseDate(deal.expected_close_date ?? ""); setNotes(deal.notes ?? ""); } else { setTitle(""); setValue(""); setCurrency("USD"); setContactId(""); setStageId(defaultStageId || stages[0]?.id || ""); setAssignedTo(""); setExpectedCloseDate(""); setNotes(""); } }, [open, deal, defaultStageId, stages]); /* eslint-enable react-hooks/set-state-in-effect */ // Load supporting data once the sheet is open useEffect(() => { if (!open) return; let cancelled = false; (async () => { const [c, p] = await Promise.all([ supabase.from("contacts").select("*").order("name"), supabase.from("profiles").select("*").order("full_name"), ]); if (cancelled) return; setContacts((c.data ?? []) as Contact[]); setProfiles((p.data ?? []) as Profile[]); })(); return () => { cancelled = true; }; }, [open, supabase]); // Fetch linked conversation for the selected contact (newest open one). // Clearing on no-selection is sync with prop state; the populated // case runs setLinkedConversation inside the async fetch callback. useEffect(() => { if (!open || !contactId) { // eslint-disable-next-line react-hooks/set-state-in-effect setLinkedConversation(null); return; } let cancelled = false; (async () => { const { data } = await supabase .from("conversations") .select("*") .eq("contact_id", contactId) .order("last_message_at", { ascending: false }) .limit(1) .maybeSingle(); if (cancelled) return; setLinkedConversation((data as Conversation | null) ?? null); })(); return () => { cancelled = true; }; }, [open, contactId, supabase]); async function handleSave() { if (!title.trim() || !contactId || !stageId) { toast.error("Title, contact, and stage are required"); return; } setSaving(true); const payload = { title: title.trim(), value: parseFloat(value) || 0, currency, contact_id: contactId, pipeline_id: pipelineId, stage_id: stageId, assigned_to: assignedTo || null, notes: notes.trim() || null, expected_close_date: expectedCloseDate || null, }; if (deal) { const { error } = await supabase .from("deals") .update(payload) .eq("id", deal.id); if (error) { toast.error("Failed to save deal"); setSaving(false); return; } } else { const { data: { session }, } = await supabase.auth.getSession(); const user = session?.user; if (!user) { toast.error("Not signed in"); setSaving(false); return; } const { error } = await supabase .from("deals") .insert({ ...payload, user_id: user.id, status: "open" }); if (error) { toast.error("Failed to create deal"); setSaving(false); return; } } setSaving(false); toast.success(deal ? "Deal updated" : "Deal created"); onOpenChange(false); onSaved(); } async function handleStatusChange(status: DealStatus) { if (!deal) return; setStatusAction(status); const { error } = await supabase .from("deals") .update({ status }) .eq("id", deal.id); setStatusAction(null); if (error) { toast.error("Failed to update deal status"); return; } toast.success( status === "won" ? "Marked as won" : status === "lost" ? "Marked as lost" : "Deal reopened", ); onOpenChange(false); onSaved(); } async function handleDelete() { if (!deal) return; setDeleting(true); const { error } = await supabase.from("deals").delete().eq("id", deal.id); setDeleting(false); if (error) { toast.error("Failed to delete deal"); return; } toast.success("Deal deleted"); setConfirmDelete(false); onOpenChange(false); onSaved(); } return (
{deal ? "Edit Deal" : "New Deal"}
setTitle(e.target.value)} placeholder="Deal title" className="border-slate-700 bg-slate-800 text-white" />
{linkedConversation && ( Link to Conversation )}
setValue(e.target.value)} placeholder="0" className="border-slate-700 bg-slate-800 pl-7 text-white" />
setExpectedCloseDate(e.target.value)} className="border-slate-700 bg-slate-800 text-white" />