Improve admin analytics and user management UI
Refactored admin users page to use client-side fetching, loading states, and search functionality. Enhanced AdminAnalytics with a best month (YTD) card and removed debug logging. Improved SystemStatusCard formatting and removed console logs. Fixed profit chart period selection logic. Minor formatting fix in nav-item component.
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import React from "react";
|
||||
"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, UserX, Package, DollarSign } from "lucide-react";
|
||||
import { fetchServer } from "@/lib/api";
|
||||
import { Search, Ban, UserCheck, Package, DollarSign, Loader2 } from "lucide-react";
|
||||
import { fetchClient } from "@/lib/api-client";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface TelegramUser {
|
||||
telegramUserId: string;
|
||||
@@ -29,34 +32,41 @@ function formatCurrency(amount: number): string {
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export default async function AdminUsersPage() {
|
||||
let users: TelegramUser[] = [];
|
||||
let error: string | null = null;
|
||||
export default function AdminUsersPage() {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [users, setUsers] = useState<TelegramUser[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
try {
|
||||
users = await fetchServer<TelegramUser[]>("/admin/users");
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch users:", err);
|
||||
error = "Failed to load users";
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
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 (
|
||||
<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>
|
||||
user.telegramUserId.toLowerCase().includes(query) ||
|
||||
user.telegramUsername.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const usersWithOrders = users.filter(u => u.totalOrders > 0);
|
||||
const blockedUsers = users.filter(u => u.isBlocked);
|
||||
@@ -77,8 +87,16 @@ export default async function AdminUsersPage() {
|
||||
<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>
|
||||
{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>
|
||||
@@ -86,10 +104,18 @@ export default async function AdminUsersPage() {
|
||||
<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>
|
||||
{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>
|
||||
@@ -97,8 +123,16 @@ export default async function AdminUsersPage() {
|
||||
<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>
|
||||
{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>
|
||||
@@ -106,10 +140,18 @@ export default async function AdminUsersPage() {
|
||||
<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>
|
||||
{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>
|
||||
@@ -125,106 +167,120 @@ export default async function AdminUsersPage() {
|
||||
<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" />
|
||||
<Input
|
||||
placeholder="Search users..."
|
||||
className="pl-8 w-64"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</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>
|
||||
{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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user