Files
ember-market-frontend/app/dashboard/admin/users/page.tsx
g 98f14304b7 Add returning customers stat to admin users page
Introduces a new 'Returning Customers' card to the admin users dashboard, showing the count and percentage of users with more than one order. Also updates the grid layout to accommodate the new card and imports the necessary icon.
2025-12-01 13:28:04 +00:00

308 lines
12 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat } from "lucide-react";
import { fetchClient } from "@/lib/api-client";
import { useToast } from "@/hooks/use-toast";
interface TelegramUser {
telegramUserId: string;
telegramUsername: string;
totalOrders: number;
totalSpent: number;
paidOrders: number;
completedOrders: number;
firstOrderDate: string | null;
lastOrderDate: string | null;
isBlocked: boolean;
blockedReason: string | null;
createdAt?: string;
}
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
}).format(amount);
}
export default function AdminUsersPage() {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<TelegramUser[]>([]);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
const data = await fetchClient<TelegramUser[]>("/admin/users");
setUsers(data);
} catch (error: any) {
console.error("Failed to fetch users:", error);
toast({
title: "Error",
description: error.message || "Failed to load users",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const filteredUsers = users.filter((user) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
user.telegramUserId.toLowerCase().includes(query) ||
user.telegramUsername.toLowerCase().includes(query)
);
});
const usersWithOrders = users.filter(u => u.totalOrders > 0);
const returningCustomers = users.filter(u => u.totalOrders > 1);
const blockedUsers = users.filter(u => u.isBlocked);
const totalSpent = users.reduce((sum, u) => sum + u.totalSpent, 0);
const totalOrders = users.reduce((sum, u) => sum + u.totalOrders, 0);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Telegram Users</h1>
<p className="text-sm text-muted-foreground mt-1">Manage Telegram user accounts and view statistics</p>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-5">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{users.length}</div>
<p className="text-xs text-muted-foreground">Registered users</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Users with Orders</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{usersWithOrders.length}</div>
<p className="text-xs text-muted-foreground">
{users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{formatCurrency(totalSpent)}</div>
<p className="text-xs text-muted-foreground">{totalOrders} total orders</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Returning Customers</CardTitle>
<Repeat className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{returningCustomers.length}</div>
<p className="text-xs text-muted-foreground">
{usersWithOrders.length > 0 ? Math.round((returningCustomers.length / usersWithOrders.length) * 100) : 0}% of customers
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Blocked Users</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{blockedUsers.length}</div>
<p className="text-xs text-muted-foreground">
{users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate
</p>
</>
)}
</CardContent>
</Card>
</div>
{/* Search and Filters */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>User Management</CardTitle>
<CardDescription>View and manage all Telegram user accounts</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
className="pl-8 w-64"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{searchQuery ? "No users found matching your search" : "No users found"}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>User ID</TableHead>
<TableHead>Username</TableHead>
<TableHead>Orders</TableHead>
<TableHead>Total Spent</TableHead>
<TableHead>Status</TableHead>
<TableHead>First Order</TableHead>
<TableHead>Last Order</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.telegramUserId}>
<TableCell>
<div className="font-mono text-sm">{user.telegramUserId}</div>
</TableCell>
<TableCell>
<div className="font-medium">
{user.telegramUsername !== "Unknown" ? `@${user.telegramUsername}` : "Unknown"}
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-muted-foreground" />
<span>{user.totalOrders}</span>
{user.completedOrders > 0 && (
<Badge variant="secondary" className="text-xs">
{user.completedOrders} completed
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<DollarSign className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{formatCurrency(user.totalSpent)}</span>
</div>
</TableCell>
<TableCell>
{user.isBlocked ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive">
<Ban className="h-3 w-3 mr-1" />
Blocked
</Badge>
</TooltipTrigger>
{user.blockedReason && (
<TooltipContent>
<p className="max-w-xs">{user.blockedReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : user.totalOrders > 0 ? (
<Badge variant="default">Active</Badge>
) : (
<Badge variant="secondary">No Orders</Badge>
)}
</TableCell>
<TableCell>
{user.firstOrderDate
? new Date(user.firstOrderDate).toLocaleDateString()
: 'N/A'}
</TableCell>
<TableCell>
{user.lastOrderDate
? new Date(user.lastOrderDate).toLocaleDateString()
: 'N/A'}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end space-x-2">
{!user.isBlocked ? (
<Button variant="outline" size="sm">
<Ban className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" size="sm">
<UserCheck className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}