'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Bell, CalendarDays, Mail, Mic, MicOff, MoreVertical, Phone, Pencil, Plus, Trash2, Users, Video, } from 'lucide-react'; import { useVoiceTranscription } from '@/hooks/use-voice-transcription'; import { WhatsAppIcon } from '@/components/icons/whatsapp'; import { format, formatDistanceToNowStrict } from 'date-fns'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { Textarea } from '@/components/ui/textarea'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useConfirmation } from '@/hooks/use-confirmation'; import { cn } from '@/lib/utils'; interface InterestContactLogTabProps { interestId: string; } type Channel = 'email' | 'phone' | 'whatsapp' | 'in_person' | 'video' | 'other'; type Direction = 'outbound' | 'inbound'; type Template = 'call' | 'visit' | 'email'; interface ContactLogEntry { id: string; occurredAt: string; channel: Channel; direction: Direction; summary: string; voiceTranscript: string | null; templateUsed: string | null; followUpAt: string | null; reminderId: string | null; createdBy: string; createdAt: string; updatedAt: string; } /** Quick-template seeds — drop a starting structure into the summary so reps * spend their typing on the substance, not the scaffolding. */ const TEMPLATE_SEEDS: Record< Template, { channel: Channel; direction: Direction; summary: string; label: string; icon: ChannelIcon } > = { call: { channel: 'phone', direction: 'outbound', summary: 'Called the client. Discussed:\n\n• \n\nNext step: ', label: 'Call', icon: Phone, }, visit: { channel: 'in_person', direction: 'outbound', summary: 'Met with the client in person. Discussed:\n\n• \n\nNext step: ', label: 'Visit', icon: Users, }, email: { channel: 'email', direction: 'outbound', summary: 'Emailed the client.\n\nTopic: \n\nResponse expected: ', label: 'Email', icon: Mail, }, }; type ChannelIcon = React.ComponentType<{ className?: string }>; const CHANNEL_META: Record = { email: { label: 'Email', icon: Mail, tone: 'bg-sky-100 text-sky-700' }, phone: { label: 'Phone', icon: Phone, tone: 'bg-emerald-100 text-emerald-700' }, whatsapp: { label: 'WhatsApp', icon: WhatsAppIcon, tone: 'bg-emerald-100 text-emerald-700' }, in_person: { label: 'In person', icon: Users, tone: 'bg-amber-100 text-amber-800' }, video: { label: 'Video', icon: Video, tone: 'bg-violet-100 text-violet-700' }, other: { label: 'Other', icon: CalendarDays, tone: 'bg-slate-100 text-slate-700' }, }; /** * Per-interaction contact log. Sales reps log every email / call / * WhatsApp / meeting touch with the client here so the team has a * structured history of "what was the last conversation about" — not * just the bare "last contact 8d ago" timestamp on the interest. * * Each entry can optionally schedule a follow-up that auto-creates a * reminder pointing back at the interest. Editing the entry's * follow-up date keeps the linked reminder in sync; deleting the * entry removes the reminder. */ export function InterestContactLogTab({ interestId }: InterestContactLogTabProps) { const [composeOpen, setComposeOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const { data: res, isLoading } = useQuery<{ data: ContactLogEntry[] }>({ queryKey: ['interests', interestId, 'contact-log'], queryFn: () => apiFetch<{ data: ContactLogEntry[] }>(`/api/v1/interests/${interestId}/contact-log`), }); const entries = res?.data ?? []; return (

Contact log

Record each conversation. The most recent log entry sets the “Last contact” chip on the interest header.

{isLoading ? (
) : entries.length === 0 ? ( setComposeOpen(true)} /> ) : (
    {entries.map((e) => ( ))}
)} {editTarget && ( !o && setEditTarget(null)} /> )}
); } // ─── Row ───────────────────────────────────────────────────────────────────── function ContactLogRow({ entry, interestId, onEdit, }: { entry: ContactLogEntry; interestId: string; onEdit: (e: ContactLogEntry) => void; }) { const queryClient = useQueryClient(); const { confirm, dialog: confirmDialog } = useConfirmation(); const channelMeta = CHANNEL_META[entry.channel]; const Icon = channelMeta.icon; const deleteMutation = useMutation({ mutationFn: () => apiFetch(`/api/v1/contact-log/${entry.id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['interests', interestId, 'contact-log'] }); queryClient.invalidateQueries({ queryKey: ['interests', interestId] }); toast.success('Contact log entry deleted.'); }, onError: (err) => toastError(err), }); return (
  • {channelMeta.label} {entry.direction} {format(new Date(entry.occurredAt), 'MMM d, yyyy · HH:mm')} ({formatDistanceToNowStrict(new Date(entry.occurredAt))} ago)

    {entry.summary}

    {entry.followUpAt && (

    Follow up {format(new Date(entry.followUpAt), 'MMM d, yyyy')} (reminder created)

    )}
    onEdit(entry)}> Edit { const ok = await confirm({ title: 'Delete contact log entry', description: 'This cannot be undone.', confirmLabel: 'Delete', }); if (ok) deleteMutation.mutate(); }} > Delete
    {confirmDialog}
  • ); } // ─── Empty state ───────────────────────────────────────────────────────────── function EmptyState({ onAdd }: { onAdd: () => void }) { return (

    No contact logged yet

    Record every call, email, and meeting so the team has full context the next time someone picks up the deal.

    ); } // ─── Compose / edit dialog ─────────────────────────────────────────────────── function ComposeDialog(props: { interestId: string; existing?: ContactLogEntry; open: boolean; onOpenChange: (open: boolean) => void; }) { // Key-based remount: body keyed on open + existing.id so useState // initializers re-run each time the dialog opens with a new row. // Replaces the prior useEffect(setState, [open, existing]) sync. return ( ); } function ComposeDialogBody({ interestId, existing, open, onOpenChange, }: { interestId: string; existing?: ContactLogEntry; open: boolean; onOpenChange: (open: boolean) => void; }) { const queryClient = useQueryClient(); const isEdit = !!existing; const defaultOccurredAt = useMemo(() => { if (existing) return localIsoString(existing.occurredAt); return localIsoString(new Date().toISOString()); }, [existing]); const [occurredAt, setOccurredAt] = useState(defaultOccurredAt); const [channel, setChannel] = useState(existing?.channel ?? 'phone'); const [direction, setDirection] = useState(existing?.direction ?? 'outbound'); const [summary, setSummary] = useState(existing?.summary ?? ''); const [followUpAt, setFollowUpAt] = useState( existing?.followUpAt ? localIsoString(existing.followUpAt) : '', ); const [templateUsed, setTemplateUsed] = useState