mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge branch 'main' of https://github.com/ArnasDon/wacrm into feat/inbox-template-picker
# Conflicts: # src/components/inbox/message-thread.tsx
This commit is contained in:
commit
fbb95eb2a7
@ -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 (<lg) we show a SINGLE pane — either the list or the
|
||||
// thread — rather than cramming both side-by-side. Selecting a
|
||||
// conversation slides the thread in; the thread's back button pops
|
||||
@ -312,6 +332,7 @@ export default function InboxPage() {
|
||||
onNewMessage={handleNewMessage}
|
||||
onUpdateMessage={handleUpdateMessage}
|
||||
onStatusChange={handleStatusChange}
|
||||
onAssignChange={handleAssignChange}
|
||||
onBack={handleCloseConversation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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<number>(0);
|
||||
const [loadingReach, setLoadingReach] = useState(true);
|
||||
@ -143,41 +142,6 @@ export function Step4ScheduleSend({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery Timing */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-white">Delivery Timing</p>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
onClick={() => setTiming('now')}
|
||||
className={`flex items-start gap-3 rounded-xl border p-4 text-left transition-all ${
|
||||
timing === 'now'
|
||||
? 'border-violet-500 bg-violet-500/5 ring-1 ring-violet-500/30'
|
||||
: 'border-slate-800 bg-slate-900/50 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Send className={`mt-0.5 h-4 w-4 ${timing === 'now' ? 'text-violet-400' : 'text-slate-400'}`} />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Send Immediately</p>
|
||||
<p className="mt-0.5 text-xs text-slate-400">Start sending right away</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="relative flex items-start gap-3 rounded-xl border border-slate-800 bg-slate-900/30 p-4 opacity-60">
|
||||
<Clock className="mt-0.5 h-4 w-4 text-slate-500" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium text-slate-400">Schedule for Later</p>
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-slate-800 px-2 py-0.5 text-[10px] font-medium text-slate-400">
|
||||
<Lock className="h-2.5 w-2.5" />
|
||||
Pro
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-slate-500">Pick a date and time</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Processing overlay */}
|
||||
{isProcessing && (
|
||||
<div className="rounded-xl border border-violet-500/20 bg-violet-500/5 p-4">
|
||||
|
||||
@ -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<Message>) => 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<HTMLDivElement>(null);
|
||||
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
|
||||
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 (
|
||||
<div className="flex flex-1 flex-col bg-slate-950">
|
||||
@ -419,15 +478,60 @@ export function MessageThread({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Assign button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-xs text-slate-400 hover:text-white"
|
||||
>
|
||||
<UserPlus className="h-3 w-3" />
|
||||
Assign
|
||||
</Button>
|
||||
{/* Assign dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
"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" />
|
||||
<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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user