Implemented comprehensive Chromebook-specific fixes including viewport adjustments, enhanced touch and keyboard detection, improved scrolling and keyboard navigation hooks, and extensive CSS optimizations for better usability. Updated chat and dashboard interfaces for larger touch targets, better focus management, and responsive layouts. Added documentation in docs/CHROMEBOOK-FIXES.md and new hooks for Chromebook scroll and keyboard handling.
525 lines
20 KiB
TypeScript
525 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Button } from "@/components/ui/button";
|
|
import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart,
|
|
TrendingUp, TrendingDown, DollarSign, Package } from "lucide-react";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
|
|
// API response data structure
|
|
interface AnalyticsData {
|
|
vendors?: {
|
|
total?: number;
|
|
newToday?: number;
|
|
newThisWeek?: number;
|
|
activeToday?: number;
|
|
active?: number;
|
|
stores?: number;
|
|
dailyGrowth?: { date: string; count: number }[];
|
|
};
|
|
orders?: {
|
|
total?: number;
|
|
totalToday?: number;
|
|
totalThisWeek?: number;
|
|
pending?: number;
|
|
completed?: number;
|
|
dailyOrders?: { date: string; count: number }[];
|
|
};
|
|
revenue?: {
|
|
total?: number;
|
|
today?: number;
|
|
thisWeek?: number;
|
|
dailyRevenue?: { date: string; amount: number }[];
|
|
};
|
|
engagement?: {
|
|
totalMessages?: number;
|
|
activeChats?: number;
|
|
dailyMessages?: { date: string; count: number }[];
|
|
};
|
|
products?: {
|
|
total?: number;
|
|
recent?: number;
|
|
};
|
|
stores?: {
|
|
total?: number;
|
|
active?: number;
|
|
};
|
|
sessions?: {
|
|
total?: number;
|
|
active?: number;
|
|
};
|
|
}
|
|
|
|
export default function AdminAnalytics() {
|
|
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dateRange, setDateRange] = useState("7days");
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const fetchAnalyticsData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setErrorMessage(null);
|
|
|
|
const token = document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("Authorization="))
|
|
?.split("=")[1];
|
|
|
|
const response = await fetch(`/api/admin/analytics?range=${dateRange}`, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch analytics data");
|
|
}
|
|
|
|
const data = await response.json();
|
|
setAnalyticsData(data);
|
|
} catch (error) {
|
|
console.error("Error fetching analytics data:", error);
|
|
setErrorMessage("Failed to load analytics data. Please try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchAnalyticsData();
|
|
}, [dateRange]);
|
|
|
|
const handleRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchAnalyticsData();
|
|
};
|
|
|
|
if (loading && !analyticsData) {
|
|
return (
|
|
<div className="flex justify-center my-8">
|
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Chart component for line/area charts
|
|
const Chart = ({ data, valueKey = "count", color = "#3b82f6", height = 200 }:
|
|
{ data: any[]; valueKey?: string; color?: string; height?: number }) => {
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<div className="w-full flex items-center justify-center text-muted-foreground" style={{ height: `${height}px` }}>
|
|
No data available
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Find min and max for scaling
|
|
const values = data.map(d => d[valueKey] || 0);
|
|
const max = Math.max(...values, 1);
|
|
const min = Math.min(...values, 0);
|
|
const range = max - min || 1;
|
|
|
|
return (
|
|
<div className="w-full relative" style={{ height: `${height}px` }}>
|
|
<div className="absolute inset-0 flex items-end">
|
|
{data.map((item, index) => {
|
|
const value = item[valueKey] || 0;
|
|
const normalizedHeight = ((value - min) / range) * 90 + 10; // Scale to 10%-100%
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="group flex-1 relative"
|
|
>
|
|
<div
|
|
className="w-full rounded-t transition-all duration-200"
|
|
style={{
|
|
height: `${normalizedHeight}%`,
|
|
backgroundColor: color,
|
|
opacity: 0.7
|
|
}}
|
|
/>
|
|
<div className="absolute bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 bg-background text-xs text-center rounded p-1 transform -translate-y-full pointer-events-none z-10 transition-opacity">
|
|
{valueKey === "amount" ?
|
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) :
|
|
value}
|
|
<div className="text-[10px] text-muted-foreground">
|
|
{new Date(item.date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-border"></div>
|
|
|
|
{/* X-axis labels */}
|
|
<div className="absolute bottom-[-24px] left-0 right-0 flex justify-between text-xs text-muted-foreground">
|
|
<span>{new Date(data[0]?.date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}</span>
|
|
<span>{new Date(data[data.length - 1]?.date).toLocaleDateString('en-US', {month: 'short', day: 'numeric'})}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Trend indicator component for metric cards
|
|
const TrendIndicator = ({ current, previous }: { current: number, previous: number }) => {
|
|
if (!current || !previous) return null;
|
|
|
|
const percentChange = ((current - previous) / previous) * 100;
|
|
|
|
if (Math.abs(percentChange) < 0.1) return null;
|
|
|
|
return (
|
|
<div className={`flex items-center text-xs font-medium ${percentChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
|
{percentChange >= 0 ?
|
|
<TrendingUp className="h-3 w-3 mr-1" /> :
|
|
<TrendingDown className="h-3 w-3 mr-1" />}
|
|
{Math.abs(percentChange).toFixed(1)}%
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Format currency
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: 0
|
|
}).format(value);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{errorMessage && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Error</AlertTitle>
|
|
<AlertDescription>{errorMessage}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">Dashboard Analytics</h2>
|
|
<p className="text-muted-foreground">Overview of your marketplace performance</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Select
|
|
value={dateRange}
|
|
onValueChange={setDateRange}
|
|
>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue placeholder="Last 7 days" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
|
<SelectItem value="7days">Last 7 days</SelectItem>
|
|
<SelectItem value="30days">Last 30 days</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{/* Orders Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
|
|
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.orders?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>Today: {analyticsData?.orders?.totalToday || 0}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.orders?.totalToday || 0}
|
|
previous={(analyticsData?.orders?.total || 0) / 30} // Rough estimate
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.orders?.dailyOrders && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.orders.dailyOrders}
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Revenue Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{formatCurrency(analyticsData?.revenue?.total || 0)}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>Today: {formatCurrency(analyticsData?.revenue?.today || 0)}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.revenue?.today || 0}
|
|
previous={(analyticsData?.revenue?.total || 0) / 30} // Rough estimate
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.revenue?.dailyRevenue && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.revenue.dailyRevenue}
|
|
valueKey="amount"
|
|
color="#10b981"
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Vendors Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Vendors</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.vendors?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
|
<TrendIndicator
|
|
current={analyticsData?.vendors?.newToday || 0}
|
|
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7} // Average per day
|
|
/>
|
|
</div>
|
|
|
|
{analyticsData?.vendors?.dailyGrowth && (
|
|
<div className="mt-3 h-10">
|
|
<Chart
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
color="#8b5cf6"
|
|
height={40}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Products Card */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-sm font-medium">Products</CardTitle>
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{analyticsData?.products?.total?.toLocaleString() || '0'}
|
|
</div>
|
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
|
<span>New This Week: {analyticsData?.products?.recent || 0}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Tabs defaultValue="orders" className="mt-6">
|
|
<TabsList>
|
|
<TabsTrigger value="orders">Orders</TabsTrigger>
|
|
<TabsTrigger value="revenue">Revenue</TabsTrigger>
|
|
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
|
<TabsTrigger value="engagement">Engagement</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="orders" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Order Trends</CardTitle>
|
|
<CardDescription>
|
|
Daily order volume over the selected time period
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.orders?.dailyOrders ? (
|
|
<Chart
|
|
data={analyticsData.orders.dailyOrders}
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No order data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Orders</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.orders?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<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>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="revenue" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Revenue Trends</CardTitle>
|
|
<CardDescription>
|
|
Daily revenue over the selected time period
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.revenue?.dailyRevenue ? (
|
|
<Chart
|
|
data={analyticsData.revenue.dailyRevenue}
|
|
valueKey="amount"
|
|
color="#10b981"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No revenue data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 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">{formatCurrency(analyticsData?.revenue?.total || 0)}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Today's Revenue</div>
|
|
<div className="text-2xl font-bold">{formatCurrency(analyticsData?.revenue?.today || 0)}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">This Week's Revenue</div>
|
|
<div className="text-2xl font-bold">{formatCurrency(analyticsData?.revenue?.thisWeek || 0)}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="vendors" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Vendor Growth</CardTitle>
|
|
<CardDescription>
|
|
New vendor registrations over time
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.vendors?.dailyGrowth ? (
|
|
<Chart
|
|
data={analyticsData.vendors.dailyGrowth}
|
|
color="#8b5cf6"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No vendor data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Vendors</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">New Today</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.newToday?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">New This Week</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.newThisWeek?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="engagement" className="mt-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>User Engagement</CardTitle>
|
|
<CardDescription>
|
|
Chat and message activity
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{analyticsData?.engagement?.dailyMessages ? (
|
|
<Chart
|
|
data={analyticsData.engagement.dailyMessages}
|
|
color="#ec4899"
|
|
height={300}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
|
No engagement data available
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Total Messages</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.engagement?.totalMessages?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Active Chats</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.engagement?.activeChats?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<div className="text-sm font-medium mb-1">Sessions</div>
|
|
<div className="text-2xl font-bold">{analyticsData?.sessions?.active?.toLocaleString() || '0'} / {analyticsData?.sessions?.total?.toLocaleString() || '0'}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|