Add Telegram Users admin page and sidebar link
Introduces a new admin page for managing Telegram users, displaying user statistics and allowing user actions. Updates the admin sidebar to include a link to the new Telegram Users page. Also updates git-info.json with the latest build metadata.
This commit is contained in:
223
app/dashboard/admin/users/page.tsx
Normal file
223
app/dashboard/admin/users/page.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
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 { 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>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
{user.isBlocked ? (
|
||||||
|
<Badge variant="destructive">
|
||||||
|
<Ban className="h-3 w-3 mr-1" />
|
||||||
|
Blocked
|
||||||
|
</Badge>
|
||||||
|
) : user.totalOrders > 0 ? (
|
||||||
|
<Badge variant="default">Active</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">No Orders</Badge>
|
||||||
|
)}
|
||||||
|
{user.blockedReason && (
|
||||||
|
<span className="text-xs text-muted-foreground">{user.blockedReason}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Home, Shield, Users, Ban, UserPlus, Package, BarChart3 } from "lucide-react"
|
import { Home, Shield, Users, Ban, UserPlus, Package, BarChart3, UserCircle } from "lucide-react"
|
||||||
|
|
||||||
export const adminSidebarConfig = [
|
export const adminSidebarConfig = [
|
||||||
{
|
{
|
||||||
@@ -12,6 +12,7 @@ export const adminSidebarConfig = [
|
|||||||
title: "User Management",
|
title: "User Management",
|
||||||
items: [
|
items: [
|
||||||
{ name: "All Vendors", href: "/dashboard/admin/vendors", icon: Users },
|
{ name: "All Vendors", href: "/dashboard/admin/vendors", icon: Users },
|
||||||
|
{ name: "Telegram Users", href: "/dashboard/admin/users", icon: UserCircle },
|
||||||
{ name: "Invite Vendor", href: "/dashboard/admin/invite", icon: UserPlus },
|
{ name: "Invite Vendor", href: "/dashboard/admin/invite", icon: UserPlus },
|
||||||
{ name: "Ban Users", href: "/dashboard/admin/ban", icon: Ban },
|
{ name: "Ban Users", href: "/dashboard/admin/ban", icon: Ban },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "f212859",
|
"commitHash": "3548cc3",
|
||||||
"buildTime": "2025-11-28T18:47:55.501Z"
|
"buildTime": "2025-11-28T20:00:38.186Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user