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/broadcasts/step4-schedule-send.tsx b/src/components/broadcasts/step4-schedule-send.tsx index b07893b..98c23c3 100644 --- a/src/components/broadcasts/step4-schedule-send.tsx +++ b/src/components/broadcasts/step4-schedule-send.tsx @@ -14,7 +14,7 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import { ArrowLeft, Send, Clock, Loader2, Users, Lock, Save } from 'lucide-react'; +import { ArrowLeft, Send, Loader2, Users, Save } from 'lucide-react'; interface AudienceConfig { type: string; @@ -45,7 +45,6 @@ export function Step4ScheduleSend({ isProcessing, progress, }: Step4Props) { - const [timing, setTiming] = useState<'now' | 'later'>('now'); const [showConfirm, setShowConfirm] = useState(false); const [estimatedReach, setEstimatedReach] = useState(0); const [loadingReach, setLoadingReach] = useState(true); @@ -143,41 +142,6 @@ export function Step4ScheduleSend({ - {/* Delivery Timing */} -
-

Delivery Timing

-
- - -
- -
-
-

Schedule for Later

- - - Pro - -
-

Pick a date and time

-
-
-
-
- {/* Processing overlay */} {isProcessing && (
diff --git a/src/components/inbox/message-thread.tsx b/src/components/inbox/message-thread.tsx index 85044c7..b80e8a1 100644 --- a/src/components/inbox/message-thread.tsx +++ b/src/components/inbox/message-thread.tsx @@ -2,6 +2,7 @@ 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, @@ -9,21 +10,23 @@ import type { Contact, ConversationStatus, MessageTemplate, + 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"; @@ -47,6 +50,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 @@ -94,11 +101,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(() => { @@ -332,6 +365,27 @@ export function MessageThread({ [conversation, onNewMessage, onUpdateMessage], ); + 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 ( @@ -354,6 +408,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 (
@@ -419,15 +478,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 + + + )} + +