'use client'; import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, Mail, MessageSquare, MoreHorizontal, Phone, Plus, Star, Trash2, } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { InlineEditableField } from '@/components/shared/inline-editable-field'; import { InlinePhoneField } from '@/components/shared/inline-phone-field'; import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input'; import { apiFetch } from '@/lib/api/client'; import { cn } from '@/lib/utils'; interface Contact { id: string; channel: string; value: string; valueE164?: string | null; valueCountry?: string | null; label?: string | null; isPrimary: boolean; } const CHANNEL_OPTIONS = [ { value: 'email', label: 'Email' }, { value: 'phone', label: 'Phone' }, { value: 'whatsapp', label: 'WhatsApp' }, { value: 'other', label: 'Other' }, ]; const CHANNEL_ICONS: Record> = { email: Mail, phone: Phone, whatsapp: MessageSquare, other: MoreHorizontal, }; export function ContactsEditor({ clientId, contacts }: { clientId: string; contacts: Contact[] }) { const qc = useQueryClient(); const [adding, setAdding] = useState(false); function invalidate() { qc.invalidateQueries({ queryKey: ['clients', clientId] }); } const updateMutation = useMutation({ mutationFn: async ({ contactId, patch, }: { contactId: string; patch: Partial< Pick >; }) => apiFetch(`/api/v1/clients/${clientId}/contacts/${contactId}`, { method: 'PATCH', body: patch, }), onSuccess: invalidate, }); const addMutation = useMutation({ mutationFn: async (data: { channel: string; value: string; valueE164?: string | null; valueCountry?: string | null; label?: string; }) => apiFetch(`/api/v1/clients/${clientId}/contacts`, { method: 'POST', body: { ...data, isPrimary: false }, }), onSuccess: invalidate, }); const removeMutation = useMutation({ mutationFn: async (contactId: string) => apiFetch(`/api/v1/clients/${clientId}/contacts/${contactId}`, { method: 'DELETE' }), onSuccess: invalidate, }); return (
{contacts.length === 0 && !adding && (

No contacts yet

)} {contacts.map((c) => ( updateMutation.mutateAsync({ contactId: c.id, patch })} onRemove={async () => { if (!confirm('Remove this contact?')) return; await removeMutation.mutateAsync(c.id); }} /> ))} {adding ? ( setAdding(false)} onSave={async (data) => { await addMutation.mutateAsync(data); setAdding(false); }} /> ) : ( )}
); } function ContactRow({ contact, onUpdate, onRemove, }: { contact: Contact; onUpdate: ( patch: Partial< Pick >, ) => Promise; onRemove: () => void; }) { const Icon = CHANNEL_ICONS[contact.channel] ?? MoreHorizontal; async function togglePrimary() { try { await onUpdate({ isPrimary: !contact.isPrimary }); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to update'); } } async function changeChannel(next: string) { if (next === contact.channel) return; try { await onUpdate({ channel: next }); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to update'); } } return (
{/* Left: channel + value */}
{contact.channel === 'phone' || contact.channel === 'whatsapp' ? ( { if (!e164) { toast.error('Phone number is required'); return; } await onUpdate({ value: e164, valueE164: e164, valueCountry: country }); }} /> ) : ( { if (!v) { toast.error('Value is required'); return; } await onUpdate({ value: v }); }} /> )}
{/* Right: tag + actions */}
{ await onUpdate({ label: v }); }} />
); } function ChannelPicker({ value, onChange, children, }: { value: string; onChange: (next: string) => void; children: React.ReactNode; }) { return ( ); } function NewContactForm({ onSave, onCancel, }: { onSave: (data: { channel: string; value: string; valueE164?: string | null; valueCountry?: string | null; label?: string; }) => Promise; onCancel: () => void; }) { const [channel, setChannel] = useState('email'); const [value, setValue] = useState(''); const [phoneValue, setPhoneValue] = useState(null); const [label, setLabel] = useState(''); const [saving, setSaving] = useState(false); const isPhoneChannel = channel === 'phone' || channel === 'whatsapp'; async function submit() { if (isPhoneChannel) { if (!phoneValue?.e164) return; setSaving(true); try { await onSave({ channel, value: phoneValue.e164, valueE164: phoneValue.e164, valueCountry: phoneValue.country, label: label.trim() || undefined, }); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add contact'); } finally { setSaving(false); } return; } if (!value.trim()) return; setSaving(true); try { await onSave({ channel, value: value.trim(), label: label.trim() || undefined }); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add contact'); } finally { setSaving(false); } } const submitDisabled = saving || (isPhoneChannel ? !phoneValue?.e164 : !value.trim()); return (
{isPhoneChannel ? (
setPhoneValue(v)} data-testid="new-contact-phone" />
) : ( setValue(e.target.value)} placeholder={channel === 'email' ? 'name@example.com' : 'value'} className="h-7 text-sm flex-1 min-w-0" autoFocus disabled={saving} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void submit(); } if (e.key === 'Escape') onCancel(); }} /> )} setLabel(e.target.value)} placeholder="tag (optional)" className="h-7 text-xs w-28" disabled={saving} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); void submit(); } if (e.key === 'Escape') onCancel(); }} />
); }