'use client'; import { useState, useEffect, useCallback } from 'react'; import { createClient } from '@/lib/supabase/client'; import { addContactTag, deleteContactTag } from '@/lib/contacts/tag-api'; import { useAuth } from '@/hooks/use-auth'; import { formatCurrency } from '@/lib/currency'; import { toast } from 'sonner'; import type { Contact, Tag, ContactTag, ContactNote, CustomField, ContactCustomValue, Deal, MessageTemplate } from '@/types'; import { TemplatePicker, type TemplateSendValues, } from '@/components/inbox/template-picker'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@/components/ui/sheet'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; 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 { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Phone, Mail, Building2, Copy, Check, Loader2, Plus, Trash2, Save, X, DollarSign, LayoutTemplate, } from 'lucide-react'; import { useTranslations } from 'next-intl'; interface ContactDetailViewProps { open: boolean; onOpenChange: (open: boolean) => void; contactId: string | null; onUpdated: () => void; } export function ContactDetailView({ open, onOpenChange, contactId, onUpdated, }: ContactDetailViewProps) { const t = useTranslations('Contacts.detailView'); const supabase = createClient(); const { accountId, defaultCurrency } = useAuth(); const [contact, setContact] = useState(null); const [loading, setLoading] = useState(false); const [copiedPhone, setCopiedPhone] = useState(false); // Send template — lets the business initiate (or re-open) a conversation // with this contact by sending an approved template. The send route // find-or-creates the conversation, so no inbound message is required. const [templatePickerOpen, setTemplatePickerOpen] = useState(false); const [sendingTemplate, setSendingTemplate] = useState(false); // Details tab const [editName, setEditName] = useState(''); const [editPhone, setEditPhone] = useState(''); const [editEmail, setEditEmail] = useState(''); const [editCompany, setEditCompany] = useState(''); const [savingDetails, setSavingDetails] = useState(false); // Tags tab const [allTags, setAllTags] = useState([]); const [contactTagIds, setContactTagIds] = useState([]); const [savingTags, setSavingTags] = useState(false); // Notes tab const [notes, setNotes] = useState([]); const [newNote, setNewNote] = useState(''); const [savingNote, setSavingNote] = useState(false); const [loadingNotes, setLoadingNotes] = useState(false); // Custom fields tab const [customFields, setCustomFields] = useState([]); const [customValues, setCustomValues] = useState>({}); const [savingCustom, setSavingCustom] = useState(false); const [loadingCustom, setLoadingCustom] = useState(false); // Deals tab const [deals, setDeals] = useState([]); const [loadingDeals, setLoadingDeals] = useState(false); const fetchContact = useCallback(async () => { if (!contactId) return; setLoading(true); const { data } = await supabase .from('contacts') .select('*') .eq('id', contactId) .single(); if (data) { setContact(data); setEditName(data.name ?? ''); setEditPhone(data.phone); setEditEmail(data.email ?? ''); setEditCompany(data.company ?? ''); } setLoading(false); }, [contactId, supabase]); const fetchTags = useCallback(async () => { if (!contactId) return; const [tagsRes, contactTagsRes] = await Promise.all([ supabase.from('tags').select('*').order('name'), supabase.from('contact_tags').select('tag_id').eq('contact_id', contactId), ]); if (tagsRes.data) setAllTags(tagsRes.data); if (contactTagsRes.data) { setContactTagIds(contactTagsRes.data.map((ct) => ct.tag_id)); } }, [contactId, supabase]); const fetchNotes = useCallback(async () => { if (!contactId) return; setLoadingNotes(true); const { data } = await supabase .from('contact_notes') .select('*') .eq('contact_id', contactId) .order('created_at', { ascending: false }); if (data) setNotes(data); setLoadingNotes(false); }, [contactId, supabase]); const fetchCustomFields = useCallback(async () => { if (!contactId) return; setLoadingCustom(true); const [fieldsRes, valuesRes] = await Promise.all([ supabase.from('custom_fields').select('*').order('field_name'), supabase .from('contact_custom_values') .select('*') .eq('contact_id', contactId), ]); if (fieldsRes.data) setCustomFields(fieldsRes.data); if (valuesRes.data) { const map: Record = {}; valuesRes.data.forEach((v) => { map[v.custom_field_id] = v.value ?? ''; }); setCustomValues(map); } setLoadingCustom(false); }, [contactId, supabase]); const fetchDeals = useCallback(async () => { if (!contactId) return; setLoadingDeals(true); const { data } = await supabase .from('deals') .select('*, stage:pipeline_stages(*)') .eq('contact_id', contactId) .order('created_at', { ascending: false }); setDeals((data ?? []) as Deal[]); setLoadingDeals(false); }, [contactId, supabase]); useEffect(() => { if (open && contactId) { fetchContact(); fetchTags(); fetchNotes(); fetchCustomFields(); fetchDeals(); } }, [open, contactId, fetchContact, fetchTags, fetchNotes, fetchCustomFields, fetchDeals]); async function copyPhone() { if (!contact) return; await navigator.clipboard.writeText(contact.phone); setCopiedPhone(true); setTimeout(() => setCopiedPhone(false), 2000); } async function saveDetails() { if (!contactId || !editPhone.trim()) { toast.error(t('toastPhoneRequired')); return; } setSavingDetails(true); const { error } = await supabase .from('contacts') .update({ name: editName.trim() || null, phone: editPhone.trim(), email: editEmail.trim() || null, company: editCompany.trim() || null, updated_at: new Date().toISOString(), }) .eq('id', contactId); if (error) { toast.error(t('toastUpdateFailed')); } else { toast.success(t('toastUpdated')); fetchContact(); onUpdated(); } setSavingDetails(false); } async function toggleTag(tagId: string) { if (!contactId) return; setSavingTags(true); const isSelected = contactTagIds.includes(tagId); try { if (isSelected) { await deleteContactTag(contactId, tagId); setContactTagIds((prev) => prev.filter((id) => id !== tagId)); } else { await addContactTag(contactId, tagId); setContactTagIds((prev) => [...prev, tagId]); } onUpdated(); } catch (error) { toast.error(error instanceof Error ? error.message : t('toastUpdateFailed')); } setSavingTags(false); } async function addNote() { if (!contactId || !newNote.trim()) return; setSavingNote(true); const { data: { session }, } = await supabase.auth.getSession(); const user = session?.user; if (!user || !accountId) { toast.error(t('toastNotAuthenticated')); setSavingNote(false); return; } const { error } = await supabase.from('contact_notes').insert({ contact_id: contactId, account_id: accountId, user_id: user.id, note_text: newNote.trim(), }); if (error) { toast.error(t('toastNoteAddFailed')); } else { setNewNote(''); fetchNotes(); toast.success(t('toastNoteAdded')); } setSavingNote(false); } async function deleteNote(noteId: string) { const { error } = await supabase .from('contact_notes') .delete() .eq('id', noteId); if (error) { toast.error(t('toastNoteDeleteFailed')); } else { setNotes((prev) => prev.filter((n) => n.id !== noteId)); toast.success(t('toastNoteDeleted')); } } async function saveCustomFields() { if (!contactId) return; setSavingCustom(true); try { // Delete existing values and re-insert await supabase .from('contact_custom_values') .delete() .eq('contact_id', contactId); const rows = Object.entries(customValues) .filter(([, val]) => val.trim()) .map(([fieldId, val]) => ({ contact_id: contactId, custom_field_id: fieldId, value: val.trim(), })); if (rows.length > 0) { const { error } = await supabase .from('contact_custom_values') .insert(rows); if (error) throw error; } toast.success(t('toastCustomFieldsSaved')); } catch { toast.error(t('toastCustomFieldsFailed')); } setSavingCustom(false); } async function handleSendTemplate( template: MessageTemplate, values: TemplateSendValues, ) { if (!contactId) return; setSendingTemplate(true); try { const res = await fetch('/api/whatsapp/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ // No conversation_id — the route find-or-creates one for this // contact, mirroring the inbox template-send payload otherwise. contact_id: contactId, message_type: 'template', template_name: template.name, template_language: template.language, template_message_params: { body: values.body, headerText: values.headerText, buttonParams: values.buttonParams, }, template_params: values.body, }), }); const payload = await res.json().catch(() => ({})); if (!res.ok) { const reason = payload?.error || `HTTP ${res.status}`; toast.error(t('toastTemplateFailed', { reason })); return; } toast.success(t('toastTemplateSent', { name: template.name })); } catch (err) { const reason = err instanceof Error ? err.message : 'network error'; toast.error(`Failed to send template: ${reason}`); } finally { setSendingTemplate(false); } } function getInitials(name?: string | null) { if (!name) return '?'; return name .split(' ') .map((w) => w[0]) .join('') .toUpperCase() .slice(0, 2); } return ( <> {loading || !contact ? (
) : (
{/* Header */}
{getInitials(contact.name)}
{contact.name || t('unnamed')} {t('contactDetailsDesc')}
{contact.email && ( {contact.email} )} {contact.company && ( {contact.company} )}
{/* Tabs */} {t('tabs.details')} {t('tabs.tags', { fallback: 'Tags' })} {t('tabs.notes')} {t('tabs.custom')} {t('tabs.deals')} {/* Details Tab */}
setEditName(e.target.value)} className="bg-muted border-border text-foreground h-8 text-sm" />
setEditPhone(e.target.value)} className="bg-muted border-border text-foreground h-8 text-sm" />
setEditEmail(e.target.value)} className="bg-muted border-border text-foreground h-8 text-sm" />
setEditCompany(e.target.value)} className="bg-muted border-border text-foreground h-8 text-sm" />
{/* Tags Tab */}

{t('tagsTab.clickTagDesc')}

{allTags.length === 0 ? (

{t('tagsTab.noTagsAvailable')}

) : (
{allTags.map((tag) => { const selected = contactTagIds.includes(tag.id); return ( ); })}
)}
{/* Notes Tab */}