Files
ember-market-frontend/app/dashboard/storefront/customers/page.tsx
2025-04-02 00:30:57 +01:00

378 lines
16 KiB
TypeScript

"use client";
import React, { useEffect, useState, useCallback } from "react";
import { getCustomers, CustomerStats } from "@/services/customerService";
import { formatCurrency } from "@/utils/format";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import Layout from "@/components/layout/layout";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
ChevronLeft,
ChevronRight,
Loader2,
Users,
ArrowUpDown,
MessageCircle
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function CustomerManagementPage() {
const router = useRouter();
const [customers, setCustomers] = useState<CustomerStats[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [selectedCustomer, setSelectedCustomer] = useState<CustomerStats | null>(null);
const [sortConfig, setSortConfig] = useState<{
column: "totalOrders" | "totalSpent" | "lastOrderDate";
direction: "asc" | "desc";
}>({ column: "totalSpent", direction: "desc" });
const fetchCustomers = useCallback(async () => {
try {
setLoading(true);
const response = await getCustomers(page, itemsPerPage);
// Sort customers based on current sort config
let sortedCustomers = [...response.customers];
sortedCustomers.sort((a, b) => {
if (sortConfig.column === "totalOrders") {
return sortConfig.direction === "asc"
? a.totalOrders - b.totalOrders
: b.totalOrders - a.totalOrders;
} else if (sortConfig.column === "totalSpent") {
return sortConfig.direction === "asc"
? a.totalSpent - b.totalSpent
: b.totalSpent - a.totalSpent;
} else if (sortConfig.column === "lastOrderDate") {
return sortConfig.direction === "asc"
? new Date(a.lastOrderDate).getTime() - new Date(b.lastOrderDate).getTime()
: new Date(b.lastOrderDate).getTime() - new Date(a.lastOrderDate).getTime();
}
return 0;
});
setCustomers(sortedCustomers);
setTotalPages(Math.ceil(response.total / itemsPerPage));
} catch (err) {
toast.error("Failed to fetch customers");
console.error(err);
} finally {
setLoading(false);
}
}, [page, itemsPerPage, sortConfig]);
useEffect(() => {
fetchCustomers();
}, [fetchCustomers]);
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
}
}, [router]);
const handlePageChange = (newPage: number) => {
setPage(newPage);
};
const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(parseInt(value, 10));
setPage(1); // Reset to first page when changing items per page
};
const handleSort = (column: "totalOrders" | "totalSpent" | "lastOrderDate") => {
setSortConfig(prev => ({
column,
direction: prev.column === column && prev.direction === "asc" ? "desc" : "asc"
}));
};
// Format date without time
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric'
}).format(date);
};
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Users className="mr-2 h-6 w-6" />
Customer Management
</h1>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">Show:</div>
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
<SelectTrigger className="w-[70px]">
<SelectValue placeholder="25" />
</SelectTrigger>
<SelectContent>
{[5, 10, 25, 50, 100].map(size => (
<SelectItem key={size} value={size.toString()}>{size}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{loading
? "Loading..."
: `Showing ${customers.length} of ${totalPages * itemsPerPage} customers`}
</div>
</div>
{loading ? (
<div className="p-8 flex justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : customers.length === 0 ? (
<div className="p-8 text-center">
<Users className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-medium mb-2">No customers found</h3>
<p className="text-muted-foreground">
Once you have customers placing orders, they will appear here.
</p>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[300px]">Customer</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("totalOrders")}
>
<div className="flex items-center">
Total Orders
<ArrowUpDown className="ml-2 h-4 w-4" />
</div>
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => handleSort("totalSpent")}
>
<div className="flex items-center">
Total Spent
<ArrowUpDown className="ml-2 h-4 w-4" />
</div>
</TableHead>
<TableHead>Status Breakdown</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow
key={customer.userId}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
onClick={() => setSelectedCustomer(customer)}
>
<TableCell>
<div className="font-medium">@{customer.telegramUsername || "Unknown"}</div>
<div className="text-sm text-gray-500">ID: {customer.telegramUserId}</div>
</TableCell>
<TableCell>
<Badge variant="outline">{customer.totalOrders}</Badge>
</TableCell>
<TableCell className="font-medium">
{formatCurrency(customer.totalSpent)}
</TableCell>
<TableCell>
<div className="flex space-x-1 text-xs">
<Badge variant="outline" className="bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300 dark:hover:bg-blue-900/30">
Paid: {customer.ordersByStatus.paid}
</Badge>
<Badge variant="outline" className="bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-300 dark:hover:bg-green-900/30">
Completed: {customer.ordersByStatus.completed}
</Badge>
<Badge variant="outline" className="bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-300 dark:hover:bg-amber-900/30">
Shipped: {customer.ordersByStatus.shipped}
</Badge>
</div>
</TableCell>
<TableCell>
{/* {formatDate(customer.lastOrderDate)} */}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center">
<div className="text-sm text-gray-500 dark:text-gray-400">
Page {page} of {totalPages}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.max(1, page - 1))}
disabled={page === 1 || loading}
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
disabled={page === totalPages || loading}
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
</div>
{/* Customer Details Dialog */}
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="text-xl font-semibold flex items-center">
<Users className="h-5 w-5 mr-2" />
Customer Details
</DialogTitle>
<DialogDescription>
Details and order statistics for this customer
</DialogDescription>
</DialogHeader>
{selectedCustomer && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-md font-medium">Customer Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Username:</div>
<div className="font-medium">@{selectedCustomer.telegramUsername || "Unknown"}</div>
</div>
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Telegram ID:</div>
<div className="font-medium">{selectedCustomer.telegramUserId}</div>
</div>
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Chat ID:</div>
<div className="font-medium">{selectedCustomer.chatId}</div>
</div>
<div className="flex justify-end pt-2">
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={() => {
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank');
}}
>
<MessageCircle className="h-3.5 w-3.5 mr-1" />
Open Chat
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-md font-medium">Order Statistics</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Total Orders:</div>
<div className="font-medium">{selectedCustomer.totalOrders}</div>
</div>
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Total Spent:</div>
<div className="font-medium">{formatCurrency(selectedCustomer.totalSpent)}</div>
</div>
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">First Order:</div>
<div className="font-medium">{formatDate(selectedCustomer.firstOrderDate)}</div>
</div>
<div className="flex justify-between text-sm">
<div className="text-gray-500 dark:text-gray-400">Last Order:</div>
<div className="font-medium">{formatDate(selectedCustomer.lastOrderDate)}</div>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-md font-medium">Order Status Breakdown</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-md">
<p className="text-sm text-blue-700 dark:text-blue-300">Paid</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.paid}</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-md">
<p className="text-sm text-purple-700 dark:text-purple-300">Acknowledged</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.acknowledged}</p>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 p-3 rounded-md">
<p className="text-sm text-amber-700 dark:text-amber-300">Shipped</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.shipped}</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
<p className="text-sm text-green-700 dark:text-green-300">Completed</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.completed}</p>
</div>
</div>
</CardContent>
</Card>
</div>
)}
</DialogContent>
</Dialog>
</div>
</Layout>
);
}