All checks were successful
Build Frontend / build (push) Successful in 1m14s
Revamps the customer details dialog with improved layout, animations, and clearer stats breakdown. Upgrades the profit analysis modal with animated cards, clearer tier breakdown, and improved cost/margin/profit explanations. Also increases recent activity fetch limit, fixes quote hydration in dashboard content, and minor animation tweak in order table.
637 lines
30 KiB
TypeScript
637 lines
30 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState, useCallback } from "react";
|
|
import { getCustomers, type CustomerStats } from "@/lib/api";
|
|
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,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Loader2,
|
|
Users,
|
|
ArrowUpDown,
|
|
MessageCircle,
|
|
UserPlus,
|
|
MoreHorizontal,
|
|
Search,
|
|
X,
|
|
CreditCard,
|
|
Calendar,
|
|
ShoppingBag,
|
|
Truck,
|
|
CheckCircle,
|
|
} from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
export default function CustomerManagementPage() {
|
|
const router = useRouter();
|
|
const [customers, setCustomers] = useState<CustomerStats[]>([]);
|
|
const [filteredCustomers, setFilteredCustomers] = useState<CustomerStats[]>([]);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
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") {
|
|
// Handle null lastOrderDate values
|
|
if (!a.lastOrderDate && !b.lastOrderDate) return 0;
|
|
if (!a.lastOrderDate) return sortConfig.direction === "asc" ? -1 : 1;
|
|
if (!b.lastOrderDate) return sortConfig.direction === "asc" ? 1 : -1;
|
|
|
|
// Both have valid dates
|
|
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);
|
|
setFilteredCustomers(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]);
|
|
|
|
// Add filter function to filter customers when search query changes
|
|
useEffect(() => {
|
|
if (!searchQuery.trim()) {
|
|
setFilteredCustomers(customers);
|
|
} else {
|
|
const query = searchQuery.toLowerCase().trim();
|
|
const filtered = customers.filter(customer => {
|
|
const usernameMatch = customer.telegramUsername?.toLowerCase().includes(query);
|
|
const userIdMatch = customer.telegramUserId.toString().includes(query);
|
|
return usernameMatch || userIdMatch;
|
|
});
|
|
setFilteredCustomers(filtered);
|
|
}
|
|
}, [searchQuery, customers]);
|
|
|
|
const handlePageChange = (newPage: number) => {
|
|
setPage(newPage);
|
|
};
|
|
|
|
const handleItemsPerPageChange = (value: string) => {
|
|
setItemsPerPage(parseInt(value, 10));
|
|
setPage(1);
|
|
};
|
|
|
|
const handleSort = (column: "totalOrders" | "totalSpent" | "lastOrderDate") => {
|
|
setSortConfig(prev => ({
|
|
column,
|
|
direction: prev.column === column && prev.direction === "asc" ? "desc" : "asc"
|
|
}));
|
|
};
|
|
|
|
const clearSearch = () => {
|
|
setSearchQuery("");
|
|
};
|
|
|
|
const formatDate = (dateString: string | null | undefined) => {
|
|
if (!dateString) return "N/A";
|
|
try {
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat('en-GB', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).format(date);
|
|
} catch (error) {
|
|
return "N/A";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-semibold text-white flex items-center">
|
|
<Users className="mr-2 h-6 w-6" />
|
|
Customer Management
|
|
</h1>
|
|
</div>
|
|
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
|
<div className="p-4 border-b border-border/50 bg-muted/30 flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<div className="text-sm font-medium text-muted-foreground">Show:</div>
|
|
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
|
|
<SelectTrigger className="w-[70px] bg-background/50 border-border/50">
|
|
<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="relative flex-1 max-w-md">
|
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
<Search className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<Input
|
|
type="text"
|
|
placeholder="Search by username or Telegram ID..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 pr-10 py-2 w-full bg-background/50 border-border/50 focus:ring-primary/20 transition-all duration-300"
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
|
onClick={clearSearch}
|
|
>
|
|
<X className="h-4 w-4 text-muted-foreground hover:text-foreground" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
|
{loading
|
|
? "Loading..."
|
|
: searchQuery
|
|
? `Found ${filteredCustomers.length} matching customers`
|
|
: `Showing ${filteredCustomers.length} of ${totalPages * itemsPerPage} customers`}
|
|
</div>
|
|
</div>
|
|
|
|
<CardContent className="p-0">
|
|
{loading ? (
|
|
<div className="p-8">
|
|
{/* Loading indicator */}
|
|
<div className="absolute top-[69px] left-0 right-0 h-0.5 bg-muted overflow-hidden">
|
|
<div className="h-full bg-primary w-1/3 animate-shimmer"
|
|
style={{
|
|
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
|
|
backgroundSize: '200% 100%',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4 pb-4 border-b border-border/50">
|
|
{['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].map((header, i) => (
|
|
<Skeleton
|
|
key={i}
|
|
className="h-4 w-20 flex-1"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{[...Array(5)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center gap-4 pb-4 border-b border-border/50 last:border-b-0"
|
|
>
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<Skeleton className="h-10 w-10 rounded-full" />
|
|
<div className="space-y-1">
|
|
<Skeleton className="h-4 w-32" />
|
|
<Skeleton className="h-3 w-24" />
|
|
</div>
|
|
</div>
|
|
<Skeleton className="h-6 w-12 flex-1 rounded-full" />
|
|
<Skeleton className="h-4 w-20 flex-1" />
|
|
<Skeleton className="h-4 w-24 flex-1" />
|
|
<Skeleton className="h-6 w-24 flex-1 rounded-full" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : filteredCustomers.length === 0 ? (
|
|
<div className="p-12 text-center">
|
|
<div className="w-16 h-16 bg-muted/50 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Users className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2 text-foreground">
|
|
{searchQuery ? "No matching customers" : "No customers yet"}
|
|
</h3>
|
|
<p className="text-muted-foreground max-w-sm mx-auto mb-6">
|
|
{searchQuery
|
|
? "We couldn't find any customers matching your search criteria."
|
|
: "Once you have customers placing orders, they will appear here."}
|
|
</p>
|
|
{searchQuery && (
|
|
<Button variant="outline" size="sm" onClick={clearSearch}>
|
|
Clear Search
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader className="bg-muted/50">
|
|
<TableRow className="hover:bg-transparent border-border/50">
|
|
<TableHead className="w-[200px]">Customer</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer w-[100px] text-center hover:text-primary transition-colors"
|
|
onClick={() => handleSort("totalOrders")}
|
|
>
|
|
<div className="flex items-center justify-center gap-1">
|
|
Orders
|
|
<ArrowUpDown className="h-3 w-3" />
|
|
</div>
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer w-[150px] text-center hover:text-primary transition-colors"
|
|
onClick={() => handleSort("totalSpent")}
|
|
>
|
|
<div className="flex items-center justify-center gap-1">
|
|
Total Spent
|
|
<ArrowUpDown className="h-3 w-3" />
|
|
</div>
|
|
</TableHead>
|
|
<TableHead
|
|
className="cursor-pointer w-[180px] text-center hover:text-primary transition-colors"
|
|
onClick={() => handleSort("lastOrderDate")}
|
|
>
|
|
<div className="flex items-center justify-center gap-1">
|
|
Last Order
|
|
<ArrowUpDown className="h-3 w-3" />
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="w-[250px] text-center">Status</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<AnimatePresence mode="popLayout">
|
|
{filteredCustomers.map((customer, index) => (
|
|
<motion.tr
|
|
key={customer.userId}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.2, delay: index * 0.03 }}
|
|
className={`cursor-pointer group hover:bg-muted/50 border-b border-border/50 transition-colors ${!customer.hasOrders ? "bg-primary/5 hover:bg-primary/10" : ""}`}
|
|
onClick={() => setSelectedCustomer(customer)}
|
|
>
|
|
<TableCell className="py-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`h-9 w-9 rounded-full flex items-center justify-center text-xs font-medium ${!customer.hasOrders ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'
|
|
}`}>
|
|
{customer.telegramUsername ? customer.telegramUsername.substring(0, 2).toUpperCase() : 'ID'}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium flex items-center gap-2">
|
|
@{customer.telegramUsername || "Unknown"}
|
|
{!customer.hasOrders && (
|
|
<Badge variant="outline" className="h-5 px-1.5 text-[10px] bg-primary/10 text-primary border-primary/20">
|
|
New
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground flex items-center font-mono mt-0.5">
|
|
<span className="opacity-50 select-none">ID:</span>
|
|
<span className="ml-1">{customer.telegramUserId}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Badge variant="secondary" className="font-mono font-normal">
|
|
{customer.totalOrders}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-center font-mono text-sm">
|
|
{formatCurrency(customer.totalSpent)}
|
|
</TableCell>
|
|
<TableCell className="text-center text-sm text-muted-foreground">
|
|
{customer.lastOrderDate ? (
|
|
<div className="flex items-center justify-center gap-1.5">
|
|
<Calendar className="h-3 w-3 opacity-70" />
|
|
{formatDate(customer.lastOrderDate).split(",")[0]}
|
|
</div>
|
|
) : "Never"}
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
{customer.hasOrders ? (
|
|
<div className="flex justify-center flex-wrap gap-1">
|
|
{customer.ordersByStatus.paid > 0 && (
|
|
<Badge className="bg-blue-500/15 text-blue-500 hover:bg-blue-500/25 border-blue-500/20 h-5 px-1.5 text-[10px]">
|
|
{customer.ordersByStatus.paid} Paid
|
|
</Badge>
|
|
)}
|
|
{customer.ordersByStatus.completed > 0 && (
|
|
<Badge className="bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25 border-emerald-500/20 h-5 px-1.5 text-[10px]">
|
|
{customer.ordersByStatus.completed} Done
|
|
</Badge>
|
|
)}
|
|
{customer.ordersByStatus.shipped > 0 && (
|
|
<Badge className="bg-amber-500/15 text-amber-500 hover:bg-amber-500/25 border-amber-500/20 h-5 px-1.5 text-[10px]">
|
|
{customer.ordersByStatus.shipped} Ship
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground italic">No activity</span>
|
|
)}
|
|
</TableCell>
|
|
</motion.tr>
|
|
))}
|
|
</AnimatePresence>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
|
|
<div className="p-4 border-t border-border/50 bg-background/50 flex justify-between items-center">
|
|
<div className="text-sm text-muted-foreground">
|
|
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}
|
|
className="h-8"
|
|
>
|
|
<ChevronLeft className="h-3 w-3 mr-1" />
|
|
Previous
|
|
</Button>
|
|
|
|
{totalPages > 2 ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" className="h-8 px-2">
|
|
<MoreHorizontal className="h-3 w-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="center" className="max-h-60 overflow-y-auto">
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => (
|
|
<DropdownMenuItem
|
|
key={pageNum}
|
|
onClick={() => handlePageChange(pageNum)}
|
|
className={pageNum === page ? 'bg-primary/10 text-primary' : ''}
|
|
>
|
|
Page {pageNum}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
|
|
disabled={page === totalPages || loading}
|
|
className="h-8"
|
|
>
|
|
Next
|
|
<ChevronRight className="h-3 w-3 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Customer Details Dialog */}
|
|
<AnimatePresence>
|
|
{selectedCustomer && (
|
|
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80] bg-black/80 backdrop-blur-xl border-white/10 shadow-2xl p-0 gap-0">
|
|
<DialogHeader className="p-6 pb-2 border-b border-white/5">
|
|
<DialogTitle className="text-xl flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-indigo-500/20">
|
|
{selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'}
|
|
</div>
|
|
<div>
|
|
<div className="font-bold">Customer Details</div>
|
|
<div className="text-sm font-normal text-muted-foreground flex items-center gap-2">
|
|
@{selectedCustomer.telegramUsername || "Unknown"}
|
|
<span className="w-1 h-1 rounded-full bg-indigo-500" />
|
|
<span className="font-mono text-xs opacity-70">ID: {selectedCustomer.telegramUserId}</span>
|
|
</div>
|
|
</div>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="p-6 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Customer Information */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
className="space-y-4"
|
|
>
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Contact Info</h3>
|
|
<div className="rounded-xl border border-white/5 bg-white/5 p-4 space-y-4 backdrop-blur-sm">
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between items-center text-sm group">
|
|
<div className="text-muted-foreground flex items-center gap-2">
|
|
<Users className="h-4 w-4 opacity-50" />
|
|
Username
|
|
</div>
|
|
<div className="font-medium text-white group-hover:text-primary transition-colors">@{selectedCustomer.telegramUsername || "Unknown"}</div>
|
|
</div>
|
|
<div className="flex justify-between items-center text-sm group">
|
|
<div className="text-muted-foreground flex items-center gap-2">
|
|
<CreditCard className="h-4 w-4 opacity-50" />
|
|
User ID
|
|
</div>
|
|
<div className="font-medium font-mono text-white/80">{selectedCustomer.telegramUserId}</div>
|
|
</div>
|
|
<div className="flex justify-between items-center text-sm group">
|
|
<div className="text-muted-foreground flex items-center gap-2">
|
|
<MessageCircle className="h-4 w-4 opacity-50" />
|
|
Chat ID
|
|
</div>
|
|
<div className="font-medium font-mono text-white/80">{selectedCustomer.chatId}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full border-indigo-500/20 hover:bg-indigo-500/10 hover:text-indigo-400 text-indigo-300 transition-colors"
|
|
onClick={() => {
|
|
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank');
|
|
}}
|
|
>
|
|
<MessageCircle className="h-4 w-4 mr-2" />
|
|
Open Telegram Chat
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Order Statistics */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.2 }}
|
|
className="space-y-4"
|
|
>
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Lifetime Stats</h3>
|
|
<div className="rounded-xl border border-white/5 bg-gradient-to-br from-white/5 to-white/[0.02] p-4 space-y-4 backdrop-blur-sm">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="bg-emerald-500/10 rounded-lg p-3 border border-emerald-500/20">
|
|
<div className="text-xs text-emerald-400/70 uppercase font-medium mb-1">Total Spent</div>
|
|
<div className="text-xl font-bold text-emerald-400">{formatCurrency(selectedCustomer.totalSpent)}</div>
|
|
</div>
|
|
<div className="bg-blue-500/10 rounded-lg p-3 border border-blue-500/20">
|
|
<div className="text-xs text-blue-400/70 uppercase font-medium mb-1">Total Orders</div>
|
|
<div className="text-xl font-bold text-blue-400">{selectedCustomer.totalOrders}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 pt-2 border-t border-white/5">
|
|
<div className="flex justify-between items-center text-sm">
|
|
<div className="text-muted-foreground text-xs">First Order</div>
|
|
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
|
|
{formatDate(selectedCustomer.firstOrderDate)}
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between items-center text-sm">
|
|
<div className="text-muted-foreground text-xs">Last Activity</div>
|
|
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
|
|
{formatDate(selectedCustomer.lastOrderDate)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Order Status Breakdown */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="space-y-4"
|
|
>
|
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Order History Breakdown</h3>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<div className="bg-blue-500/5 hover:bg-blue-500/10 transition-colors rounded-xl border border-blue-500/20 p-4 text-center group">
|
|
<ShoppingBag className="h-5 w-5 text-blue-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
|
|
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.paid}</p>
|
|
<p className="text-xs font-medium text-blue-400/70 uppercase">Paid</p>
|
|
</div>
|
|
<div className="bg-purple-500/5 hover:bg-purple-500/10 transition-colors rounded-xl border border-purple-500/20 p-4 text-center group">
|
|
<Loader2 className="h-5 w-5 text-purple-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
|
|
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.acknowledged}</p>
|
|
<p className="text-xs font-medium text-purple-400/70 uppercase">Processing</p>
|
|
</div>
|
|
<div className="bg-amber-500/5 hover:bg-amber-500/10 transition-colors rounded-xl border border-amber-500/20 p-4 text-center group">
|
|
<Truck className="h-5 w-5 text-amber-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
|
|
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.shipped}</p>
|
|
<p className="text-xs font-medium text-amber-400/70 uppercase">Shipped</p>
|
|
</div>
|
|
<div className="bg-emerald-500/5 hover:bg-emerald-500/10 transition-colors rounded-xl border border-emerald-500/20 p-4 text-center group">
|
|
<CheckCircle className="h-5 w-5 text-emerald-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
|
|
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.completed}</p>
|
|
<p className="text-xs font-medium text-emerald-400/70 uppercase">Completed</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<DialogFooter className="p-6 pt-2 border-t border-white/5 bg-white/[0.02]">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setSelectedCustomer(null)}
|
|
className="hover:bg-white/5 text-muted-foreground hover:text-white"
|
|
>
|
|
Close Profile
|
|
</Button>
|
|
<Button
|
|
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
|
|
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg shadow-indigo-500/25 border-0"
|
|
>
|
|
<MessageCircle className="h-4 w-4 mr-2" />
|
|
Message Customer
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|