Files
ember-market-frontend/components/dashboard/ChatTable.tsx
2025-03-08 05:30:23 +00:00

271 lines
8.0 KiB
TypeScript

"use client"
import { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { formatDistanceToNow } from "date-fns";
import {
Plus,
MessageCircle,
Loader2,
RefreshCw
} from "lucide-react";
import axios from "axios";
import { toast } from "sonner";
import { getCookie } from "@/lib/client-utils";
interface Chat {
_id: string;
buyerId: string;
vendorId: string;
storeId: string;
lastUpdated: string;
orderId?: string;
}
interface UnreadCounts {
totalUnread: number;
chatCounts: Record<string, number>;
}
export default function ChatTable() {
const router = useRouter();
const [chats, setChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true);
const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} });
const audioRef = useRef<HTMLAudioElement | null>(null);
// Initialize audio element for notifications
useEffect(() => {
audioRef.current = new Audio('/notification.mp3');
return () => {
if (audioRef.current) {
audioRef.current = null;
}
};
}, []);
// Play notification sound
const playNotificationSound = () => {
if (audioRef.current) {
audioRef.current.play().catch(e => {
console.log("Failed to play notification sound:", e);
});
}
};
// Get vendor ID from JWT token
const getVendorIdFromToken = () => {
const authToken = getCookie("Authorization") || "";
if (!authToken) {
throw new Error("No auth token found");
}
const tokenParts = authToken.split(".");
if (tokenParts.length !== 3) {
throw new Error("Invalid token format");
}
const payload = JSON.parse(atob(tokenParts[1]));
const vendorId = payload.id;
if (!vendorId) {
throw new Error("Vendor ID not found in token");
}
return { vendorId, authToken };
};
// Fetch chats when component mounts
useEffect(() => {
fetchChats();
// Set up polling for unread messages
const interval = setInterval(() => {
fetchUnreadCounts();
}, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, []);
// Fetch unread counts
const fetchUnreadCounts = async () => {
try {
// Get the vendor ID from the auth token
const { vendorId, authToken } = getVendorIdFromToken();
// Fetch unread counts for this vendor
const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/chats/vendor/${vendorId}/unread`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
const newUnreadCounts = response.data;
// Play sound if there are new messages
if (newUnreadCounts.totalUnread > unreadCounts.totalUnread) {
playNotificationSound();
}
setUnreadCounts(newUnreadCounts);
} catch (error) {
console.error("Failed to fetch unread counts:", error);
}
};
// Fetch chats
const fetchChats = async () => {
setLoading(true);
try {
// Get the vendor ID from the auth token
const { vendorId, authToken } = getVendorIdFromToken();
// Now fetch chats for this vendor
const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/chats/vendor/${vendorId}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
setChats(Array.isArray(response.data) ? response.data : []);
await fetchUnreadCounts();
} catch (error) {
console.error("Failed to fetch chats:", error);
toast.error("Failed to load chat conversations");
setChats([]);
} finally {
setLoading(false);
}
};
// Navigate to chat detail page
const handleChatClick = (chatId: string) => {
router.push(`/dashboard/chats/${chatId}`);
};
// Create new chat
const handleCreateChat = () => {
router.push("/dashboard/chats/new");
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Button
variant="outline"
size="sm"
onClick={fetchChats}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="ml-2">Refresh</span>
</Button>
<Button onClick={handleCreateChat} size="sm">
<Plus className="h-4 w-4 mr-2" />
New Chat
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]">Customer</TableHead>
<TableHead>Last Activity</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : chats.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
<div className="flex flex-col items-center justify-center">
<MessageCircle className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-muted-foreground">No chats found</p>
</div>
</TableCell>
</TableRow>
) : (
chats.map((chat) => (
<TableRow
key={chat._id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleChatClick(chat._id)}
>
<TableCell>
<div className="flex items-center space-x-3">
<Avatar>
<AvatarFallback>
{chat.buyerId?.slice(0, 2).toUpperCase() || 'CU'}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">Customer {chat.buyerId.slice(0, 4)}</div>
{chat.orderId && (
<div className="text-xs text-muted-foreground">
Order #{chat.orderId}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
{formatDistanceToNow(new Date(chat.lastUpdated), { addSuffix: true })}
</TableCell>
<TableCell>
{unreadCounts.chatCounts[chat._id] > 0 ? (
<Badge variant="destructive" className="ml-1">
{unreadCounts.chatCounts[chat._id]} new
</Badge>
) : (
<Badge variant="outline">Read</Badge>
)}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleChatClick(chat._id);
}}
>
View
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}