Enhance admin dashboard analytics and system status
Added 'Year to Date' and 'Last Year' filters to analytics, and improved summary cards to show total revenue and orders for the selected period. Refactored SystemStatusCard to include a debug view with detailed system metrics and raw JSON response. Updated nav-item active state detection for more precision and improved navigation handling. Removed redundant recent activity card from admin status page.
This commit is contained in:
@@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Server, Database, Cpu, HardDrive, Activity } from "lucide-react";
|
import { Server, Database, Cpu, HardDrive, Activity } from "lucide-react";
|
||||||
import { fetchServer } from "@/lib/api";
|
import { fetchServer } from "@/lib/api";
|
||||||
|
import SystemStatusCard from "@/components/admin/SystemStatusCard";
|
||||||
|
|
||||||
interface SystemStatus {
|
interface SystemStatus {
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number;
|
||||||
@@ -74,6 +75,8 @@ export default async function AdminStatusPage() {
|
|||||||
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
|
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SystemStatusCard />
|
||||||
|
|
||||||
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{/* Server Status */}
|
{/* Server Status */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -172,39 +175,6 @@ export default async function AdminStatusPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Activity */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recent System Activity</CardTitle>
|
|
||||||
<CardDescription>Latest system events and changes</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium">System health check completed</p>
|
|
||||||
<p className="text-xs text-muted-foreground">2 minutes ago</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium">Database backup completed</p>
|
|
||||||
<p className="text-xs text-muted-foreground">1 hour ago</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium">High memory usage detected</p>
|
|
||||||
<p className="text-xs text-muted-foreground">3 hours ago</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,8 @@ export default function AdminAnalytics() {
|
|||||||
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
||||||
<SelectItem value="7days">Last 7 days</SelectItem>
|
<SelectItem value="7days">Last 7 days</SelectItem>
|
||||||
<SelectItem value="30days">Last 30 days</SelectItem>
|
<SelectItem value="30days">Last 30 days</SelectItem>
|
||||||
|
<SelectItem value="ytd">Year to Date</SelectItem>
|
||||||
|
<SelectItem value="year">Last Year</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -534,20 +536,25 @@ export default function AdminAnalytics() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
{/* Calculate totals for the selected period */}
|
||||||
|
{analyticsData?.orders?.dailyOrders && analyticsData?.revenue?.dailyRevenue && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg">
|
||||||
|
<div className="text-sm font-medium mb-1">Total Revenue</div>
|
||||||
|
<div className="text-2xl font-bold text-green-600">
|
||||||
|
{formatCurrency(
|
||||||
|
analyticsData.revenue.dailyRevenue.reduce((sum, day) => sum + (day.amount || 0), 0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="bg-muted/50 p-4 rounded-lg">
|
<div className="bg-muted/50 p-4 rounded-lg">
|
||||||
<div className="text-sm font-medium mb-1">Total Orders</div>
|
<div className="text-sm font-medium mb-1">Total Orders</div>
|
||||||
<div className="text-2xl font-bold">{analyticsData?.orders?.total?.toLocaleString() || '0'}</div>
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
</div>
|
{analyticsData.orders.dailyOrders.reduce((sum, day) => sum + (day.count || 0), 0).toLocaleString()}
|
||||||
<div className="bg-muted/50 p-4 rounded-lg">
|
|
||||||
<div className="text-sm font-medium mb-1">Pending Orders</div>
|
|
||||||
<div className="text-2xl font-bold">{analyticsData?.orders?.pending?.toLocaleString() || '0'}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted/50 p-4 rounded-lg">
|
|
||||||
<div className="text-sm font-medium mb-1">Completed Orders</div>
|
|
||||||
<div className="text-2xl font-bold">{analyticsData?.orders?.completed?.toLocaleString() || '0'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchClient } from "@/lib/api-client";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number;
|
||||||
@@ -16,9 +18,18 @@ function formatDuration(seconds: number) {
|
|||||||
return `${h}h ${m}m ${s}s`;
|
return `${h}h ${m}m ${s}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
export default function SystemStatusCard() {
|
export default function SystemStatusCard() {
|
||||||
const [data, setData] = useState<Status | null>(null);
|
const [data, setData] = useState<Status | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
@@ -35,13 +46,23 @@ export default function SystemStatusCard() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-medium">System status</h2>
|
<h2 className="font-medium">System status</h2>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Uptime, versions, environment</p>
|
<p className="text-sm text-muted-foreground mt-1">Uptime, versions, environment</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">OK</span>
|
<span className="text-xs px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400">OK</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
|
>
|
||||||
|
{showDebug ? 'Hide' : 'Show'} Debug
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-sm text-muted-foreground mt-3">{error}</p>}
|
{error && <p className="text-sm text-muted-foreground mt-3">{error}</p>}
|
||||||
@@ -67,6 +88,63 @@ export default function SystemStatusCard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showDebug && data && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Debug: Raw System Status Data</CardTitle>
|
||||||
|
<CardDescription>Complete system status response from backend</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4 text-xs font-mono">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold mb-2">Memory Usage:</div>
|
||||||
|
<div className="pl-4 space-y-1">
|
||||||
|
<div>RSS (Resident Set Size): {formatBytes(data.memory?.rss || 0)}</div>
|
||||||
|
<div>Heap Total: {formatBytes(data.memory?.heapTotal || 0)}</div>
|
||||||
|
<div>Heap Used: {formatBytes(data.memory?.heapUsed || 0)}</div>
|
||||||
|
<div>External: {formatBytes(data.memory?.external || 0)}</div>
|
||||||
|
<div>Array Buffers: {formatBytes(data.memory?.arrayBuffers || 0)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold mb-2">Versions:</div>
|
||||||
|
<div className="pl-4 space-y-1">
|
||||||
|
{Object.entries(data.versions || {}).map(([key, value]) => (
|
||||||
|
<div key={key}>{key}: {value}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold mb-2">Counts:</div>
|
||||||
|
<div className="pl-4 space-y-1">
|
||||||
|
<div>Vendors: {data.counts?.vendors || 0}</div>
|
||||||
|
<div>Orders: {data.counts?.orders || 0}</div>
|
||||||
|
<div>Products: {data.counts?.products || 0}</div>
|
||||||
|
<div>Chats: {data.counts?.chats || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold mb-2">Uptime:</div>
|
||||||
|
<div className="pl-4">
|
||||||
|
{data.uptimeSeconds} seconds ({formatDuration(data.uptimeSeconds)})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className="mt-4">
|
||||||
|
<summary className="font-semibold cursor-pointer">Full JSON Response</summary>
|
||||||
|
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
|
||||||
|
{JSON.stringify(data, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ interface NavItemProps {
|
|||||||
|
|
||||||
export const NavItem: React.FC<NavItemProps> = ({ href, icon: Icon, children, onClick }) => {
|
export const NavItem: React.FC<NavItemProps> = ({ href, icon: Icon, children, onClick }) => {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const isActive = pathname === href || (href !== '/dashboard' && pathname?.startsWith(href))
|
// More precise active state detection - exact match or starts with href followed by /
|
||||||
|
const isActive = pathname === href || (href !== '/dashboard' && pathname?.startsWith(href + '/'))
|
||||||
const isNavigatingRef = useRef(false)
|
const isNavigatingRef = useRef(false)
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
@@ -26,26 +27,18 @@ export const NavItem: React.FC<NavItemProps> = ({ href, icon: Icon, children, on
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already on this page, just close mobile menu if needed
|
|
||||||
if (isActive) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (onClick) onClick()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as navigating to prevent double-clicks
|
// Mark as navigating to prevent double-clicks
|
||||||
isNavigatingRef.current = true
|
isNavigatingRef.current = true
|
||||||
|
|
||||||
// Call onClick handler (for mobile menu closing) - don't block navigation
|
// Always allow navigation - close mobile menu if needed
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
// Use setTimeout to ensure navigation happens first
|
onClick()
|
||||||
setTimeout(() => onClick(), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset flag after navigation completes
|
// Reset flag after navigation completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isNavigatingRef.current = false
|
isNavigatingRef.current = false
|
||||||
}, 300)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user