import { useEffect, useState, useRef } from "react"; interface Conversation { id: string; clientId: string; clientName: string; channel: string; clientPhone: string; lastMessageAt: string | null; unreadCount: number; status: string; lastMessage: { body: string | null; direction: string; createdAt: string } | null; } interface Message { id: string; direction: "inbound" | "outbound"; body: string | null; status: string; createdAt: string; sentByStaffId: string | null; } function relativeTime(dateStr: string | null): string { if (!dateStr) return ""; const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } function truncate(text: string | null, max: number): string { if (!text) return ""; return text.length > max ? text.slice(0, max) + "…" : text; } export function MessagesPage() { const [conversations, setConversations] = useState([]); const [messages, setMessages] = useState([]); const [selectedId, setSelectedId] = useState(null); const [loading, setLoading] = useState(true); const [messagesLoading, setMessagesLoading] = useState(false); const [error, setError] = useState(null); const [messageError, setMessageError] = useState(null); const [body, setBody] = useState(""); const [sending, setSending] = useState(false); const messagesEndRef = useRef(null); async function loadConversations() { try { const res = await fetch("/api/conversations?limit=20"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); const data = json.items as Conversation[]; setConversations(data); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to load conversations"); } } async function loadMessages(conversationId: string) { setMessagesLoading(true); setMessageError(null); try { const res = await fetch(`/api/conversations/${conversationId}/messages?limit=50`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); setMessages((json.items as Message[]).reverse()); } catch (e: unknown) { setMessageError(e instanceof Error ? e.message : "Failed to load messages"); } finally { setMessagesLoading(false); } } useEffect(() => { loadConversations().finally(() => setLoading(false)); const interval = setInterval(loadConversations, 10000); return () => clearInterval(interval); }, []); useEffect(() => { if (selectedId) { loadMessages(selectedId); } else { setMessages([]); } }, [selectedId]); useEffect(() => { if (messages.length > 0) { messagesEndRef.current?.scrollIntoView?.({ behavior: "smooth" }); } }, [messages]); async function handleSend(e: React.FormEvent) { e.preventDefault(); if (!selectedId || !body.trim() || sending) return; setSending(true); setMessageError(null); const optimistic: Message = { id: `temp-${Date.now()}`, direction: "outbound", body: body.trim(), status: "queued", createdAt: new Date().toISOString(), sentByStaffId: null, }; setMessages((prev) => [...prev, optimistic]); const currentBody = body; setBody(""); try { const res = await fetch(`/api/conversations/${selectedId}/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body: currentBody }), }); if (res.status === 409) { const data = (await res.json()) as { error?: string }; setMessageError(data.error ?? "Client has opted out of SMS"); setMessages((prev) => prev.filter((m) => m.id !== optimistic.id)); return; } if (!res.ok) { const data = (await res.json()) as { error?: string }; throw new Error(data.error ?? `HTTP ${res.status}`); } const sent = (await res.json()) as Message; setMessages((prev) => prev.map((m) => (m.id === optimistic.id ? sent : m))); loadConversations(); } catch (e: unknown) { setMessageError(e instanceof Error ? e.message : "Failed to send message"); setMessages((prev) => prev.filter((m) => m.id !== optimistic.id)); } finally { setSending(false); } } return (
{/* Thread list */}
Conversations
{loading ? (

Loading…

) : error ? (

{error}

) : conversations.length === 0 ? (

No conversations yet

) : ( conversations.map((conv) => (
setSelectedId(conv.id)} style={{ padding: "0.75rem 1rem", borderBottom: "1px solid #f3f4f6", cursor: "pointer", background: selectedId === conv.id ? "#ecfdf5" : "transparent", }} >
{conv.clientName} {conv.unreadCount > 0 && ( {conv.unreadCount} )}
{truncate(conv.lastMessage?.body ?? null, 60)}
{relativeTime(conv.lastMessageAt)}
)) )}
{/* Conversation view */}
{!selectedId ? (
Select a conversation
) : messagesLoading ? (
Loading messages…
) : ( <>
{messages.map((msg) => (
{msg.body}
{new Date(msg.createdAt).toLocaleString()}
))}
{messageError && (
{messageError}
)}
setBody(e.target.value)} placeholder="Type a message…" disabled={sending} style={{ flex: 1, padding: "0.5rem 0.75rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 14 }} />
)}
); }