Update page.tsx
This commit is contained in:
@@ -21,8 +21,24 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight, Loader2, Users } from "lucide-react";
|
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() {
|
export default function CustomerManagementPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -32,12 +48,36 @@ export default function CustomerManagementPage() {
|
|||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState<CustomerStats | null>(null);
|
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 () => {
|
const fetchCustomers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getCustomers(page, itemsPerPage);
|
const response = await getCustomers(page, itemsPerPage);
|
||||||
setCustomers(response.customers);
|
|
||||||
|
// 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));
|
setTotalPages(Math.ceil(response.total / itemsPerPage));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error("Failed to fetch customers");
|
toast.error("Failed to fetch customers");
|
||||||
@@ -45,7 +85,7 @@ export default function CustomerManagementPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, itemsPerPage]);
|
}, [page, itemsPerPage, sortConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
@@ -71,6 +111,23 @@ export default function CustomerManagementPage() {
|
|||||||
setPage(1); // Reset to first page when changing items per page
|
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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -81,13 +138,14 @@ export default function CustomerManagementPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
<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="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="flex items-center gap-2">
|
||||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">Show:</div>
|
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">Show:</div>
|
||||||
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
|
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
|
||||||
<SelectTrigger className="w-20">
|
<SelectTrigger className="w-[70px]">
|
||||||
<SelectValue placeholder="Page Size" />
|
<SelectValue placeholder="25" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{[5, 10, 25, 50, 100].map(size => (
|
{[5, 10, 25, 50, 100].map(size => (
|
||||||
@@ -98,23 +156,59 @@ export default function CustomerManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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 ? (
|
{loading ? (
|
||||||
<div className="p-8 flex justify-center">
|
<div className="p-8 flex justify-center">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
) : customers.length === 0 ? (
|
) : customers.length === 0 ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<p className="text-gray-500 dark:text-gray-400">No customers found</p>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[300px]">Customer</TableHead>
|
<TableHead className="w-[300px]">Customer</TableHead>
|
||||||
<TableHead>Total Orders</TableHead>
|
<TableHead
|
||||||
<TableHead>Total Spent</TableHead>
|
className="cursor-pointer"
|
||||||
<TableHead className="w-[300px]">Order Status</TableHead>
|
onClick={() => handleSort("totalOrders")}
|
||||||
<TableHead>Last Order</TableHead>
|
>
|
||||||
|
<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>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleSort("lastOrderDate")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Last Order Date
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -125,25 +219,36 @@ export default function CustomerManagementPage() {
|
|||||||
onClick={() => setSelectedCustomer(customer)}
|
onClick={() => setSelectedCustomer(customer)}
|
||||||
>
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium">@{customer.telegramUsername}</div>
|
<div className="font-medium">@{customer.telegramUsername || "Unknown"}</div>
|
||||||
<div className="text-sm text-gray-500">ID: {customer.telegramUserId}</div>
|
<div className="text-sm text-gray-500">ID: {customer.telegramUserId}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{customer.totalOrders}</TableCell>
|
|
||||||
<TableCell>{formatCurrency(customer.totalSpent)}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="text-sm">
|
<Badge variant="outline">{customer.totalOrders}</Badge>
|
||||||
Paid: {customer.ordersByStatus.paid} |
|
</TableCell>
|
||||||
Completed: {customer.ordersByStatus.completed} |
|
<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}
|
Shipped: {customer.ordersByStatus.shipped}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{new Date(customer.lastOrderDate).toLocaleDateString()}
|
{formatDate(customer.lastOrderDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
@@ -155,83 +260,127 @@ export default function CustomerManagementPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(Math.max(1, page - 1))}
|
onClick={() => handlePageChange(Math.max(1, page - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1 || loading}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
<span className="ml-1">Previous</span>
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
|
onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
|
||||||
disabled={page === totalPages}
|
disabled={page === totalPages || loading}
|
||||||
>
|
>
|
||||||
<span className="mr-1">Next</span>
|
Next
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Customer Details Modal */}
|
{/* 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 && (
|
{selectedCustomer && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div className="space-y-4">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<Card>
|
||||||
<h2 className="text-xl font-bold">Customer Details</h2>
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSelectedCustomer(null)}
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
</CardContent>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</Card>
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold mb-2">Customer Information</h3>
|
<Card>
|
||||||
<div className="space-y-1">
|
<CardHeader className="pb-2">
|
||||||
<p>Telegram Username: @{selectedCustomer.telegramUsername}</p>
|
<CardTitle className="text-md font-medium">Order Statistics</CardTitle>
|
||||||
<p>Telegram User ID: {selectedCustomer.telegramUserId}</p>
|
</CardHeader>
|
||||||
<p>Chat ID: {selectedCustomer.chatId}</p>
|
<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>
|
||||||
|
<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>
|
||||||
<div>
|
<div className="flex justify-between text-sm">
|
||||||
<h3 className="font-semibold mb-2">Order Statistics</h3>
|
<div className="text-gray-500 dark:text-gray-400">First Order:</div>
|
||||||
<div className="space-y-1">
|
<div className="font-medium">{formatDate(selectedCustomer.firstOrderDate)}</div>
|
||||||
<p>Total Orders: {selectedCustomer.totalOrders}</p>
|
|
||||||
<p>Total Spent: {formatCurrency(selectedCustomer.totalSpent)}</p>
|
|
||||||
<p>First Order: {new Date(selectedCustomer.firstOrderDate).toLocaleDateString()}</p>
|
|
||||||
<p>Last Order: {new Date(selectedCustomer.lastOrderDate).toLocaleDateString()}</p>
|
|
||||||
</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>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold mb-2">Order Status Breakdown</h3>
|
<Card>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-2 rounded">
|
<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-sm text-blue-700 dark:text-blue-300">Paid</p>
|
||||||
<p className="font-medium">{selectedCustomer.ordersByStatus.paid}</p>
|
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.paid}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 dark:bg-green-900/20 p-2 rounded">
|
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-md">
|
||||||
<p className="text-sm text-green-700 dark:text-green-300">Completed</p>
|
|
||||||
<p className="font-medium">{selectedCustomer.ordersByStatus.completed}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-2 rounded">
|
|
||||||
<p className="text-sm text-purple-700 dark:text-purple-300">Acknowledged</p>
|
<p className="text-sm text-purple-700 dark:text-purple-300">Acknowledged</p>
|
||||||
<p className="font-medium">{selectedCustomer.ordersByStatus.acknowledged}</p>
|
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.acknowledged}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-amber-50 dark:bg-amber-900/20 p-2 rounded">
|
<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-sm text-amber-700 dark:text-amber-300">Shipped</p>
|
||||||
<p className="font-medium">{selectedCustomer.ordersByStatus.shipped}</p>
|
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.shipped}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user