Replaced mock data in the admin ban page with real API calls for fetching, banning, and unbanning users. Improved form validation, loading states, and search functionality. Updated banned users table to show live data and added confirmation dialogs for ban/unban actions. Enhanced user status display in the admin users page with tooltips for blocked reasons.
231 lines
9.0 KiB
TypeScript
231 lines
9.0 KiB
TypeScript
import React 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, UserX, Package, DollarSign } from "lucide-react";
|
|
import { fetchServer } from "@/lib/api";
|
|
|
|
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 async function AdminUsersPage() {
|
|
let users: TelegramUser[] = [];
|
|
let error: string | null = null;
|
|
|
|
try {
|
|
users = await fetchServer<TelegramUser[]>("/admin/users");
|
|
} catch (err) {
|
|
console.error("Failed to fetch users:", err);
|
|
error = "Failed to load users";
|
|
}
|
|
|
|
if (error) {
|
|
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</p>
|
|
</div>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-center text-red-500">
|
|
<p>{error}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const usersWithOrders = users.filter(u => u.totalOrders > 0);
|
|
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-4">
|
|
<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>
|
|
<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>
|
|
<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>
|
|
<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">Blocked Users</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<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" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<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>
|
|
{users.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>
|
|
);
|
|
}
|
|
|