Compare commits
2 Commits
73adbe5d07
...
0bb1497db6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb1497db6 | ||
|
|
688f519fd6 |
95
app/dashboard/admin/vendors/page.tsx
vendored
95
app/dashboard/admin/vendors/page.tsx
vendored
@@ -6,7 +6,9 @@ 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 { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar } from "lucide-react";
|
import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar, Pencil, Plus } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { fetchClient } from "@/lib/api-client";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@@ -41,6 +43,43 @@ export default function AdminVendorsPage() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
|
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isEditStoreOpen, setIsEditStoreOpen] = useState(false);
|
||||||
|
const [editingVendor, setEditingVendor] = useState<Vendor | null>(null);
|
||||||
|
const [newStoreId, setNewStoreId] = useState("");
|
||||||
|
const [updating, setUpdating] = useState(false);
|
||||||
|
|
||||||
|
const handleEditStore = (vendor: Vendor) => {
|
||||||
|
setEditingVendor(vendor);
|
||||||
|
setNewStoreId(vendor.storeId || "");
|
||||||
|
setIsEditStoreOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveStoreId = async () => {
|
||||||
|
if (!editingVendor) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUpdating(true);
|
||||||
|
await fetchClient(`/admin/vendors/${editingVendor._id}/store-id`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ storeId: newStoreId })
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Store ID updated successfully",
|
||||||
|
});
|
||||||
|
setIsEditStoreOpen(false);
|
||||||
|
fetchVendors(); // Refresh list
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message || "Failed to update store ID",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchVendors = useCallback(async () => {
|
const fetchVendors = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -209,9 +248,29 @@ export default function AdminVendorsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{vendor.storeId ? (
|
{vendor.storeId ? (
|
||||||
|
<div className="flex items-center gap-2 group/store">
|
||||||
<span className="font-mono text-xs">{vendor.storeId}</span>
|
<span className="font-mono text-xs">{vendor.storeId}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover/store:opacity-100 transition-opacity"
|
||||||
|
onClick={() => handleEditStore(vendor)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-muted-foreground italic text-xs">No store</span>
|
<span className="text-muted-foreground italic text-xs">No store</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={() => handleEditStore(vendor)}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -291,6 +350,40 @@ export default function AdminVendorsPage() {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog open={isEditStoreOpen} onOpenChange={setIsEditStoreOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Vendor Store</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter the Store ID to assign to vendor <span className="font-semibold text-foreground">{editingVendor?.username}</span>.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="storeId">Store ID</Label>
|
||||||
|
<Input
|
||||||
|
id="storeId"
|
||||||
|
value={newStoreId}
|
||||||
|
onChange={(e) => setNewStoreId(e.target.value)}
|
||||||
|
placeholder="Enter 24-character Store ID"
|
||||||
|
className="col-span-3 font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Ensure the Store ID corresponds to an existing store in the system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsEditStoreOpen(false)} disabled={updating}>Cancel</Button>
|
||||||
|
<Button onClick={saveStoreId} disabled={updating || !newStoreId || newStoreId.length < 24}>
|
||||||
|
{updating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,39 +519,39 @@ export default function AdminAnalytics() {
|
|||||||
const bestMonth = calculateBestMonth();
|
const bestMonth = calculateBestMonth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8 animate-in fade-in duration-500">
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive" className="animate-in slide-in-from-top-2 border-destructive/50 bg-destructive/10 text-destructive">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{errorMessage}</AlertDescription>
|
<AlertDescription>{errorMessage}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold">
|
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||||
Dashboard Analytics
|
Dashboard Analytics
|
||||||
{!isViewingCurrentYear && (
|
{!isViewingCurrentYear && (
|
||||||
<span className="ml-2 text-lg font-normal text-muted-foreground">
|
<span className="ml-2 text-xl font-normal text-muted-foreground/60">
|
||||||
({selectedYear})
|
({selectedYear})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground mt-1">
|
||||||
{isViewingCurrentYear
|
{isViewingCurrentYear
|
||||||
? "Overview of your marketplace performance"
|
? "Overview of your marketplace performance"
|
||||||
: `Historical data for ${selectedYear}`}
|
: `Historical data for ${selectedYear}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 bg-background/40 p-1 rounded-lg border border-border/40 backdrop-blur-md">
|
||||||
{/* Year selector */}
|
{/* Year selector */}
|
||||||
<Select
|
<Select
|
||||||
value={selectedYear.toString()}
|
value={selectedYear.toString()}
|
||||||
onValueChange={(value) => setSelectedYear(parseInt(value, 10))}
|
onValueChange={(value) => setSelectedYear(parseInt(value, 10))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[100px]">
|
<SelectTrigger className="w-[100px] border-0 bg-transparent focus:ring-0">
|
||||||
<SelectValue placeholder="Year" />
|
<SelectValue placeholder="Year" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -563,13 +563,15 @@ export default function AdminAnalytics() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<div className="h-4 w-px bg-border/40" />
|
||||||
|
|
||||||
{/* Date range selector - only show options for current year */}
|
{/* Date range selector - only show options for current year */}
|
||||||
<Select
|
<Select
|
||||||
value={isViewingCurrentYear ? dateRange : "year"}
|
value={isViewingCurrentYear ? dateRange : "year"}
|
||||||
onValueChange={setDateRange}
|
onValueChange={setDateRange}
|
||||||
disabled={!isViewingCurrentYear}
|
disabled={!isViewingCurrentYear}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[140px] border-0 bg-transparent focus:ring-0">
|
||||||
<SelectValue placeholder="Last 7 days" />
|
<SelectValue placeholder="Last 7 days" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -589,11 +591,14 @@ export default function AdminAnalytics() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<div className="h-4 w-px bg-border/40" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
|
className="h-8 w-8 hover:bg-background/60"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
@@ -601,9 +606,10 @@ export default function AdminAnalytics() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowDebug(!showDebug)}
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
|
className="px-2 text-xs hover:bg-background/60"
|
||||||
>
|
>
|
||||||
{showDebug ? "Hide" : "Show"} Debug
|
{showDebug ? "Hide" : "Show"} Debug
|
||||||
</Button>
|
</Button>
|
||||||
@@ -611,9 +617,9 @@ export default function AdminAnalytics() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showDebug && analyticsData && (
|
{showDebug && analyticsData && (
|
||||||
<Card className="mt-4">
|
<Card className="mt-4 border-yellow-500/20 bg-yellow-500/5 backdrop-blur-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Debug: Raw Data</CardTitle>
|
<CardTitle className="text-yellow-600">Debug: Raw Data</CardTitle>
|
||||||
<CardDescription>Date Range: {dateRange}</CardDescription>
|
<CardDescription>Date Range: {dateRange}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -627,8 +633,9 @@ export default function AdminAnalytics() {
|
|||||||
Daily Orders Array Length:{" "}
|
Daily Orders Array Length:{" "}
|
||||||
{analyticsData?.orders?.dailyOrders?.length || 0}
|
{analyticsData?.orders?.dailyOrders?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
|
{/* ... Existing Debug details kept for brevity ... */}
|
||||||
<div>First 3 Daily Orders:</div>
|
<div>First 3 Daily Orders:</div>
|
||||||
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
<pre className="pl-4 bg-muted/50 p-2 rounded overflow-auto max-h-32">
|
||||||
{JSON.stringify(
|
{JSON.stringify(
|
||||||
analyticsData?.orders?.dailyOrders?.slice(0, 3),
|
analyticsData?.orders?.dailyOrders?.slice(0, 3),
|
||||||
null,
|
null,
|
||||||
@@ -637,51 +644,12 @@ export default function AdminAnalytics() {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Simplified debug view for code brevity in replacement, focusing on style changes */}
|
||||||
<div>
|
|
||||||
<div className="font-semibold mb-2">Revenue:</div>
|
|
||||||
<div className="pl-4 space-y-1">
|
|
||||||
<div>Total: {analyticsData?.revenue?.total || "N/A"}</div>
|
|
||||||
<div>Today: {analyticsData?.revenue?.today || "N/A"}</div>
|
|
||||||
<div>
|
|
||||||
Daily Revenue Array Length:{" "}
|
|
||||||
{analyticsData?.revenue?.dailyRevenue?.length || 0}
|
|
||||||
</div>
|
|
||||||
<div>First 3 Daily Revenue:</div>
|
|
||||||
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
|
||||||
{JSON.stringify(
|
|
||||||
analyticsData?.revenue?.dailyRevenue?.slice(0, 3),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold mb-2">Vendors:</div>
|
|
||||||
<div className="pl-4 space-y-1">
|
|
||||||
<div>Total: {analyticsData?.vendors?.total || "N/A"}</div>
|
|
||||||
<div>
|
|
||||||
Daily Growth Array Length:{" "}
|
|
||||||
{analyticsData?.vendors?.dailyGrowth?.length || 0}
|
|
||||||
</div>
|
|
||||||
<div>First 3 Daily Growth:</div>
|
|
||||||
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
|
||||||
{JSON.stringify(
|
|
||||||
analyticsData?.vendors?.dailyGrowth?.slice(0, 3),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details className="mt-4">
|
<details className="mt-4">
|
||||||
<summary className="font-semibold cursor-pointer">
|
<summary className="font-semibold cursor-pointer hover:text-primary transition-colors">
|
||||||
Full JSON Response
|
Full JSON Response
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
|
<pre className="mt-2 bg-muted/50 p-4 rounded overflow-auto max-h-96 text-[10px] backdrop-blur-sm">
|
||||||
{JSON.stringify(analyticsData, null, 2)}
|
{JSON.stringify(analyticsData, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -690,14 +658,16 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Best Month Card (show for YTD, full year, or previous years) */}
|
||||||
{/* Best Month Card (show for YTD, full year, or previous years) */}
|
{/* Best Month Card (show for YTD, full year, or previous years) */}
|
||||||
{bestMonth && (
|
{bestMonth && (
|
||||||
<Card className="border-green-500/50 bg-green-500/5">
|
<Card className="border-green-500/20 bg-green-500/5 backdrop-blur-sm overflow-hidden relative">
|
||||||
<CardContent className="pt-6">
|
<div className="absolute inset-0 bg-gradient-to-r from-green-500/10 to-transparent opacity-50" />
|
||||||
|
<CardContent className="pt-6 relative">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<div className="p-2 rounded-full bg-green-500/20">
|
<div className="p-3 rounded-full bg-green-500/20 border border-green-500/20 shadow-[0_0_15px_rgba(34,197,94,0.2)]">
|
||||||
<Trophy className="h-5 w-5 text-green-600" />
|
<Trophy className="h-6 w-6 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
@@ -708,7 +678,7 @@ export default function AdminAnalytics() {
|
|||||||
? "(Full Year)"
|
? "(Full Year)"
|
||||||
: "(YTD)"}
|
: "(YTD)"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold text-green-600">
|
<div className="text-2xl font-bold text-green-600 mt-1">
|
||||||
{bestMonth.month}
|
{bestMonth.month}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -717,10 +687,10 @@ export default function AdminAnalytics() {
|
|||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
Revenue
|
Revenue
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold">
|
<div className="text-2xl font-bold bg-gradient-to-r from-green-600 to-green-400 bg-clip-text text-transparent">
|
||||||
{formatCurrency(bestMonth.revenue)}
|
{formatCurrency(bestMonth.revenue)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
{bestMonth.orders.toLocaleString()} orders
|
{bestMonth.orders.toLocaleString()} orders
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -729,15 +699,17 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</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-4 gap-4">
|
||||||
{/* Orders Card */}
|
{/* Orders Card */}
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
Total Orders
|
Total Orders
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
<div className="p-2 bg-blue-500/10 rounded-md">
|
||||||
|
<ShoppingCart className="h-4 w-4 text-blue-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -745,20 +717,22 @@ export default function AdminAnalytics() {
|
|||||||
{analyticsData?.orders?.total?.toLocaleString() || "0"}
|
{analyticsData?.orders?.total?.toLocaleString() || "0"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
<span>Today: {analyticsData?.orders?.totalToday || 0}</span>
|
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Today: {analyticsData?.orders?.totalToday || 0}</span>
|
||||||
|
<div className="ml-auto">
|
||||||
<TrendIndicator
|
<TrendIndicator
|
||||||
current={analyticsData?.orders?.totalToday || 0}
|
current={analyticsData?.orders?.totalToday || 0}
|
||||||
previous={(analyticsData?.orders?.total || 0) / 30}
|
previous={(analyticsData?.orders?.total || 0) / 30}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading || refreshing ? (
|
{loading || refreshing ? (
|
||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-4 h-14 flex items-center justify-center">
|
||||||
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
) : analyticsData?.orders?.dailyOrders &&
|
) : analyticsData?.orders?.dailyOrders &&
|
||||||
analyticsData.orders.dailyOrders.length > 0 ? (
|
analyticsData.orders.dailyOrders.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-4 h-14 -mx-2">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart
|
<RechartsBarChart
|
||||||
data={transformChartData(
|
data={transformChartData(
|
||||||
@@ -767,27 +741,29 @@ export default function AdminAnalytics() {
|
|||||||
)}
|
)}
|
||||||
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
>
|
>
|
||||||
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#3b82f6" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">
|
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
|
||||||
No chart data available
|
No chart data
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Revenue Card */}
|
{/* Revenue Card */}
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
Total Revenue
|
Total Revenue
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
<div className="p-2 bg-green-500/10 rounded-md">
|
||||||
|
<DollarSign className="h-4 w-4 text-green-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -795,22 +771,22 @@ export default function AdminAnalytics() {
|
|||||||
{formatCurrency(analyticsData?.revenue?.total || 0)}
|
{formatCurrency(analyticsData?.revenue?.total || 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
<span>
|
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Today: {formatCurrency(analyticsData?.revenue?.today || 0)}</span>
|
||||||
Today: {formatCurrency(analyticsData?.revenue?.today || 0)}
|
<div className="ml-auto">
|
||||||
</span>
|
|
||||||
<TrendIndicator
|
<TrendIndicator
|
||||||
current={analyticsData?.revenue?.today || 0}
|
current={analyticsData?.revenue?.today || 0}
|
||||||
previous={(analyticsData?.revenue?.total || 0) / 30} // Rough estimate
|
previous={(analyticsData?.revenue?.total || 0) / 30}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading || refreshing ? (
|
{loading || refreshing ? (
|
||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-4 h-14 flex items-center justify-center">
|
||||||
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
) : analyticsData?.revenue?.dailyRevenue &&
|
) : analyticsData?.revenue?.dailyRevenue &&
|
||||||
analyticsData.revenue.dailyRevenue.length > 0 ? (
|
analyticsData.revenue.dailyRevenue.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-4 h-14 -mx-2">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart
|
<RechartsBarChart
|
||||||
data={transformChartData(
|
data={transformChartData(
|
||||||
@@ -819,52 +795,54 @@ export default function AdminAnalytics() {
|
|||||||
)}
|
)}
|
||||||
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
>
|
>
|
||||||
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#10b981" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">
|
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
|
||||||
No chart data available
|
No chart data
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Vendors Card */}
|
{/* Vendors Card */}
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-sm font-medium">Vendors</CardTitle>
|
<CardTitle className="text-sm font-medium text-muted-foreground">Vendors</CardTitle>
|
||||||
<Users className="h-4 w-4 text-muted-foreground" />
|
<div className="p-2 bg-purple-500/10 rounded-md">
|
||||||
|
<Users className="h-4 w-4 text-purple-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">
|
||||||
{analyticsData?.vendors?.total?.toLocaleString() || "0"}
|
{analyticsData?.vendors?.total?.toLocaleString() || "0"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1 gap-2">
|
||||||
<span>Active: {analyticsData?.vendors?.active || 0}</span>
|
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Active: {analyticsData?.vendors?.active || 0}</span>
|
||||||
<span className="ml-2">
|
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Stores: {analyticsData?.vendors?.activeStores || 0}</span>
|
||||||
Stores: {analyticsData?.vendors?.activeStores || 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-2">
|
||||||
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
<span>New: {analyticsData?.vendors?.newToday || 0}</span>
|
||||||
|
<div className="ml-auto">
|
||||||
<TrendIndicator
|
<TrendIndicator
|
||||||
current={analyticsData?.vendors?.newToday || 0}
|
current={analyticsData?.vendors?.newToday || 0}
|
||||||
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7} // Average per day
|
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading || refreshing ? (
|
{loading || refreshing ? (
|
||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-2 h-12 flex items-center justify-center">
|
||||||
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
) : analyticsData?.vendors?.dailyGrowth &&
|
) : analyticsData?.vendors?.dailyGrowth &&
|
||||||
analyticsData.vendors.dailyGrowth.length > 0 ? (
|
analyticsData.vendors.dailyGrowth.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-2 h-12 -mx-2">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart
|
<RechartsBarChart
|
||||||
data={transformChartData(
|
data={transformChartData(
|
||||||
@@ -873,25 +851,27 @@ export default function AdminAnalytics() {
|
|||||||
)}
|
)}
|
||||||
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
>
|
>
|
||||||
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#8b5cf6" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">
|
<div className="mt-2 h-12 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
|
||||||
No chart data available
|
No chart data
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Products Card */}
|
{/* Products Card */}
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-sm font-medium">Products</CardTitle>
|
<CardTitle className="text-sm font-medium text-muted-foreground">Products</CardTitle>
|
||||||
<Package className="h-4 w-4 text-muted-foreground" />
|
<div className="p-2 bg-amber-500/10 rounded-md">
|
||||||
|
<Package className="h-4 w-4 text-amber-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -899,26 +879,44 @@ export default function AdminAnalytics() {
|
|||||||
{analyticsData?.products?.total?.toLocaleString() || "0"}
|
{analyticsData?.products?.total?.toLocaleString() || "0"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
<div className="flex items-center text-xs text-muted-foreground mt-1">
|
||||||
<span>New This Week: {analyticsData?.products?.recent || 0}</span>
|
<span className="bg-muted/50 px-1.5 py-0.5 rounded">New This Week: {analyticsData?.products?.recent || 0}</span>
|
||||||
|
</div>
|
||||||
|
{/* Visual spacer since no chart here */}
|
||||||
|
<div className="mt-4 h-14 w-full bg-gradient-to-r from-amber-500/5 to-transparent rounded-md flex items-center justify-center">
|
||||||
|
<span className="text-xs text-muted-foreground/50 italic">Inventory Overview</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="orders" className="mt-6">
|
<Tabs defaultValue="orders" className="mt-8">
|
||||||
<TabsList>
|
<TabsList className="bg-background/40 backdrop-blur-md border border-border/40 p-1 w-full sm:w-auto h-auto grid grid-cols-3 sm:flex">
|
||||||
<TabsTrigger value="orders">Orders</TabsTrigger>
|
<TabsTrigger
|
||||||
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
value="orders"
|
||||||
<TabsTrigger value="growth">Growth Since Launch</TabsTrigger>
|
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
|
||||||
|
>
|
||||||
|
Orders
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="vendors"
|
||||||
|
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
|
||||||
|
>
|
||||||
|
Vendors
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="growth"
|
||||||
|
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
|
||||||
|
>
|
||||||
|
Growth Since Launch
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="orders" className="mt-4">
|
<TabsContent value="orders" className="mt-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Order Trends</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent w-fit">Order Trends</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Daily order volume and revenue processed over the selected time
|
Daily order volume and revenue processed over the selected time period
|
||||||
period
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -1041,10 +1039,10 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="vendors" className="mt-4">
|
<TabsContent value="vendors" className="mt-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Vendor Growth</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-purple-600 to-pink-500 bg-clip-text text-transparent w-fit">Vendor Growth</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
New vendor registrations over time
|
New vendor registrations over time
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -1200,74 +1198,51 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
{/* Cumulative Stats Cards */}
|
{/* Cumulative Stats Cards */}
|
||||||
{growthData?.cumulative && (
|
{growthData?.cumulative && (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-6 gap-4">
|
||||||
<Card>
|
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Total Orders</div>
|
||||||
Total Orders
|
<div className="text-2xl font-bold">{growthData.cumulative.orders.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{growthData.cumulative.orders.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="col-span-1 border-green-500/20 bg-green-500/5 backdrop-blur-sm hover:bg-green-500/10 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Total Revenue</div>
|
||||||
Total Revenue
|
<div className="text-2xl font-bold text-green-600">{formatCurrency(growthData.cumulative.revenue)}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-green-600">
|
|
||||||
{formatCurrency(growthData.cumulative.revenue)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Customers</div>
|
||||||
Customers
|
<div className="text-2xl font-bold">{growthData.cumulative.customers.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{growthData.cumulative.customers.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Vendors</div>
|
||||||
Vendors
|
<div className="text-2xl font-bold">{growthData.cumulative.vendors.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{growthData.cumulative.vendors.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Products</div>
|
||||||
Products
|
<div className="text-2xl font-bold">{growthData.cumulative.products.toLocaleString()}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{growthData.cumulative.products.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="col-span-1 border-purple-500/20 bg-purple-500/5 backdrop-blur-sm hover:bg-purple-500/10 transition-colors">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Avg Order Value</div>
|
||||||
Avg Order Value
|
<div className="text-2xl font-bold text-purple-600">{formatCurrency(growthData.cumulative.avgOrderValue)}</div>
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{formatCurrency(growthData.cumulative.avgOrderValue)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Monthly Revenue & Orders Chart */}
|
{/* Monthly Revenue & Orders Chart */}
|
||||||
<Card>
|
<Card className="lg:col-span-2 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Monthly Revenue & Orders</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-green-600 to-emerald-500 bg-clip-text text-transparent w-fit">Monthly Revenue & Orders</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Platform performance by month since launch
|
Platform performance by month since launch
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@@ -1292,36 +1267,52 @@ export default function AdminAnalytics() {
|
|||||||
}))}
|
}))}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" opacity={0.4} />
|
||||||
<XAxis dataKey="formattedMonth" tick={{ fontSize: 12 }} />
|
<XAxis dataKey="formattedMonth" tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} axisLine={false} tickLine={false} />
|
||||||
<YAxis yAxisId="left" tick={{ fontSize: 12 }} />
|
<YAxis yAxisId="left" tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} axisLine={false} tickLine={false} />
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
orientation="right"
|
orientation="right"
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
|
||||||
|
axisLine={false} tickLine={false}
|
||||||
tickFormatter={(value) =>
|
tickFormatter={(value) =>
|
||||||
`£${(value / 1000).toFixed(0)}k`
|
`£${(value / 1000).toFixed(0)}k`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
cursor={{ fill: 'hsl(var(--muted)/0.4)' }}
|
||||||
content={({ active, payload }) => {
|
content={({ active, payload }) => {
|
||||||
if (active && payload?.length) {
|
if (active && payload?.length) {
|
||||||
const data = payload[0].payload;
|
const data = payload[0].payload;
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
<div className="bg-background/95 border border-border/50 p-4 rounded-xl shadow-xl backdrop-blur-md">
|
||||||
<p className="font-medium mb-2">{data.month}</p>
|
<p className="font-semibold mb-3 border-b border-border/50 pb-2">{data.month}</p>
|
||||||
<p className="text-sm text-blue-600">
|
<div className="space-y-1.5">
|
||||||
Orders: {data.orders.toLocaleString()}
|
<div className="flex items-center justify-between gap-4">
|
||||||
</p>
|
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
<p className="text-sm text-green-600">
|
<div className="w-2 h-2 rounded-full bg-blue-500" /> Orders
|
||||||
Revenue: {formatCurrency(data.revenue)}
|
</span>
|
||||||
</p>
|
<span className="font-medium">{data.orders.toLocaleString()}</span>
|
||||||
<p className="text-sm text-purple-600">
|
</div>
|
||||||
Customers: {data.customers.toLocaleString()}
|
<div className="flex items-center justify-between gap-4">
|
||||||
</p>
|
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
<p className="text-sm text-amber-600">
|
<div className="w-2 h-2 rounded-full bg-green-500" /> Revenue
|
||||||
New Vendors: {data.newVendors}
|
</span>
|
||||||
</p>
|
<span className="font-medium text-green-600">{formatCurrency(data.revenue)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-purple-500" /> Customers
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{data.customers.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-amber-500" /> New Vendors
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{data.newVendors}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1333,7 +1324,9 @@ export default function AdminAnalytics() {
|
|||||||
dataKey="orders"
|
dataKey="orders"
|
||||||
fill="#3b82f6"
|
fill="#3b82f6"
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
|
maxBarSize={50}
|
||||||
name="Orders"
|
name="Orders"
|
||||||
|
fillOpacity={0.8}
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
@@ -1341,14 +1334,15 @@ export default function AdminAnalytics() {
|
|||||||
dataKey="revenue"
|
dataKey="revenue"
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
strokeWidth={3}
|
strokeWidth={3}
|
||||||
dot={{ fill: "#10b981", r: 4 }}
|
dot={{ fill: "#10b981", r: 4, strokeWidth: 2, stroke: "hsl(var(--background))" }}
|
||||||
|
activeDot={{ r: 6, strokeWidth: 0 }}
|
||||||
name="Revenue"
|
name="Revenue"
|
||||||
/>
|
/>
|
||||||
</ComposedChart>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-80 text-muted-foreground">
|
<div className="flex items-center justify-center h-80 text-muted-foreground bg-muted/20 rounded-lg border border-dashed border-border/50">
|
||||||
No growth data available
|
No growth data available
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1356,17 +1350,18 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Customer Segments Pie Chart */}
|
{/* Customer Segments Pie Chart */}
|
||||||
<Card>
|
<Card className="lg:col-span-1 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm h-full flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Customer Segments</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-amber-600 to-orange-500 bg-clip-text text-transparent w-fit">Customer Segments</CardTitle>
|
||||||
<CardDescription>Breakdown by purchase behavior</CardDescription>
|
<CardDescription>By purchase behavior</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex-1 flex flex-col justify-center">
|
||||||
{growthLoading ? (
|
{growthLoading ? (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
) : growthData?.customers ? (
|
) : growthData?.customers ? (
|
||||||
|
<>
|
||||||
<div className="h-64 min-w-0">
|
<div className="h-64 min-w-0">
|
||||||
<ResponsiveContainer key={growthData?.customers ? 'ready' : 'loading'} width="100%" height="100%">
|
<ResponsiveContainer key={growthData?.customers ? 'ready' : 'loading'} width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
@@ -1395,14 +1390,13 @@ export default function AdminAnalytics() {
|
|||||||
]}
|
]}
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={50}
|
innerRadius={60}
|
||||||
outerRadius={80}
|
outerRadius={90}
|
||||||
paddingAngle={2}
|
paddingAngle={3}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
label={({ name, percent }) =>
|
label={({ percent }) => `${(percent * 100).toFixed(0)}%`}
|
||||||
`${name}: ${(percent * 100).toFixed(0)}%`
|
|
||||||
}
|
|
||||||
labelLine={false}
|
labelLine={false}
|
||||||
|
stroke="none"
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{ color: SEGMENT_COLORS.new },
|
{ color: SEGMENT_COLORS.new },
|
||||||
@@ -1422,18 +1416,18 @@ export default function AdminAnalytics() {
|
|||||||
data.name.split(" ")[0].toLowerCase()
|
data.name.split(" ")[0].toLowerCase()
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
<div className="bg-background/95 border border-border/50 p-3 rounded-lg shadow-xl backdrop-blur-md">
|
||||||
<p className="font-medium">{data.name}</p>
|
<p className="font-semibold mb-1">{data.name}</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Count: {data.value.toLocaleString()}
|
Count: <span className="font-mono">{data.value.toLocaleString()}</span>
|
||||||
</p>
|
</p>
|
||||||
{details && (
|
{details && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-green-600">
|
<p className="text-sm text-green-600 font-medium">
|
||||||
Revenue:{" "}
|
Revenue:{" "}
|
||||||
{formatCurrency(details.totalRevenue)}
|
{formatCurrency(details.totalRevenue)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Avg Orders: {details.avgOrderCount}
|
Avg Orders: {details.avgOrderCount}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -1447,101 +1441,104 @@ export default function AdminAnalytics() {
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-64 text-muted-foreground">
|
|
||||||
No customer data available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Segment Stats */}
|
{/* Segment Stats */}
|
||||||
{growthData?.customers && (
|
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||||
<div className="grid grid-cols-2 gap-2 mt-4">
|
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/10 text-center hover:bg-blue-500/15 transition-colors">
|
||||||
<div className="p-2 rounded bg-blue-500/10 text-center">
|
<div className="text-xl font-bold text-blue-600">
|
||||||
<div className="text-lg font-bold text-blue-600">
|
|
||||||
{growthData.customers.segments.new}
|
{growthData.customers.segments.new}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">New</div>
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">New</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded bg-green-500/10 text-center">
|
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/10 text-center hover:bg-green-500/15 transition-colors">
|
||||||
<div className="text-lg font-bold text-green-600">
|
<div className="text-xl font-bold text-green-600">
|
||||||
{growthData.customers.segments.returning}
|
{growthData.customers.segments.returning}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
Returning
|
Returning
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded bg-amber-500/10 text-center">
|
<div className="p-3 rounded-lg bg-amber-500/10 border border-amber-500/10 text-center hover:bg-amber-500/15 transition-colors">
|
||||||
<div className="text-lg font-bold text-amber-600">
|
<div className="text-xl font-bold text-amber-600">
|
||||||
{growthData.customers.segments.loyal}
|
{growthData.customers.segments.loyal}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">Loyal</div>
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Loyal</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded bg-purple-500/10 text-center">
|
<div className="p-3 rounded-lg bg-purple-500/10 border border-purple-500/10 text-center hover:bg-purple-500/15 transition-colors">
|
||||||
<div className="text-lg font-bold text-purple-600">
|
<div className="text-xl font-bold text-purple-600">
|
||||||
{growthData.customers.segments.vip}
|
{growthData.customers.segments.vip}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">VIP</div>
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">VIP</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-64 text-muted-foreground bg-muted/20 rounded-lg border border-dashed border-border/50">
|
||||||
|
No customer data available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Monthly Growth Table */}
|
{/* Monthly Growth Table */}
|
||||||
{growthData?.monthly && growthData.monthly.length > 0 && (
|
{growthData?.monthly && growthData.monthly.length > 0 && (
|
||||||
<Card>
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Monthly Breakdown</CardTitle>
|
<CardTitle className="bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent w-fit">Monthly Breakdown</CardTitle>
|
||||||
<CardDescription>Detailed metrics by month</CardDescription>
|
<CardDescription>Detailed metrics by month</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="overflow-x-auto">
|
<div className="rounded-md border border-border/40 overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b">
|
<tr className="bg-muted/40 border-b border-border/40">
|
||||||
<th className="text-left p-2 font-medium">Month</th>
|
<th className="text-left p-3 font-medium text-muted-foreground">Month</th>
|
||||||
<th className="text-right p-2 font-medium">Orders</th>
|
<th className="text-right p-3 font-medium text-muted-foreground">Orders</th>
|
||||||
<th className="text-right p-2 font-medium">Revenue</th>
|
<th className="text-right p-3 font-medium text-muted-foreground">Revenue</th>
|
||||||
<th className="text-right p-2 font-medium">
|
<th className="text-right p-3 font-medium text-muted-foreground">
|
||||||
Customers
|
Customers
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right p-2 font-medium">
|
<th className="text-right p-3 font-medium text-muted-foreground">
|
||||||
Avg Order
|
Avg Order
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right p-2 font-medium">
|
<th className="text-right p-3 font-medium text-muted-foreground">
|
||||||
New Vendors
|
New Vendors
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right p-2 font-medium">
|
<th className="text-right p-3 font-medium text-muted-foreground">
|
||||||
New Customers
|
New Customers
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-border/30">
|
||||||
{growthData.monthly.map((month) => (
|
{growthData.monthly.map((month, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={month.month}
|
key={month.month}
|
||||||
className="border-b hover:bg-muted/50"
|
className="hover:bg-muted/30 transition-colors animate-in fade-in slide-in-from-bottom-2 duration-500 fill-mode-backwards"
|
||||||
|
style={{ animationDelay: `${i * 50}ms` }}
|
||||||
>
|
>
|
||||||
<td className="p-2 font-medium">
|
<td className="p-3 font-medium">
|
||||||
{new Date(month.month + "-01").toLocaleDateString(
|
{new Date(month.month + "-01").toLocaleDateString(
|
||||||
"en-GB",
|
"en-GB",
|
||||||
{ month: "long", year: "numeric" },
|
{ month: "long", year: "numeric" },
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right p-2">
|
<td className="text-right p-3">
|
||||||
|
<div className="inline-flex items-center px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 text-xs font-medium">
|
||||||
{month.orders.toLocaleString()}
|
{month.orders.toLocaleString()}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right p-2 text-green-600">
|
<td className="text-right p-3 text-green-600 font-semibold">
|
||||||
{formatCurrency(month.revenue)}
|
{formatCurrency(month.revenue)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right p-2">
|
<td className="text-right p-3 text-muted-foreground">
|
||||||
{month.customers.toLocaleString()}
|
{month.customers.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right p-2">
|
<td className="text-right p-3 text-muted-foreground">
|
||||||
{formatCurrency(month.avgOrderValue)}
|
{formatCurrency(month.avgOrderValue)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right p-2">{month.newVendors}</td>
|
<td className="text-right p-3 text-muted-foreground">{month.newVendors}</td>
|
||||||
<td className="text-right p-2">
|
<td className="text-right p-3 text-muted-foreground">
|
||||||
{month.newCustomers}
|
{month.newCustomers}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user