718 lines
35 KiB
TypeScript
718 lines
35 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[]>([]);
|
|
// State for browser detection
|
|
const [isFirefox, setIsFirefox] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setIsFirefox(navigator.userAgent.toLowerCase().indexOf("firefox") > -1);
|
|
}, []);
|
|
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>
|
|
{isFirefox ? (
|
|
filteredCustomers.map((customer, index) => (
|
|
<motion.tr
|
|
key={customer.userId}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 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 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>
|
|
);
|
|
}
|