mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #89 from ArnasDon/fix/inbox-assign-button
feat(inbox): wire conversation assignment dropdown
This commit is contained in:
commit
ac55c74ad4
@ -256,6 +256,26 @@ export default function InboxPage() {
|
|||||||
[activeConversation]
|
[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 (<lg) we show a SINGLE pane — either the list or the
|
// On mobile (<lg) we show a SINGLE pane — either the list or the
|
||||||
// thread — rather than cramming both side-by-side. Selecting a
|
// thread — rather than cramming both side-by-side. Selecting a
|
||||||
// conversation slides the thread in; the thread's back button pops
|
// conversation slides the thread in; the thread's back button pops
|
||||||
@ -312,6 +332,7 @@ export default function InboxPage() {
|
|||||||
onNewMessage={handleNewMessage}
|
onNewMessage={handleNewMessage}
|
||||||
onUpdateMessage={handleUpdateMessage}
|
onUpdateMessage={handleUpdateMessage}
|
||||||
onStatusChange={handleStatusChange}
|
onStatusChange={handleStatusChange}
|
||||||
|
onAssignChange={handleAssignChange}
|
||||||
onBack={handleCloseConversation}
|
onBack={handleCloseConversation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,22 +2,30 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
import { createClient } from "@/lib/supabase/client";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { Conversation, Message, Contact, ConversationStatus } from "@/types";
|
import type {
|
||||||
|
Conversation,
|
||||||
|
Message,
|
||||||
|
Contact,
|
||||||
|
ConversationStatus,
|
||||||
|
Profile,
|
||||||
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
|
Check,
|
||||||
Clock,
|
Clock,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { format, isToday, isYesterday, differenceInHours } from "date-fns";
|
import { format, isToday, isYesterday, differenceInHours } from "date-fns";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
@ -33,6 +41,10 @@ interface MessageThreadProps {
|
|||||||
onNewMessage: (message: Message) => void;
|
onNewMessage: (message: Message) => void;
|
||||||
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
||||||
onStatusChange: (conversationId: string, status: ConversationStatus) => 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
|
* On mobile, the thread is shown full-screen with the conversation list
|
||||||
* hidden. This callback lets the page deselect the active conversation
|
* hidden. This callback lets the page deselect the active conversation
|
||||||
@ -80,11 +92,37 @@ export function MessageThread({
|
|||||||
onNewMessage,
|
onNewMessage,
|
||||||
onUpdateMessage,
|
onUpdateMessage,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
|
onAssignChange,
|
||||||
onBack,
|
onBack,
|
||||||
}: MessageThreadProps) {
|
}: MessageThreadProps) {
|
||||||
|
const { user } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [templateModalOpen, setTemplateModalOpen] = useState(false);
|
const [templateModalOpen, setTemplateModalOpen] = useState(false);
|
||||||
|
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||||
|
|
||||||
|
// 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
|
// 24-hour session timer
|
||||||
const sessionInfo = useMemo(() => {
|
const sessionInfo = useMemo(() => {
|
||||||
@ -266,6 +304,27 @@ export function MessageThread({
|
|||||||
// Template modal implementation would go here
|
// 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
|
// Empty state
|
||||||
if (!conversation || !contact) {
|
if (!conversation || !contact) {
|
||||||
return (
|
return (
|
||||||
@ -288,6 +347,11 @@ export function MessageThread({
|
|||||||
const currentStatus = STATUS_OPTIONS.find(
|
const currentStatus = STATUS_OPTIONS.find(
|
||||||
(s) => s.value === conversation.status
|
(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 (
|
return (
|
||||||
<div className="flex flex-1 flex-col bg-slate-950">
|
<div className="flex flex-1 flex-col bg-slate-950">
|
||||||
@ -353,15 +417,60 @@ export function MessageThread({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
{/* Assign button */}
|
{/* Assign dropdown */}
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="ghost"
|
<DropdownMenuTrigger
|
||||||
size="sm"
|
className={cn(
|
||||||
className="h-7 gap-1 text-xs text-slate-400 hover:text-white"
|
"inline-flex items-center justify-center h-7 gap-1 px-2 text-xs rounded-md hover:bg-slate-800",
|
||||||
>
|
assignedAgentId ? "text-violet-400" : "text-slate-400"
|
||||||
<UserPlus className="h-3 w-3" />
|
)}
|
||||||
Assign
|
>
|
||||||
</Button>
|
<UserPlus className="h-3 w-3" />
|
||||||
|
<span className="hidden sm:inline">{assignLabel}</span>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="border-slate-700 bg-slate-800"
|
||||||
|
>
|
||||||
|
{profiles.length === 0 ? (
|
||||||
|
<DropdownMenuItem disabled className="text-sm text-slate-500">
|
||||||
|
No teammates available
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
profiles.map((p) => {
|
||||||
|
const isSelected = p.user_id === assignedAgentId;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => handleAssignChange(p.user_id)}
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
isSelected ? "text-violet-400" : "text-slate-300"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="flex-1">
|
||||||
|
{p.full_name}
|
||||||
|
{p.user_id === user?.id ? " (me)" : ""}
|
||||||
|
</span>
|
||||||
|
{isSelected && <Check className="ml-2 h-3 w-3" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{assignedAgentId && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator className="bg-slate-700" />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleAssignChange(null)}
|
||||||
|
className="text-sm text-slate-400"
|
||||||
|
>
|
||||||
|
Unassign
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user