Revamp admin dashboard analytics and UI
Refactored the admin dashboard to use tabbed navigation for analytics and management. Enhanced AdminAnalytics with Recharts visualizations, added top vendors by revenue, and improved chart tooltips. Removed unused columns from vendor table. Updated layout and notification context to exclude admin pages from dashboard-specific UI and notifications. Minor debug logging added to SystemStatusCard.
This commit is contained in:
@@ -1,38 +1,47 @@
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import AdminAnalytics from "@/components/admin/AdminAnalytics";
|
||||||
import InviteVendorCard from "@/components/admin/InviteVendorCard";
|
import InviteVendorCard from "@/components/admin/InviteVendorCard";
|
||||||
import BanUserCard from "@/components/admin/BanUserCard";
|
import BanUserCard from "@/components/admin/BanUserCard";
|
||||||
import RecentOrdersCard from "@/components/admin/RecentOrdersCard";
|
|
||||||
import SystemStatusCard from "@/components/admin/SystemStatusCard";
|
|
||||||
import InvitationsListCard from "@/components/admin/InvitationsListCard";
|
import InvitationsListCard from "@/components/admin/InvitationsListCard";
|
||||||
import VendorsCard from "@/components/admin/VendorsCard";
|
import VendorsCard from "@/components/admin/VendorsCard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Admin</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Admin Dashboard</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Restricted area. Only admin1 can access.</p>
|
<p className="text-sm text-muted-foreground mt-1">Platform analytics and vendor management</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm">
|
<Button asChild variant="outline" size="sm">
|
||||||
<Link href="/dashboard">Back to Dashboard</Link>
|
<Link href="/dashboard">Back to Dashboard</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="analytics" className="space-y-6">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
|
<TabsTrigger value="management">Management</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="analytics" className="space-y-6">
|
||||||
|
<AdminAnalytics />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="management" className="space-y-6">
|
||||||
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch">
|
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3 items-stretch">
|
||||||
<SystemStatusCard />
|
|
||||||
<VendorsCard />
|
<VendorsCard />
|
||||||
<InviteVendorCard />
|
<InviteVendorCard />
|
||||||
<BanUserCard />
|
<BanUserCard />
|
||||||
<RecentOrdersCard />
|
|
||||||
<InvitationsListCard />
|
<InvitationsListCard />
|
||||||
|
|
||||||
{/* Disabled/hidden cards as requested */}
|
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
7
app/dashboard/admin/vendors/page.tsx
vendored
7
app/dashboard/admin/vendors/page.tsx
vendored
@@ -130,8 +130,6 @@ export default async function AdminVendorsPage() {
|
|||||||
<TableHead>Store</TableHead>
|
<TableHead>Store</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Join Date</TableHead>
|
<TableHead>Join Date</TableHead>
|
||||||
<TableHead>Orders</TableHead>
|
|
||||||
<TableHead>Revenue</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -139,10 +137,7 @@ export default async function AdminVendorsPage() {
|
|||||||
{vendors.map((vendor) => (
|
{vendors.map((vendor) => (
|
||||||
<TableRow key={vendor._id}>
|
<TableRow key={vendor._id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div>
|
|
||||||
<div className="font-medium">{vendor.username}</div>
|
<div className="font-medium">{vendor.username}</div>
|
||||||
<div className="text-sm text-muted-foreground">{vendor.email || 'No email'}</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{vendor.storeId || 'No store'}</TableCell>
|
<TableCell>{vendor.storeId || 'No store'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -162,8 +157,6 @@ export default async function AdminVendorsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
|
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>N/A</TableCell>
|
|
||||||
<TableCell>N/A</TableCell>
|
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ 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 } 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 { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts';
|
||||||
|
import { formatGBP } from "@/utils/format";
|
||||||
|
|
||||||
// API response data structure
|
// API response data structure
|
||||||
interface AnalyticsData {
|
interface AnalyticsData {
|
||||||
@@ -18,7 +21,14 @@ interface AnalyticsData {
|
|||||||
activeToday?: number;
|
activeToday?: number;
|
||||||
active?: number;
|
active?: number;
|
||||||
stores?: number;
|
stores?: number;
|
||||||
|
activeStores?: number;
|
||||||
dailyGrowth?: { date: string; count: number }[];
|
dailyGrowth?: { date: string; count: number }[];
|
||||||
|
topVendors?: Array<{
|
||||||
|
vendorId: string;
|
||||||
|
vendorName: string;
|
||||||
|
totalRevenue: number;
|
||||||
|
orderCount: number;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
orders?: {
|
orders?: {
|
||||||
total?: number;
|
total?: number;
|
||||||
@@ -35,8 +45,9 @@ interface AnalyticsData {
|
|||||||
dailyRevenue?: { date: string; amount: number }[];
|
dailyRevenue?: { date: string; amount: number }[];
|
||||||
};
|
};
|
||||||
engagement?: {
|
engagement?: {
|
||||||
totalMessages?: number;
|
totalChats?: number;
|
||||||
activeChats?: number;
|
activeChats?: number;
|
||||||
|
totalMessages?: number;
|
||||||
dailyMessages?: { date: string; count: number }[];
|
dailyMessages?: { date: string; count: number }[];
|
||||||
};
|
};
|
||||||
products?: {
|
products?: {
|
||||||
@@ -59,29 +70,36 @@ export default function AdminAnalytics() {
|
|||||||
const [dateRange, setDateRange] = useState("7days");
|
const [dateRange, setDateRange] = useState("7days");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
const fetchAnalyticsData = async () => {
|
const fetchAnalyticsData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
const token = document.cookie
|
const data = await fetchClient<AnalyticsData>(`/admin/analytics?range=${dateRange}`);
|
||||||
.split("; ")
|
console.log('=== ADMIN ANALYTICS DATA ===');
|
||||||
.find((row) => row.startsWith("Authorization="))
|
console.log('Date Range:', dateRange);
|
||||||
?.split("=")[1];
|
console.log('Full Response:', JSON.stringify(data, null, 2));
|
||||||
|
console.log('Orders:', {
|
||||||
const response = await fetch(`/api/admin/analytics?range=${dateRange}`, {
|
total: data?.orders?.total,
|
||||||
method: "GET",
|
dailyOrders: data?.orders?.dailyOrders,
|
||||||
headers: {
|
dailyOrdersLength: data?.orders?.dailyOrders?.length,
|
||||||
Authorization: `Bearer ${token}`,
|
sample: data?.orders?.dailyOrders?.slice(0, 3)
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
console.log('Revenue:', {
|
||||||
if (!response.ok) {
|
total: data?.revenue?.total,
|
||||||
throw new Error("Failed to fetch analytics data");
|
dailyRevenue: data?.revenue?.dailyRevenue,
|
||||||
}
|
dailyRevenueLength: data?.revenue?.dailyRevenue?.length,
|
||||||
|
sample: data?.revenue?.dailyRevenue?.slice(0, 3)
|
||||||
const data = await response.json();
|
});
|
||||||
|
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);
|
||||||
@@ -109,64 +127,89 @@ export default function AdminAnalytics() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chart component for line/area charts
|
// Helper to transform data for recharts
|
||||||
const Chart = ({ data, valueKey = "count", color = "#3b82f6", height = 200 }:
|
const transformChartData = (data: Array<{ date: string; [key: string]: any }>, valueKey: string = "count") => {
|
||||||
{ data: any[]; valueKey?: string; color?: string; height?: number }) => {
|
if (!data || data.length === 0) return [];
|
||||||
if (!data || data.length === 0) {
|
|
||||||
|
return data.map(item => {
|
||||||
|
const dateStr = item.date;
|
||||||
|
// Parse YYYY-MM-DD format
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
const date = parts.length === 3
|
||||||
|
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
|
||||||
|
: new Date(dateStr);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: dateStr,
|
||||||
|
formattedDate: date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }),
|
||||||
|
value: Number(item[valueKey]) || 0,
|
||||||
|
[valueKey]: Number(item[valueKey]) || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to combine orders and revenue data for dual-axis chart
|
||||||
|
const combineOrdersAndRevenue = (orders: Array<{ date: string; count: number }>, revenue: Array<{ date: string; amount: number }>) => {
|
||||||
|
if (!orders || orders.length === 0) return [];
|
||||||
|
|
||||||
|
// Create a map of revenue by date for quick lookup
|
||||||
|
const revenueMap = new Map<string, number>();
|
||||||
|
if (revenue && revenue.length > 0) {
|
||||||
|
revenue.forEach(r => {
|
||||||
|
revenueMap.set(r.date, r.amount || 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders.map(order => {
|
||||||
|
const dateStr = order.date;
|
||||||
|
const parts = dateStr.split('-');
|
||||||
|
const date = parts.length === 3
|
||||||
|
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
|
||||||
|
: new Date(dateStr);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: dateStr,
|
||||||
|
formattedDate: date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' }),
|
||||||
|
orders: order.count || 0,
|
||||||
|
revenue: revenueMap.get(dateStr) || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom tooltip for charts
|
||||||
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||||
|
if (active && payload && payload.length) {
|
||||||
|
const data = payload[0].payload;
|
||||||
|
const dataKey = payload[0].dataKey;
|
||||||
|
const isDualAxis = data.orders !== undefined && data.revenue !== undefined;
|
||||||
|
|
||||||
|
// Determine if this is a currency amount or a count
|
||||||
|
// transformChartData creates both 'value' and the original key (count/amount)
|
||||||
|
// So we check the original key to determine the type
|
||||||
|
const isAmount = dataKey === 'amount' || dataKey === 'revenue' ||
|
||||||
|
(dataKey === 'value' && data.amount !== undefined && data.count === undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground" style={{ height: `${height}px` }}>
|
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
||||||
No data available
|
<p className="text-sm font-medium mb-2">{data.formattedDate || label}</p>
|
||||||
|
{isDualAxis ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-blue-600">
|
||||||
|
Orders: <span className="font-semibold">{data.orders}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-green-600">
|
||||||
|
Revenue: <span className="font-semibold">{formatGBP(data.revenue)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-primary">
|
||||||
|
{isAmount ? formatGBP(data.value || data.amount || 0) : `${data.value || data.count || 0}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
// 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
|
// Trend indicator component for metric cards
|
||||||
@@ -189,9 +232,9 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
// Format currency
|
// Format currency
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-GB', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD',
|
currency: 'GBP',
|
||||||
maximumFractionDigits: 0
|
maximumFractionDigits: 0
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
@@ -237,9 +280,74 @@ export default function AdminAnalytics() {
|
|||||||
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
|
>
|
||||||
|
{showDebug ? 'Hide' : 'Show'} Debug
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showDebug && analyticsData && (
|
||||||
|
<Card className="mt-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Debug: Raw Data</CardTitle>
|
||||||
|
<CardDescription>Date Range: {dateRange}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4 text-xs font-mono">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold mb-2">Orders:</div>
|
||||||
|
<div className="pl-4 space-y-1">
|
||||||
|
<div>Total: {analyticsData?.orders?.total || 'N/A'}</div>
|
||||||
|
<div>Today: {analyticsData?.orders?.totalToday || 'N/A'}</div>
|
||||||
|
<div>Daily Orders Array Length: {analyticsData?.orders?.dailyOrders?.length || 0}</div>
|
||||||
|
<div>First 3 Daily Orders:</div>
|
||||||
|
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
||||||
|
{JSON.stringify(analyticsData?.orders?.dailyOrders?.slice(0, 3), null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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(analyticsData, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</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>
|
||||||
@@ -261,13 +369,17 @@ export default function AdminAnalytics() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{analyticsData?.orders?.dailyOrders && (
|
{analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
|
||||||
<div className="mt-3 h-10">
|
<div className="mt-3 h-12">
|
||||||
<Chart
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
data={analyticsData.orders.dailyOrders}
|
<RechartsBarChart data={transformChartData(analyticsData.orders.dailyOrders, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||||
height={40}
|
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
|
||||||
/>
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
</RechartsBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -292,15 +404,17 @@ export default function AdminAnalytics() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{analyticsData?.revenue?.dailyRevenue && (
|
{analyticsData?.revenue?.dailyRevenue && analyticsData.revenue.dailyRevenue.length > 0 ? (
|
||||||
<div className="mt-3 h-10">
|
<div className="mt-3 h-12">
|
||||||
<Chart
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
data={analyticsData.revenue.dailyRevenue}
|
<RechartsBarChart data={transformChartData(analyticsData.revenue.dailyRevenue, 'amount')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||||
valueKey="amount"
|
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
|
||||||
color="#10b981"
|
<Tooltip content={<CustomTooltip />} />
|
||||||
height={40}
|
</RechartsBarChart>
|
||||||
/>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -317,6 +431,10 @@ export default function AdminAnalytics() {
|
|||||||
<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">
|
||||||
|
<span>Active: {analyticsData?.vendors?.active || 0}</span>
|
||||||
|
<span className="ml-2">Stores: {analyticsData?.vendors?.activeStores || 0}</span>
|
||||||
|
</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 Today: {analyticsData?.vendors?.newToday || 0}</span>
|
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
||||||
<TrendIndicator
|
<TrendIndicator
|
||||||
@@ -325,14 +443,17 @@ export default function AdminAnalytics() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{analyticsData?.vendors?.dailyGrowth && (
|
{analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? (
|
||||||
<div className="mt-3 h-10">
|
<div className="mt-3 h-12">
|
||||||
<Chart
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
data={analyticsData.vendors.dailyGrowth}
|
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||||
color="#8b5cf6"
|
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
||||||
height={40}
|
<Tooltip content={<CustomTooltip />} />
|
||||||
/>
|
</RechartsBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -359,9 +480,7 @@ export default function AdminAnalytics() {
|
|||||||
<Tabs defaultValue="orders" className="mt-6">
|
<Tabs defaultValue="orders" className="mt-6">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="orders">Orders</TabsTrigger>
|
<TabsTrigger value="orders">Orders</TabsTrigger>
|
||||||
<TabsTrigger value="revenue">Revenue</TabsTrigger>
|
|
||||||
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
<TabsTrigger value="vendors">Vendors</TabsTrigger>
|
||||||
<TabsTrigger value="engagement">Engagement</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="orders" className="mt-4">
|
<TabsContent value="orders" className="mt-4">
|
||||||
@@ -369,18 +488,49 @@ export default function AdminAnalytics() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Order Trends</CardTitle>
|
<CardTitle>Order Trends</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Daily order volume over the selected time period
|
Daily order volume and revenue processed over the selected time period
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{analyticsData?.orders?.dailyOrders ? (
|
{analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
|
||||||
<Chart
|
<div className="h-80">
|
||||||
data={analyticsData.orders.dailyOrders}
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
height={300}
|
<ComposedChart
|
||||||
|
data={combineOrdersAndRevenue(
|
||||||
|
analyticsData.orders.dailyOrders,
|
||||||
|
analyticsData.revenue?.dailyRevenue || []
|
||||||
|
)}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="formattedDate"
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
|
angle={-45}
|
||||||
|
textAnchor="end"
|
||||||
|
height={60}
|
||||||
/>
|
/>
|
||||||
|
<YAxis
|
||||||
|
yAxisId="left"
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
|
label={{ value: 'Orders', angle: -90, position: 'insideLeft' }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
yAxisId="right"
|
||||||
|
orientation="right"
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
|
tickFormatter={(value) => `£${(value / 1000).toFixed(0)}k`}
|
||||||
|
label={{ value: 'Revenue', angle: 90, position: 'insideRight' }}
|
||||||
|
/>
|
||||||
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
<Bar yAxisId="left" dataKey="orders" fill="#3b82f6" radius={[2, 2, 0, 0]} name="Orders" />
|
||||||
|
<Line yAxisId="right" type="monotone" dataKey="revenue" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 4 }} name="Revenue" />
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
||||||
No order data available
|
No order data available for the selected time period
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -402,46 +552,6 @@ export default function AdminAnalytics() {
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="vendors" className="mt-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -451,71 +561,73 @@ export default function AdminAnalytics() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{analyticsData?.vendors?.dailyGrowth ? (
|
{analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? (
|
||||||
<Chart
|
<div className="h-80">
|
||||||
data={analyticsData.vendors.dailyGrowth}
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
color="#8b5cf6"
|
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
height={300}
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="formattedDate"
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
|
angle={-45}
|
||||||
|
textAnchor="end"
|
||||||
|
height={60}
|
||||||
/>
|
/>
|
||||||
|
<YAxis tick={{ fontSize: 12 }} />
|
||||||
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
||||||
|
</RechartsBarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
||||||
No vendor data available
|
No vendor data available for the selected time period
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-6">
|
||||||
<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 Vendors</div>
|
<div className="text-sm font-medium mb-1">Total Vendors</div>
|
||||||
<div className="text-2xl font-bold">{analyticsData?.vendors?.total?.toLocaleString() || '0'}</div>
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.total?.toLocaleString() || '0'}</div>
|
||||||
</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">New Today</div>
|
<div className="text-sm font-medium mb-1">Active Vendors</div>
|
||||||
<div className="text-2xl font-bold">{analyticsData?.vendors?.newToday?.toLocaleString() || '0'}</div>
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.active?.toLocaleString() || '0'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg">
|
||||||
|
<div className="text-sm font-medium mb-1">Active Stores</div>
|
||||||
|
<div className="text-2xl font-bold">{analyticsData?.vendors?.activeStores?.toLocaleString() || '0'}</div>
|
||||||
</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">New This Week</div>
|
<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 className="text-2xl font-bold">{analyticsData?.vendors?.newThisWeek?.toLocaleString() || '0'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="engagement" className="mt-4">
|
{/* Top Vendors by Revenue */}
|
||||||
<Card>
|
{analyticsData?.vendors?.topVendors && analyticsData.vendors.topVendors.length > 0 && (
|
||||||
<CardHeader>
|
<div className="mt-6">
|
||||||
<CardTitle>User Engagement</CardTitle>
|
<h3 className="text-lg font-semibold mb-4">Top Vendors by Revenue</h3>
|
||||||
<CardDescription>
|
<div className="space-y-2">
|
||||||
Chat and message activity
|
{analyticsData.vendors.topVendors.map((vendor, index) => (
|
||||||
</CardDescription>
|
<div key={vendor.vendorId} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
|
||||||
</CardHeader>
|
<div className="flex items-center gap-3">
|
||||||
<CardContent>
|
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
|
||||||
{analyticsData?.engagement?.dailyMessages ? (
|
{index + 1}
|
||||||
<Chart
|
</div>
|
||||||
data={analyticsData.engagement.dailyMessages}
|
<div>
|
||||||
color="#ec4899"
|
<div className="font-medium">{vendor.vendorName}</div>
|
||||||
height={300}
|
<div className="text-xs text-muted-foreground">{vendor.orderCount} orders</div>
|
||||||
/>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
<div className="text-right">
|
||||||
No engagement data available
|
<div className="font-semibold text-green-600">{formatCurrency(vendor.totalRevenue)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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");
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import KeepOnline from "@/components/KeepOnline";
|
|||||||
const KeepOnlineWrapper = () => {
|
const KeepOnlineWrapper = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
if (!pathname?.includes("/dashboard")) {
|
// Don't show KeepOnline on admin pages or non-dashboard pages
|
||||||
|
if (!pathname?.includes("/dashboard") || pathname?.includes("/dashboard/admin")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
|
|
||||||
// Check if we're in a chat detail page
|
// Check if we're in a chat detail page
|
||||||
const isChatDetailPage = pathname?.includes('/dashboard/chats/') && !pathname?.endsWith('/chats') && !pathname?.endsWith('/new')
|
const isChatDetailPage = pathname?.includes('/dashboard/chats/') && !pathname?.endsWith('/chats') && !pathname?.endsWith('/new')
|
||||||
|
// Check if we're on an admin page
|
||||||
|
const isAdminPage = pathname?.includes('/dashboard/admin')
|
||||||
|
|
||||||
useEffect(() => setMounted(true), [])
|
useEffect(() => setMounted(true), [])
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<div className={`flex h-screen ${theme === "dark" ? "dark" : ""}`}>
|
<div className={`flex h-screen ${theme === "dark" ? "dark" : ""}`}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="w-full flex flex-1 flex-col">
|
<div className="w-full flex flex-1 flex-col">
|
||||||
{!isChatDetailPage && (
|
{!isChatDetailPage && !isAdminPage && (
|
||||||
<header className="h-16 border-b border-border flex items-center justify-end px-6">
|
<header className="h-16 border-b border-border flex items-center justify-end px-6">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<UnifiedNotifications />
|
<UnifiedNotifications />
|
||||||
|
|||||||
@@ -167,8 +167,8 @@ export function NotificationProvider({ children }: NotificationProviderProps) {
|
|||||||
|
|
||||||
// Check for new paid orders
|
// Check for new paid orders
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run this on dashboard pages
|
// Only run this on dashboard pages, but not on admin pages
|
||||||
if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return;
|
if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard") || window.location.pathname.includes("/dashboard/admin")) return;
|
||||||
|
|
||||||
const checkForNewOrders = async () => {
|
const checkForNewOrders = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -250,8 +250,8 @@ export function NotificationProvider({ children }: NotificationProviderProps) {
|
|||||||
|
|
||||||
// Fetch unread chat counts
|
// Fetch unread chat counts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run this on dashboard pages
|
// Only run this on dashboard pages, but not on admin pages
|
||||||
if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard")) return;
|
if (typeof window === 'undefined' || !window.location.pathname.includes("/dashboard") || window.location.pathname.includes("/dashboard/admin")) return;
|
||||||
|
|
||||||
const fetchUnreadCounts = async () => {
|
const fetchUnreadCounts = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "28e292a",
|
"commitHash": "f212859",
|
||||||
"buildTime": "2025-11-28T18:29:50.128Z"
|
"buildTime": "2025-11-28T18:47:55.501Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user