'use client'; import { useState, useEffect } from 'react'; import { createClient } from '@/lib/supabase/client'; import { useAuth } from '@/hooks/use-auth'; import { toast } from 'sonner'; import type { Contact, Tag, ContactTag } from '@/types'; import { findExistingContact, isExactMatch, isUniqueViolation, type ExistingContact, } from '@/lib/contacts/dedupe'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Loader2, AlertTriangle } from 'lucide-react'; interface ContactFormProps { open: boolean; onOpenChange: (open: boolean) => void; contact?: Contact | null; contactTags?: ContactTag[]; onSaved: () => void; /** Open an existing contact's detail view — used by the duplicate * notice to jump to the contact that already owns this number. */ onViewExisting?: (contactId: string) => void; } export function ContactForm({ open, onOpenChange, contact, contactTags = [], onSaved, onViewExisting, }: ContactFormProps) { const supabase = createClient(); const { accountId } = useAuth(); const isEdit = !!contact; const [name, setName] = useState(''); const [phone, setPhone] = useState(''); const [email, setEmail] = useState(''); const [company, setCompany] = useState(''); const [saving, setSaving] = useState(false); // Duplicate-phone detection for NEW contacts. `exact` (same digits) // hard-blocks the save; a fuzzy trunk-variant match only warns. The // DB unique index (migration 022) is the real backstop — this is the // friendly heads-up before we get there. const [dupMatch, setDupMatch] = useState< { contact: ExistingContact; exact: boolean } | null >(null); const [checkingDup, setCheckingDup] = useState(false); const [tags, setTags] = useState([]); const [selectedTagIds, setSelectedTagIds] = useState([]); const [loadingTags, setLoadingTags] = useState(false); useEffect(() => { if (open) { setName(contact?.name ?? ''); setPhone(contact?.phone ?? ''); setEmail(contact?.email ?? ''); setCompany(contact?.company ?? ''); setSelectedTagIds(contactTags.map((ct) => ct.tag_id)); setDupMatch(null); fetchTags(); } }, [open, contact]); // Look up an existing contact with this number (new contacts only). // Runs on blur so we don't query on every keystroke. async function checkDuplicate() { if (isEdit || !accountId) return; const value = phone.trim(); if (!value) { setDupMatch(null); return; } setCheckingDup(true); try { const existing = await findExistingContact(supabase, accountId, value); setDupMatch( existing ? { contact: existing, exact: isExactMatch(existing, value) } : null, ); } finally { setCheckingDup(false); } } async function fetchTags() { setLoadingTags(true); const { data } = await supabase .from('tags') .select('*') .order('name'); if (data) setTags(data); setLoadingTags(false); } function toggleTag(tagId: string) { setSelectedTagIds((prev) => prev.includes(tagId) ? prev.filter((id) => id !== tagId) : [...prev, tagId] ); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!phone.trim()) { toast.error('Phone number is required'); return; } // Hard-block an exact duplicate on create (the DB unique index is // the real backstop; this avoids a round-trip + a raw error toast). if (!isEdit && dupMatch?.exact) { toast.error('A contact with this phone number already exists'); return; } setSaving(true); try { const { data: { session }, } = await supabase.auth.getSession(); const user = session?.user; if (!user) throw new Error('Not authenticated'); if (!accountId) throw new Error('Your profile is not linked to an account.'); let contactId = contact?.id; if (isEdit && contactId) { const { error } = await supabase .from('contacts') .update({ name: name.trim() || null, phone: phone.trim(), email: email.trim() || null, company: company.trim() || null, updated_at: new Date().toISOString(), }) .eq('id', contactId); if (error) throw error; } else { const { data, error } = await supabase .from('contacts') .insert({ user_id: user.id, account_id: accountId, name: name.trim() || null, phone: phone.trim(), email: email.trim() || null, company: company.trim() || null, }) .select('id') .single(); if (error) throw error; contactId = data.id; } // Sync tags if (contactId) { await supabase .from('contact_tags') .delete() .eq('contact_id', contactId); if (selectedTagIds.length > 0) { const tagRows = selectedTagIds.map((tag_id) => ({ contact_id: contactId!, tag_id, })); const { error: tagError } = await supabase .from('contact_tags') .insert(tagRows); if (tagError) throw tagError; } } toast.success(isEdit ? 'Contact updated' : 'Contact created'); onOpenChange(false); onSaved(); } catch (err: unknown) { // The unique index (migration 022) rejects a duplicate phone that // slipped past the on-blur check (race, or a format that // normalizes equal). Surface it as the friendly duplicate notice // and, for new contacts, point the user at the existing record. if (isUniqueViolation(err)) { toast.error('A contact with this phone number already exists'); if (!isEdit && accountId) { const existing = await findExistingContact( supabase, accountId, phone.trim(), ); if (existing) setDupMatch({ contact: existing, exact: true }); } return; } const message = err instanceof Error ? err.message : 'Failed to save contact'; toast.error(message); } finally { setSaving(false); } } return ( {isEdit ? 'Edit Contact' : 'Add Contact'} {isEdit ? 'Update the contact details below.' : 'Fill in the details to create a new contact.'}
setName(e.target.value)} placeholder="John Doe" className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" />
{ setPhone(e.target.value); if (dupMatch) setDupMatch(null); }} onBlur={checkDuplicate} placeholder="+1 234 567 8900" className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" /> {dupMatch ? (

{dupMatch.exact ? 'A contact with this phone number already exists.' : 'A contact with a very similar number already exists.'}

{onViewExisting && ( )}
) : (

Include country code, e.g. +1 for US

)}
setEmail(e.target.value)} placeholder="john@example.com" className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" />
setCompany(e.target.value)} placeholder="Acme Inc." className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500" />
{loadingTags ? (
Loading tags...
) : tags.length === 0 ? (

No tags available. Create tags in Settings.

) : (
{tags.map((tag) => { const selected = selectedTagIds.includes(tag.id); return ( ); })}
)}
); }