diff --git a/src/app/(dashboard)/inbox/page.tsx b/src/app/(dashboard)/inbox/page.tsx index ffbf775..0ea5c0a 100644 --- a/src/app/(dashboard)/inbox/page.tsx +++ b/src/app/(dashboard)/inbox/page.tsx @@ -256,6 +256,26 @@ export default function InboxPage() { [activeConversation] ); + const handleAssignChange = useCallback( + (conversationId: string, assignedAgentId: string | null) => { + setConversations((prev) => + prev.map((c) => + c.id === conversationId + ? { ...c, assigned_agent_id: assignedAgentId ?? undefined } + : c + ) + ); + if (activeConversation?.id === conversationId) { + setActiveConversation((prev) => + prev + ? { ...prev, assigned_agent_id: assignedAgentId ?? undefined } + : prev + ); + } + }, + [activeConversation] + ); + // On mobile ( diff --git a/src/components/inbox/message-thread.tsx b/src/components/inbox/message-thread.tsx index c3b8671..d8af203 100644 --- a/src/components/inbox/message-thread.tsx +++ b/src/components/inbox/message-thread.tsx @@ -2,22 +2,30 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { createClient } from "@/lib/supabase/client"; +import { useAuth } from "@/hooks/use-auth"; import { cn } from "@/lib/utils"; -import type { Conversation, Message, Contact, ConversationStatus } from "@/types"; +import type { + Conversation, + Message, + Contact, + ConversationStatus, + Profile, +} from "@/types"; import { MessageSquare, ChevronDown, UserPlus, + Check, Clock, ArrowLeft, } from "lucide-react"; import { format, isToday, isYesterday, differenceInHours } from "date-fns"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -33,6 +41,10 @@ interface MessageThreadProps { onNewMessage: (message: Message) => void; onUpdateMessage: (id: string, updates: Partial) => void; onStatusChange: (conversationId: string, status: ConversationStatus) => void; + onAssignChange: ( + conversationId: string, + assignedAgentId: string | null, + ) => void; /** * On mobile, the thread is shown full-screen with the conversation list * hidden. This callback lets the page deselect the active conversation @@ -80,11 +92,37 @@ export function MessageThread({ onNewMessage, onUpdateMessage, onStatusChange, + onAssignChange, onBack, }: MessageThreadProps) { + const { user } = useAuth(); const [loading, setLoading] = useState(false); const scrollRef = useRef(null); const [templateModalOpen, setTemplateModalOpen] = useState(false); + const [profiles, setProfiles] = useState([]); + + // Profiles are bounded by RLS to rows the current user is allowed to + // see — today that's just the current user, but the dropdown keeps the + // shape ready for shared-team workspaces without a refactor. + useEffect(() => { + let cancelled = false; + const supabase = createClient(); + supabase + .from("profiles") + .select("*") + .order("full_name") + .then(({ data, error }) => { + if (cancelled) return; + if (error) { + console.error("Failed to fetch profiles:", error); + return; + } + setProfiles((data as Profile[]) ?? []); + }); + return () => { + cancelled = true; + }; + }, []); // 24-hour session timer const sessionInfo = useMemo(() => { @@ -266,6 +304,27 @@ export function MessageThread({ // Template modal implementation would go here }, []); + const handleAssignChange = useCallback( + async (agentId: string | null) => { + if (!conversation) return; + + const supabase = createClient(); + const { error } = await supabase + .from("conversations") + .update({ assigned_agent_id: agentId }) + .eq("id", conversation.id); + + if (error) { + console.error("Failed to update assignment:", error); + toast.error("Failed to update assignment"); + return; + } + + onAssignChange(conversation.id, agentId); + }, + [conversation, onAssignChange], + ); + // Empty state if (!conversation || !contact) { return ( @@ -288,6 +347,11 @@ export function MessageThread({ const currentStatus = STATUS_OPTIONS.find( (s) => s.value === conversation.status ); + const assignedAgentId = conversation.assigned_agent_id ?? null; + const currentAssignee = profiles.find((p) => p.user_id === assignedAgentId); + const assignLabel = assignedAgentId + ? (currentAssignee?.full_name ?? "Assigned") + : "Assign"; return (
@@ -353,15 +417,60 @@ export function MessageThread({ - {/* Assign button */} - + {/* Assign dropdown */} + + + + {assignLabel} + + + + {profiles.length === 0 ? ( + + No teammates available + + ) : ( + profiles.map((p) => { + const isSelected = p.user_id === assignedAgentId; + return ( + handleAssignChange(p.user_id)} + className={cn( + "text-sm", + isSelected ? "text-violet-400" : "text-slate-300" + )} + > + + {p.full_name} + {p.user_id === user?.id ? " (me)" : ""} + + {isSelected && } + + ); + }) + )} + {assignedAgentId && ( + <> + + handleAssignChange(null)} + className="text-sm text-slate-400" + > + Unassign + + + )} + +