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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Search, Ban, UserCheck, UserX, Package, DollarSign } from "lucide-react";
|
import { Search, Ban, UserCheck, Package, DollarSign, Loader2 } from "lucide-react";
|
||||||
import { fetchServer } from "@/lib/api";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface TelegramUser {
|
interface TelegramUser {
|
||||||
telegramUserId: string;
|
telegramUserId: string;
|
||||||
@@ -29,34 +32,41 @@ function formatCurrency(amount: number): string {
|
|||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AdminUsersPage() {
|
export default function AdminUsersPage() {
|
||||||
let users: TelegramUser[] = [];
|
const { toast } = useToast();
|
||||||
let error: string | null = null;
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [users, setUsers] = useState<TelegramUser[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
try {
|
useEffect(() => {
|
||||||
users = await fetchServer<TelegramUser[]>("/admin/users");
|
fetchUsers();
|
||||||
} catch (err) {
|
}, []);
|
||||||
console.error("Failed to fetch users:", err);
|
|
||||||
error = "Failed to load users";
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="space-y-6">
|
user.telegramUserId.toLowerCase().includes(query) ||
|
||||||
<div>
|
user.telegramUsername.toLowerCase().includes(query)
|
||||||
<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 usersWithOrders = users.filter(u => u.totalOrders > 0);
|
||||||
const blockedUsers = users.filter(u => u.isBlocked);
|
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>
|
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{users.length}</div>
|
{loading ? (
|
||||||
<p className="text-xs text-muted-foreground">Registered users</p>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -86,10 +104,18 @@ export default async function AdminUsersPage() {
|
|||||||
<CardTitle className="text-sm font-medium">Users with Orders</CardTitle>
|
<CardTitle className="text-sm font-medium">Users with Orders</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{usersWithOrders.length}</div>
|
{loading ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center justify-center py-4">
|
||||||
{users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</p>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -97,8 +123,16 @@ export default async function AdminUsersPage() {
|
|||||||
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{formatCurrency(totalSpent)}</div>
|
{loading ? (
|
||||||
<p className="text-xs text-muted-foreground">{totalOrders} total orders</p>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -106,10 +140,18 @@ export default async function AdminUsersPage() {
|
|||||||
<CardTitle className="text-sm font-medium">Blocked Users</CardTitle>
|
<CardTitle className="text-sm font-medium">Blocked Users</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{blockedUsers.length}</div>
|
{loading ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<div className="flex items-center justify-center py-4">
|
||||||
{users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</p>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,106 +167,120 @@ export default async function AdminUsersPage() {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
{loading ? (
|
||||||
<TableHeader>
|
<div className="flex items-center justify-center py-12">
|
||||||
<TableRow>
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
<TableHead>User ID</TableHead>
|
</div>
|
||||||
<TableHead>Username</TableHead>
|
) : filteredUsers.length === 0 ? (
|
||||||
<TableHead>Orders</TableHead>
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
<TableHead>Total Spent</TableHead>
|
{searchQuery ? "No users found matching your search" : "No users found"}
|
||||||
<TableHead>Status</TableHead>
|
</div>
|
||||||
<TableHead>First Order</TableHead>
|
) : (
|
||||||
<TableHead>Last Order</TableHead>
|
<Table>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHeader>
|
||||||
</TableRow>
|
<TableRow>
|
||||||
</TableHeader>
|
<TableHead>User ID</TableHead>
|
||||||
<TableBody>
|
<TableHead>Username</TableHead>
|
||||||
{users.map((user) => (
|
<TableHead>Orders</TableHead>
|
||||||
<TableRow key={user.telegramUserId}>
|
<TableHead>Total Spent</TableHead>
|
||||||
<TableCell>
|
<TableHead>Status</TableHead>
|
||||||
<div className="font-mono text-sm">{user.telegramUserId}</div>
|
<TableHead>First Order</TableHead>
|
||||||
</TableCell>
|
<TableHead>Last Order</TableHead>
|
||||||
<TableCell>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
<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>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart,
|
import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart,
|
||||||
TrendingUp, TrendingDown, DollarSign, Package } from "lucide-react";
|
TrendingUp, TrendingDown, DollarSign, Package, Trophy } from "lucide-react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { fetchClient } from "@/lib/api-client";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts';
|
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts';
|
||||||
@@ -78,28 +78,6 @@ export default function AdminAnalytics() {
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
const data = await fetchClient<AnalyticsData>(`/admin/analytics?range=${dateRange}`);
|
const data = await fetchClient<AnalyticsData>(`/admin/analytics?range=${dateRange}`);
|
||||||
console.log('=== ADMIN ANALYTICS DATA ===');
|
|
||||||
console.log('Date Range:', dateRange);
|
|
||||||
console.log('Full Response:', JSON.stringify(data, null, 2));
|
|
||||||
console.log('Orders:', {
|
|
||||||
total: data?.orders?.total,
|
|
||||||
dailyOrders: data?.orders?.dailyOrders,
|
|
||||||
dailyOrdersLength: data?.orders?.dailyOrders?.length,
|
|
||||||
sample: data?.orders?.dailyOrders?.slice(0, 3)
|
|
||||||
});
|
|
||||||
console.log('Revenue:', {
|
|
||||||
total: data?.revenue?.total,
|
|
||||||
dailyRevenue: data?.revenue?.dailyRevenue,
|
|
||||||
dailyRevenueLength: data?.revenue?.dailyRevenue?.length,
|
|
||||||
sample: data?.revenue?.dailyRevenue?.slice(0, 3)
|
|
||||||
});
|
|
||||||
console.log('Vendors:', {
|
|
||||||
total: data?.vendors?.total,
|
|
||||||
dailyGrowth: data?.vendors?.dailyGrowth,
|
|
||||||
dailyGrowthLength: data?.vendors?.dailyGrowth?.length,
|
|
||||||
sample: data?.vendors?.dailyGrowth?.slice(0, 3)
|
|
||||||
});
|
|
||||||
console.log('===========================');
|
|
||||||
setAnalyticsData(data);
|
setAnalyticsData(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching analytics data:", error);
|
console.error("Error fetching analytics data:", error);
|
||||||
@@ -239,6 +217,57 @@ export default function AdminAnalytics() {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate best month from daily data (for YTD view)
|
||||||
|
const calculateBestMonth = () => {
|
||||||
|
if (dateRange !== 'ytd' || !analyticsData?.revenue?.dailyRevenue || analyticsData.revenue.dailyRevenue.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group daily revenue by month
|
||||||
|
const monthlyTotals = new Map<string, { revenue: number; orders: number }>();
|
||||||
|
|
||||||
|
analyticsData.revenue.dailyRevenue.forEach(day => {
|
||||||
|
const date = new Date(day.date);
|
||||||
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
|
||||||
|
monthlyTotals.set(monthKey, {
|
||||||
|
revenue: current.revenue + (day.amount || 0),
|
||||||
|
orders: current.orders
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also group orders by month
|
||||||
|
if (analyticsData.orders?.dailyOrders) {
|
||||||
|
analyticsData.orders.dailyOrders.forEach(day => {
|
||||||
|
const date = new Date(day.date);
|
||||||
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
|
||||||
|
monthlyTotals.set(monthKey, {
|
||||||
|
revenue: current.revenue,
|
||||||
|
orders: current.orders + (day.count || 0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the month with highest revenue
|
||||||
|
let bestMonth: { month: string; revenue: number; orders: number } | null = null;
|
||||||
|
monthlyTotals.forEach((totals, monthKey) => {
|
||||||
|
if (!bestMonth || totals.revenue > bestMonth.revenue) {
|
||||||
|
const [year, month] = monthKey.split('-');
|
||||||
|
const monthName = new Date(parseInt(year), parseInt(month) - 1, 1).toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
||||||
|
bestMonth = {
|
||||||
|
month: monthName,
|
||||||
|
revenue: totals.revenue,
|
||||||
|
orders: totals.orders
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bestMonth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bestMonth = calculateBestMonth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
@@ -350,6 +379,30 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Best Month Card (only show for YTD) */}
|
||||||
|
{bestMonth && (
|
||||||
|
<Card className="border-green-500/50 bg-green-500/5">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 rounded-full bg-green-500/20">
|
||||||
|
<Trophy className="h-5 w-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-muted-foreground">Best Month (YTD)</div>
|
||||||
|
<div className="text-xl font-bold text-green-600">{bestMonth.month}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground">Revenue</div>
|
||||||
|
<div className="text-xl font-bold">{formatCurrency(bestMonth.revenue)}</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">{bestMonth.orders.toLocaleString()} orders</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
{/* Orders Card */}
|
{/* Orders Card */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ function formatDuration(seconds: number) {
|
|||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
if (bytes < 0) return '0 Bytes'; // Handle negative values
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(k))); // Ensure i is at least 0
|
||||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
// Clamp i to valid array index
|
||||||
|
const clampedI = Math.min(i, sizes.length - 1);
|
||||||
|
return Math.round(bytes / Math.pow(k, clampedI) * 100) / 100 + ' ' + sizes[clampedI];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SystemStatusCard() {
|
export default function SystemStatusCard() {
|
||||||
@@ -36,7 +39,6 @@ export default function SystemStatusCard() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchClient<Status>("/admin/system-status");
|
const res = await fetchClient<Status>("/admin/system-status");
|
||||||
console.log(`Here is your mother fuckin data: ${JSON.stringify(res)}`);
|
|
||||||
if (mounted) setData(res);
|
if (mounted) setData(res);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (mounted) setError(e?.message || "Failed to load status");
|
if (mounted) setError(e?.message || "Failed to load status");
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
// Use dateRange if provided, otherwise fall back to timeRange
|
// Use dateRange if provided, otherwise fall back to timeRange, otherwise default to '30'
|
||||||
const response = await getProfitOverview(dateRange || timeRange || '30');
|
const periodOrRange = dateRange || (timeRange ? timeRange : '30');
|
||||||
|
const response = await getProfitOverview(periodOrRange);
|
||||||
setData(response);
|
setData(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching profit data:', error);
|
console.error('Error fetching profit data:', error);
|
||||||
|
|||||||
@@ -65,5 +65,4 @@ export const NavItem: React.FC<NavItemProps> = ({ href, icon: Icon, children, on
|
|||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "1001911",
|
"commitHash": "4ef0fd1",
|
||||||
"buildTime": "2025-11-28T20:06:53.158Z"
|
"buildTime": "2025-11-30T15:39:38.047Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user