Files
ember-market-frontend/components/dashboard/ChatTable.tsx
2025-04-07 19:25:24 +01:00

403 lines
12 KiB
TypeScript

"use client"
import React, { useState, useEffect, useRef, useCallback } 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,
Eye,
User,
ChevronLeft,
ChevronRight,
MessageSquare,
ArrowRightCircle,
X,
Clock,
CheckCheck,
Search,
Volume2,
VolumeX
} from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "sonner";
import { clientFetch, getCookie } from "@/lib/api";
import { formatDistance } from 'date-fns';
import Link from 'next/link';
import { cn } from '@/lib/utils/general';
interface Chat {
_id: string;
buyerId: string;
vendorId: string;
storeId: string;
lastUpdated: string;
orderId?: string;
telegramUsername?: string | null;
}
interface UnreadCounts {
totalUnread: number;
chatCounts: Record<string, number>;
}
interface ChatResponse {
chats: Chat[];
page: number;
totalPages: number;
totalChats: 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);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalChats, setTotalChats] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState<number>(10);
const isManualRefresh = useRef(false);
// 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 or page/limit changes
useEffect(() => {
// Skip fetch if this effect was triggered by a manual refresh
// since we'll call fetchChats directly in that case
if (!isManualRefresh.current) {
fetchChats();
}
isManualRefresh.current = false;
// Set up polling for unread messages
const interval = setInterval(() => {
fetchUnreadCounts();
}, 30000); // Check every 30 seconds
return () => clearInterval(interval);
}, [currentPage, itemsPerPage]);
// Handle refresh button click
const handleRefresh = () => {
isManualRefresh.current = true;
setCurrentPage(1);
fetchChats();
};
// Fetch unread counts
const fetchUnreadCounts = async () => {
try {
// Get the vendor ID from the auth token
const { vendorId } = getVendorIdFromToken();
// Fetch unread counts for this vendor using clientFetch
const response = await clientFetch(`/chats/vendor/${vendorId}/unread`);
const newUnreadCounts = response;
// 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 with pagination
const fetchChats = async () => {
setLoading(true);
try {
// Get the vendor ID from the auth token
const { vendorId } = getVendorIdFromToken();
// Now fetch chats for this vendor using clientFetch with pagination
const response = await clientFetch(`/chats/vendor/${vendorId}?page=${currentPage}&limit=${itemsPerPage}`);
// Check if the response is the old format (array) or new paginated format
if (Array.isArray(response)) {
// Handle old API response format (backward compatibility)
setChats(response);
setTotalPages(1);
setTotalChats(response.length);
} else {
// Handle new paginated response format
setChats(response.chats || []);
setTotalPages(response.totalPages || 1);
setCurrentPage(response.page || 1);
setTotalChats(response.totalChats || 0);
}
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");
};
// Handle pagination
const goToNextPage = () => {
if (currentPage < totalPages) {
isManualRefresh.current = true;
setCurrentPage(prev => prev + 1);
fetchChats();
}
};
const goToPrevPage = () => {
if (currentPage > 1) {
isManualRefresh.current = true;
setCurrentPage(prev => prev - 1);
fetchChats();
}
};
// Handle items per page change
const handleItemsPerPageChange = (value: string) => {
const newLimit = parseInt(value);
isManualRefresh.current = true;
setItemsPerPage(newLimit);
setCurrentPage(1); // Reset to first page when changing limit
fetchChats();
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
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>
<User className="h-4t w-4" />
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">
{chat.telegramUsername ? `@${chat.telegramUsername}` : 'Customer'}
</div>
<div className="text-xs text-muted-foreground">
ID: {chat.buyerId}
</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">
<div className="flex justify-end space-x-2">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleChatClick(chat._id);
}}
>
<Eye className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination controls */}
{!loading && chats.length > 0 && (
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Showing {chats.length} of {totalChats} chats
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">Rows per page:</span>
<Select
value={itemsPerPage.toString()}
onValueChange={handleItemsPerPageChange}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={itemsPerPage.toString()} />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={goToPrevPage}
disabled={currentPage <= 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="text-sm">
Page {currentPage} of {totalPages}
</div>
<Button
variant="outline"
size="sm"
onClick={goToNextPage}
disabled={currentPage >= totalPages || loading}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</div>
);
}