Add year selection to admin analytics and split product table
AdminAnalytics now supports selecting historical years and updates available years dynamically from the API. The product table is refactored to display enabled and disabled products in separate tables, improving clarity. Minor formatting and code style improvements are also included.
This commit is contained in:
@@ -1,18 +1,58 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertCircle, BarChart, RefreshCw, Users, ShoppingCart,
|
import {
|
||||||
TrendingUp, TrendingDown, DollarSign, Package, Trophy } from "lucide-react";
|
AlertCircle,
|
||||||
|
BarChart,
|
||||||
|
RefreshCw,
|
||||||
|
Users,
|
||||||
|
ShoppingCart,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
DollarSign,
|
||||||
|
Package,
|
||||||
|
Trophy,
|
||||||
|
} from "lucide-react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { fetchClient } from "@/lib/api-client";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, ComposedChart } from 'recharts';
|
import {
|
||||||
|
BarChart as RechartsBarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
ComposedChart,
|
||||||
|
} from "recharts";
|
||||||
import { formatGBP } from "@/utils/format";
|
import { formatGBP } from "@/utils/format";
|
||||||
|
|
||||||
interface AnalyticsData {
|
interface AnalyticsData {
|
||||||
|
meta?: {
|
||||||
|
year: number;
|
||||||
|
currentYear: number;
|
||||||
|
availableYears: number[];
|
||||||
|
range: string;
|
||||||
|
isCurrentYear: boolean;
|
||||||
|
};
|
||||||
vendors?: {
|
vendors?: {
|
||||||
total?: number;
|
total?: number;
|
||||||
newToday?: number;
|
newToday?: number;
|
||||||
@@ -41,7 +81,12 @@ interface AnalyticsData {
|
|||||||
total?: number;
|
total?: number;
|
||||||
today?: number;
|
today?: number;
|
||||||
thisWeek?: number;
|
thisWeek?: number;
|
||||||
dailyRevenue?: { date: string; amount: number; orders?: number; avgOrderValue?: number }[];
|
dailyRevenue?: {
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
orders?: number;
|
||||||
|
avgOrderValue?: number;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
engagement?: {
|
engagement?: {
|
||||||
totalChats?: number;
|
totalChats?: number;
|
||||||
@@ -64,20 +109,45 @@ interface AnalyticsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminAnalytics() {
|
export default function AdminAnalytics() {
|
||||||
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(null);
|
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dateRange, setDateRange] = useState("7days");
|
const [dateRange, setDateRange] = useState("7days");
|
||||||
|
const [selectedYear, setSelectedYear] = useState<number>(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
);
|
||||||
|
const [availableYears, setAvailableYears] = useState<number[]>([
|
||||||
|
new Date().getFullYear(),
|
||||||
|
]);
|
||||||
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 [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const isViewingCurrentYear = selectedYear === currentYear;
|
||||||
|
|
||||||
const fetchAnalyticsData = async () => {
|
const fetchAnalyticsData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
const data = await fetchClient<AnalyticsData>(`/admin/analytics?range=${dateRange}`);
|
// Build query params - include year if not current year
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("range", dateRange);
|
||||||
|
if (selectedYear !== currentYear) {
|
||||||
|
params.set("year", selectedYear.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchClient<AnalyticsData>(
|
||||||
|
`/admin/analytics?${params.toString()}`,
|
||||||
|
);
|
||||||
setAnalyticsData(data);
|
setAnalyticsData(data);
|
||||||
|
|
||||||
|
// Update available years from response if provided
|
||||||
|
if (data.meta?.availableYears && data.meta.availableYears.length > 0) {
|
||||||
|
setAvailableYears(data.meta.availableYears);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching analytics data:", error);
|
console.error("Error fetching analytics data:", error);
|
||||||
setErrorMessage("Failed to load analytics data. Please try again.");
|
setErrorMessage("Failed to load analytics data. Please try again.");
|
||||||
@@ -89,7 +159,7 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAnalyticsData();
|
fetchAnalyticsData();
|
||||||
}, [dateRange]);
|
}, [dateRange, selectedYear]);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
@@ -105,64 +175,97 @@ export default function AdminAnalytics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to transform data for recharts
|
// Helper to transform data for recharts
|
||||||
const transformChartData = (data: Array<{ date: string; [key: string]: any }>, valueKey: string = "count") => {
|
const transformChartData = (
|
||||||
|
data: Array<{ date: string; [key: string]: any }>,
|
||||||
|
valueKey: string = "count",
|
||||||
|
) => {
|
||||||
if (!data || data.length === 0) return [];
|
if (!data || data.length === 0) return [];
|
||||||
|
|
||||||
return data.map(item => {
|
return data.map((item) => {
|
||||||
const dateStr = item.date;
|
const dateStr = item.date;
|
||||||
// Parse YYYY-MM-DD format
|
// Parse YYYY-MM-DD format
|
||||||
const parts = dateStr.split('-');
|
const parts = dateStr.split("-");
|
||||||
const date = parts.length === 3
|
const date =
|
||||||
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
|
parts.length === 3
|
||||||
|
? new Date(
|
||||||
|
parseInt(parts[0]),
|
||||||
|
parseInt(parts[1]) - 1,
|
||||||
|
parseInt(parts[2]),
|
||||||
|
)
|
||||||
: new Date(dateStr);
|
: new Date(dateStr);
|
||||||
|
|
||||||
// Format with day of week: "Mon, Nov 21"
|
// Format with day of week: "Mon, Nov 21"
|
||||||
const dayOfWeek = date.toLocaleDateString('en-GB', { weekday: 'short' });
|
const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" });
|
||||||
const monthDay = date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' });
|
const monthDay = date.toLocaleDateString("en-GB", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
formattedDate: `${dayOfWeek}, ${monthDay}`,
|
formattedDate: `${dayOfWeek}, ${monthDay}`,
|
||||||
value: Number(item[valueKey]) || 0,
|
value: Number(item[valueKey]) || 0,
|
||||||
[valueKey]: Number(item[valueKey]) || 0
|
[valueKey]: Number(item[valueKey]) || 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to combine orders and revenue data for dual-axis chart
|
// 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; orders?: number; avgOrderValue?: number }>) => {
|
const combineOrdersAndRevenue = (
|
||||||
|
orders: Array<{ date: string; count: number }>,
|
||||||
|
revenue: Array<{
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
orders?: number;
|
||||||
|
avgOrderValue?: number;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
if (!orders || orders.length === 0) return [];
|
if (!orders || orders.length === 0) return [];
|
||||||
|
|
||||||
// Create a map of revenue and AOV by date for quick lookup
|
// Create a map of revenue and AOV by date for quick lookup
|
||||||
const revenueMap = new Map<string, { amount: number; avgOrderValue: number }>();
|
const revenueMap = new Map<
|
||||||
|
string,
|
||||||
|
{ amount: number; avgOrderValue: number }
|
||||||
|
>();
|
||||||
if (revenue && revenue.length > 0) {
|
if (revenue && revenue.length > 0) {
|
||||||
revenue.forEach(r => {
|
revenue.forEach((r) => {
|
||||||
revenueMap.set(r.date, {
|
revenueMap.set(r.date, {
|
||||||
amount: r.amount || 0,
|
amount: r.amount || 0,
|
||||||
avgOrderValue: r.avgOrderValue || 0
|
avgOrderValue: r.avgOrderValue || 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return orders.map(order => {
|
return orders.map((order) => {
|
||||||
const dateStr = order.date;
|
const dateStr = order.date;
|
||||||
const parts = dateStr.split('-');
|
const parts = dateStr.split("-");
|
||||||
const date = parts.length === 3
|
const date =
|
||||||
? new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
|
parts.length === 3
|
||||||
|
? new Date(
|
||||||
|
parseInt(parts[0]),
|
||||||
|
parseInt(parts[1]) - 1,
|
||||||
|
parseInt(parts[2]),
|
||||||
|
)
|
||||||
: new Date(dateStr);
|
: new Date(dateStr);
|
||||||
|
|
||||||
// Format with day of week: "Mon, Nov 21"
|
// Format with day of week: "Mon, Nov 21"
|
||||||
const dayOfWeek = date.toLocaleDateString('en-GB', { weekday: 'short' });
|
const dayOfWeek = date.toLocaleDateString("en-GB", { weekday: "short" });
|
||||||
const monthDay = date.toLocaleDateString('en-GB', { month: 'short', day: 'numeric' });
|
const monthDay = date.toLocaleDateString("en-GB", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
const revenueData = revenueMap.get(dateStr) || { amount: 0, avgOrderValue: 0 };
|
const revenueData = revenueMap.get(dateStr) || {
|
||||||
|
amount: 0,
|
||||||
|
avgOrderValue: 0,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
formattedDate: `${dayOfWeek}, ${monthDay}`,
|
formattedDate: `${dayOfWeek}, ${monthDay}`,
|
||||||
orders: order.count || 0,
|
orders: order.count || 0,
|
||||||
revenue: revenueData.amount,
|
revenue: revenueData.amount,
|
||||||
avgOrderValue: revenueData.avgOrderValue
|
avgOrderValue: revenueData.avgOrderValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -172,34 +275,47 @@ export default function AdminAnalytics() {
|
|||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
const data = payload[0].payload;
|
const data = payload[0].payload;
|
||||||
const dataKey = payload[0].dataKey;
|
const dataKey = payload[0].dataKey;
|
||||||
const isDualAxis = data.orders !== undefined && data.revenue !== undefined;
|
const isDualAxis =
|
||||||
|
data.orders !== undefined && data.revenue !== undefined;
|
||||||
|
|
||||||
// Determine if this is a currency amount or a count
|
// Determine if this is a currency amount or a count
|
||||||
// transformChartData creates both 'value' and the original key (count/amount)
|
// transformChartData creates both 'value' and the original key (count/amount)
|
||||||
// So we check the original key to determine the type
|
// So we check the original key to determine the type
|
||||||
const isAmount = dataKey === 'amount' || dataKey === 'revenue' ||
|
const isAmount =
|
||||||
(dataKey === 'value' && data.amount !== undefined && data.count === undefined);
|
dataKey === "amount" ||
|
||||||
|
dataKey === "revenue" ||
|
||||||
|
(dataKey === "value" &&
|
||||||
|
data.amount !== undefined &&
|
||||||
|
data.count === undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
|
||||||
<p className="text-sm font-medium mb-2">{data.formattedDate || label}</p>
|
<p className="text-sm font-medium mb-2">
|
||||||
|
{data.formattedDate || label}
|
||||||
|
</p>
|
||||||
{isDualAxis ? (
|
{isDualAxis ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm text-blue-600">
|
<p className="text-sm text-blue-600">
|
||||||
Orders: <span className="font-semibold">{data.orders}</span>
|
Orders: <span className="font-semibold">{data.orders}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-green-600">
|
<p className="text-sm text-green-600">
|
||||||
Revenue: <span className="font-semibold">{formatGBP(data.revenue)}</span>
|
Revenue:{" "}
|
||||||
|
<span className="font-semibold">{formatGBP(data.revenue)}</span>
|
||||||
</p>
|
</p>
|
||||||
{data.avgOrderValue !== undefined && data.avgOrderValue > 0 && (
|
{data.avgOrderValue !== undefined && data.avgOrderValue > 0 && (
|
||||||
<p className="text-sm text-purple-600">
|
<p className="text-sm text-purple-600">
|
||||||
Avg Order Value: <span className="font-semibold">{formatGBP(data.avgOrderValue)}</span>
|
Avg Order Value:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{formatGBP(data.avgOrderValue)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-primary">
|
<p className="text-sm text-primary">
|
||||||
{isAmount ? formatGBP(data.value || data.amount || 0) : `${data.value || data.count || 0}`}
|
{isAmount
|
||||||
|
? formatGBP(data.value || data.amount || 0)
|
||||||
|
: `${data.value || data.count || 0}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +325,13 @@ export default function AdminAnalytics() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Trend indicator component for metric cards
|
// Trend indicator component for metric cards
|
||||||
const TrendIndicator = ({ current, previous }: { current: number, previous: number }) => {
|
const TrendIndicator = ({
|
||||||
|
current,
|
||||||
|
previous,
|
||||||
|
}: {
|
||||||
|
current: number;
|
||||||
|
previous: number;
|
||||||
|
}) => {
|
||||||
if (!current || !previous) return null;
|
if (!current || !previous) return null;
|
||||||
|
|
||||||
const percentChange = ((current - previous) / previous) * 100;
|
const percentChange = ((current - previous) / previous) * 100;
|
||||||
@@ -217,10 +339,14 @@ export default function AdminAnalytics() {
|
|||||||
if (Math.abs(percentChange) < 0.1) return null;
|
if (Math.abs(percentChange) < 0.1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center text-xs font-medium ${percentChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
<div
|
||||||
{percentChange >= 0 ?
|
className={`flex items-center text-xs font-medium ${percentChange >= 0 ? "text-green-500" : "text-red-500"}`}
|
||||||
<TrendingUp className="h-3 w-3 mr-1" /> :
|
>
|
||||||
<TrendingDown className="h-3 w-3 mr-1" />}
|
{percentChange >= 0 ? (
|
||||||
|
<TrendingUp className="h-3 w-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="h-3 w-3 mr-1" />
|
||||||
|
)}
|
||||||
{Math.abs(percentChange).toFixed(1)}%
|
{Math.abs(percentChange).toFixed(1)}%
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -228,55 +354,70 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
// Format currency
|
// Format currency
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('en-GB', {
|
return new Intl.NumberFormat("en-GB", {
|
||||||
style: 'currency',
|
style: "currency",
|
||||||
currency: 'GBP',
|
currency: "GBP",
|
||||||
maximumFractionDigits: 0
|
maximumFractionDigits: 0,
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate best month from daily data (for YTD view)
|
// Calculate best month from daily data (for YTD view)
|
||||||
const calculateBestMonth = () => {
|
const calculateBestMonth = () => {
|
||||||
if (dateRange !== 'ytd' || !analyticsData?.revenue?.dailyRevenue || analyticsData.revenue.dailyRevenue.length === 0) {
|
if (
|
||||||
|
dateRange !== "ytd" ||
|
||||||
|
!analyticsData?.revenue?.dailyRevenue ||
|
||||||
|
analyticsData.revenue.dailyRevenue.length === 0
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group daily revenue by month
|
// Group daily revenue by month
|
||||||
const monthlyTotals = new Map<string, { revenue: number; orders: number }>();
|
const monthlyTotals = new Map<
|
||||||
|
string,
|
||||||
|
{ revenue: number; orders: number }
|
||||||
|
>();
|
||||||
|
|
||||||
analyticsData.revenue.dailyRevenue.forEach(day => {
|
analyticsData.revenue.dailyRevenue.forEach((day) => {
|
||||||
const date = new Date(day.date);
|
const date = new Date(day.date);
|
||||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||||
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
|
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
|
||||||
monthlyTotals.set(monthKey, {
|
monthlyTotals.set(monthKey, {
|
||||||
revenue: current.revenue + (day.amount || 0),
|
revenue: current.revenue + (day.amount || 0),
|
||||||
orders: current.orders
|
orders: current.orders,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also group orders by month
|
// Also group orders by month
|
||||||
if (analyticsData.orders?.dailyOrders) {
|
if (analyticsData.orders?.dailyOrders) {
|
||||||
analyticsData.orders.dailyOrders.forEach(day => {
|
analyticsData.orders.dailyOrders.forEach((day) => {
|
||||||
const date = new Date(day.date);
|
const date = new Date(day.date);
|
||||||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||||
const current = monthlyTotals.get(monthKey) || { revenue: 0, orders: 0 };
|
const current = monthlyTotals.get(monthKey) || {
|
||||||
|
revenue: 0,
|
||||||
|
orders: 0,
|
||||||
|
};
|
||||||
monthlyTotals.set(monthKey, {
|
monthlyTotals.set(monthKey, {
|
||||||
revenue: current.revenue,
|
revenue: current.revenue,
|
||||||
orders: current.orders + (day.count || 0)
|
orders: current.orders + (day.count || 0),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the month with highest revenue
|
// Find the month with highest revenue
|
||||||
let bestMonth: { month: string; revenue: number; orders: number } | null = null;
|
let bestMonth: { month: string; revenue: number; orders: number } | null =
|
||||||
|
null;
|
||||||
monthlyTotals.forEach((totals, monthKey) => {
|
monthlyTotals.forEach((totals, monthKey) => {
|
||||||
if (!bestMonth || totals.revenue > bestMonth.revenue) {
|
if (!bestMonth || totals.revenue > bestMonth.revenue) {
|
||||||
const [year, month] = monthKey.split('-');
|
const [year, month] = monthKey.split("-");
|
||||||
const monthName = new Date(parseInt(year), parseInt(month) - 1, 1).toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
const monthName = new Date(
|
||||||
|
parseInt(year),
|
||||||
|
parseInt(month) - 1,
|
||||||
|
1,
|
||||||
|
).toLocaleDateString("en-GB", { month: "long", year: "numeric" });
|
||||||
bestMonth = {
|
bestMonth = {
|
||||||
month: monthName,
|
month: monthName,
|
||||||
revenue: totals.revenue,
|
revenue: totals.revenue,
|
||||||
orders: totals.orders
|
orders: totals.orders,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -298,24 +439,60 @@ export default function AdminAnalytics() {
|
|||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold">Dashboard Analytics</h2>
|
<h2 className="text-2xl font-bold">
|
||||||
<p className="text-muted-foreground">Overview of your marketplace performance</p>
|
Dashboard Analytics
|
||||||
|
{!isViewingCurrentYear && (
|
||||||
|
<span className="ml-2 text-lg font-normal text-muted-foreground">
|
||||||
|
({selectedYear})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{isViewingCurrentYear
|
||||||
|
? "Overview of your marketplace performance"
|
||||||
|
: `Historical data for ${selectedYear}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Year selector */}
|
||||||
<Select
|
<Select
|
||||||
value={dateRange}
|
value={selectedYear.toString()}
|
||||||
|
onValueChange={(value) => setSelectedYear(parseInt(value, 10))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue placeholder="Year" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableYears.map((year) => (
|
||||||
|
<SelectItem key={year} value={year.toString()}>
|
||||||
|
{year}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Date range selector - only show options for current year */}
|
||||||
|
<Select
|
||||||
|
value={isViewingCurrentYear ? dateRange : "year"}
|
||||||
onValueChange={setDateRange}
|
onValueChange={setDateRange}
|
||||||
|
disabled={!isViewingCurrentYear}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[140px]">
|
<SelectTrigger className="w-[140px]">
|
||||||
<SelectValue placeholder="Last 7 days" />
|
<SelectValue placeholder="Last 7 days" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
{isViewingCurrentYear ? (
|
||||||
|
<>
|
||||||
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
<SelectItem value="24hours">Last 24 hours</SelectItem>
|
||||||
<SelectItem value="7days">Last 7 days</SelectItem>
|
<SelectItem value="7days">Last 7 days</SelectItem>
|
||||||
<SelectItem value="30days">Last 30 days</SelectItem>
|
<SelectItem value="30days">Last 30 days</SelectItem>
|
||||||
<SelectItem value="ytd">Year to Date</SelectItem>
|
<SelectItem value="ytd">Year to Date</SelectItem>
|
||||||
<SelectItem value="year">Last Year</SelectItem>
|
<SelectItem value="year">Full Year</SelectItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<SelectItem value="year">Full Year</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
@@ -326,7 +503,7 @@ export default function AdminAnalytics() {
|
|||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`}
|
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -335,7 +512,7 @@ export default function AdminAnalytics() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowDebug(!showDebug)}
|
onClick={() => setShowDebug(!showDebug)}
|
||||||
>
|
>
|
||||||
{showDebug ? 'Hide' : 'Show'} Debug
|
{showDebug ? "Hide" : "Show"} Debug
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,12 +528,19 @@ export default function AdminAnalytics() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold mb-2">Orders:</div>
|
<div className="font-semibold mb-2">Orders:</div>
|
||||||
<div className="pl-4 space-y-1">
|
<div className="pl-4 space-y-1">
|
||||||
<div>Total: {analyticsData?.orders?.total || 'N/A'}</div>
|
<div>Total: {analyticsData?.orders?.total || "N/A"}</div>
|
||||||
<div>Today: {analyticsData?.orders?.totalToday || 'N/A'}</div>
|
<div>Today: {analyticsData?.orders?.totalToday || "N/A"}</div>
|
||||||
<div>Daily Orders Array Length: {analyticsData?.orders?.dailyOrders?.length || 0}</div>
|
<div>
|
||||||
|
Daily Orders Array Length:{" "}
|
||||||
|
{analyticsData?.orders?.dailyOrders?.length || 0}
|
||||||
|
</div>
|
||||||
<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 p-2 rounded overflow-auto max-h-32">
|
||||||
{JSON.stringify(analyticsData?.orders?.dailyOrders?.slice(0, 3), null, 2)}
|
{JSON.stringify(
|
||||||
|
analyticsData?.orders?.dailyOrders?.slice(0, 3),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,12 +548,19 @@ export default function AdminAnalytics() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold mb-2">Revenue:</div>
|
<div className="font-semibold mb-2">Revenue:</div>
|
||||||
<div className="pl-4 space-y-1">
|
<div className="pl-4 space-y-1">
|
||||||
<div>Total: {analyticsData?.revenue?.total || 'N/A'}</div>
|
<div>Total: {analyticsData?.revenue?.total || "N/A"}</div>
|
||||||
<div>Today: {analyticsData?.revenue?.today || 'N/A'}</div>
|
<div>Today: {analyticsData?.revenue?.today || "N/A"}</div>
|
||||||
<div>Daily Revenue Array Length: {analyticsData?.revenue?.dailyRevenue?.length || 0}</div>
|
<div>
|
||||||
|
Daily Revenue Array Length:{" "}
|
||||||
|
{analyticsData?.revenue?.dailyRevenue?.length || 0}
|
||||||
|
</div>
|
||||||
<div>First 3 Daily Revenue:</div>
|
<div>First 3 Daily Revenue:</div>
|
||||||
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
||||||
{JSON.stringify(analyticsData?.revenue?.dailyRevenue?.slice(0, 3), null, 2)}
|
{JSON.stringify(
|
||||||
|
analyticsData?.revenue?.dailyRevenue?.slice(0, 3),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -377,17 +568,26 @@ export default function AdminAnalytics() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold mb-2">Vendors:</div>
|
<div className="font-semibold mb-2">Vendors:</div>
|
||||||
<div className="pl-4 space-y-1">
|
<div className="pl-4 space-y-1">
|
||||||
<div>Total: {analyticsData?.vendors?.total || 'N/A'}</div>
|
<div>Total: {analyticsData?.vendors?.total || "N/A"}</div>
|
||||||
<div>Daily Growth Array Length: {analyticsData?.vendors?.dailyGrowth?.length || 0}</div>
|
<div>
|
||||||
|
Daily Growth Array Length:{" "}
|
||||||
|
{analyticsData?.vendors?.dailyGrowth?.length || 0}
|
||||||
|
</div>
|
||||||
<div>First 3 Daily Growth:</div>
|
<div>First 3 Daily Growth:</div>
|
||||||
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
|
||||||
{JSON.stringify(analyticsData?.vendors?.dailyGrowth?.slice(0, 3), null, 2)}
|
{JSON.stringify(
|
||||||
|
analyticsData?.vendors?.dailyGrowth?.slice(0, 3),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details className="mt-4">
|
<details className="mt-4">
|
||||||
<summary className="font-semibold cursor-pointer">Full JSON Response</summary>
|
<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]">
|
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
|
||||||
{JSON.stringify(analyticsData, null, 2)}
|
{JSON.stringify(analyticsData, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -407,14 +607,24 @@ export default function AdminAnalytics() {
|
|||||||
<Trophy className="h-5 w-5 text-green-600" />
|
<Trophy className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground">Best Month (YTD)</div>
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
<div className="text-xl font-bold text-green-600">{bestMonth.month}</div>
|
Best Month (YTD)
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-green-600">
|
||||||
|
{bestMonth.month}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm font-medium text-muted-foreground">Revenue</div>
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
<div className="text-xl font-bold">{formatCurrency(bestMonth.revenue)}</div>
|
Revenue
|
||||||
<div className="text-xs text-muted-foreground mt-1">{bestMonth.orders.toLocaleString()} orders</div>
|
</div>
|
||||||
|
<div className="text-xl font-bold">
|
||||||
|
{formatCurrency(bestMonth.revenue)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
{bestMonth.orders.toLocaleString()} orders
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -426,13 +636,15 @@ export default function AdminAnalytics() {
|
|||||||
<Card>
|
<Card>
|
||||||
<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">Total Orders</CardTitle>
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total Orders
|
||||||
|
</CardTitle>
|
||||||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">
|
||||||
{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>Today: {analyticsData?.orders?.totalToday || 0}</span>
|
||||||
@@ -446,17 +658,26 @@ export default function AdminAnalytics() {
|
|||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-3 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?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
|
) : analyticsData?.orders?.dailyOrders &&
|
||||||
|
analyticsData.orders.dailyOrders.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-3 h-12">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart data={transformChartData(analyticsData.orders.dailyOrders, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
<RechartsBarChart
|
||||||
|
data={transformChartData(
|
||||||
|
analyticsData.orders.dailyOrders,
|
||||||
|
"count",
|
||||||
|
)}
|
||||||
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
|
>
|
||||||
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
No chart data available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -465,7 +686,9 @@ export default function AdminAnalytics() {
|
|||||||
<Card>
|
<Card>
|
||||||
<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">Total Revenue</CardTitle>
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total Revenue
|
||||||
|
</CardTitle>
|
||||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -474,7 +697,9 @@ 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>Today: {formatCurrency(analyticsData?.revenue?.today || 0)}</span>
|
<span>
|
||||||
|
Today: {formatCurrency(analyticsData?.revenue?.today || 0)}
|
||||||
|
</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} // Rough estimate
|
||||||
@@ -485,17 +710,26 @@ export default function AdminAnalytics() {
|
|||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-3 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?.revenue?.dailyRevenue && analyticsData.revenue.dailyRevenue.length > 0 ? (
|
) : analyticsData?.revenue?.dailyRevenue &&
|
||||||
|
analyticsData.revenue.dailyRevenue.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-3 h-12">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart data={transformChartData(analyticsData.revenue.dailyRevenue, 'amount')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
<RechartsBarChart
|
||||||
|
data={transformChartData(
|
||||||
|
analyticsData.revenue.dailyRevenue,
|
||||||
|
"amount",
|
||||||
|
)}
|
||||||
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
|
>
|
||||||
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
No chart data available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -510,11 +744,13 @@ export default function AdminAnalytics() {
|
|||||||
</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">
|
||||||
<span>Active: {analyticsData?.vendors?.active || 0}</span>
|
<span>Active: {analyticsData?.vendors?.active || 0}</span>
|
||||||
<span className="ml-2">Stores: {analyticsData?.vendors?.activeStores || 0}</span>
|
<span className="ml-2">
|
||||||
|
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-1">
|
||||||
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
|
||||||
@@ -528,17 +764,26 @@ export default function AdminAnalytics() {
|
|||||||
<div className="mt-3 h-12 flex items-center justify-center">
|
<div className="mt-3 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.length > 0 ? (
|
) : analyticsData?.vendors?.dailyGrowth &&
|
||||||
|
analyticsData.vendors.dailyGrowth.length > 0 ? (
|
||||||
<div className="mt-3 h-12">
|
<div className="mt-3 h-12">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
<RechartsBarChart
|
||||||
|
data={transformChartData(
|
||||||
|
analyticsData.vendors.dailyGrowth,
|
||||||
|
"count",
|
||||||
|
)}
|
||||||
|
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
|
||||||
|
>
|
||||||
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 text-xs text-muted-foreground">No chart data available</div>
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
No chart data available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -553,7 +798,7 @@ export default function AdminAnalytics() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">
|
<div className="text-2xl font-bold">
|
||||||
{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>New This Week: {analyticsData?.products?.recent || 0}</span>
|
||||||
@@ -573,7 +818,8 @@ export default function AdminAnalytics() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Order Trends</CardTitle>
|
<CardTitle>Order Trends</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Daily order volume and revenue processed over the selected time period
|
Daily order volume and revenue processed over the selected time
|
||||||
|
period
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -581,16 +827,19 @@ export default function AdminAnalytics() {
|
|||||||
<div className="flex items-center justify-center h-80">
|
<div className="flex items-center justify-center h-80">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<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>
|
||||||
<p className="text-sm text-muted-foreground">Loading chart data...</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Loading chart data...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : analyticsData?.orders?.dailyOrders && analyticsData.orders.dailyOrders.length > 0 ? (
|
) : analyticsData?.orders?.dailyOrders &&
|
||||||
|
analyticsData.orders.dailyOrders.length > 0 ? (
|
||||||
<div className="h-80">
|
<div className="h-80">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<ComposedChart
|
<ComposedChart
|
||||||
data={combineOrdersAndRevenue(
|
data={combineOrdersAndRevenue(
|
||||||
analyticsData.orders.dailyOrders,
|
analyticsData.orders.dailyOrders,
|
||||||
analyticsData.revenue?.dailyRevenue || []
|
analyticsData.revenue?.dailyRevenue || [],
|
||||||
)}
|
)}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
>
|
>
|
||||||
@@ -605,19 +854,52 @@ export default function AdminAnalytics() {
|
|||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
label={{ value: 'Orders', angle: -90, position: 'insideLeft' }}
|
label={{
|
||||||
|
value: "Orders",
|
||||||
|
angle: -90,
|
||||||
|
position: "insideLeft",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
orientation="right"
|
orientation="right"
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
tickFormatter={(value) => `£${(value / 1000).toFixed(0)}k`}
|
tickFormatter={(value) =>
|
||||||
label={{ value: 'Revenue / AOV', angle: 90, position: 'insideRight' }}
|
`£${(value / 1000).toFixed(0)}k`
|
||||||
|
}
|
||||||
|
label={{
|
||||||
|
value: "Revenue / AOV",
|
||||||
|
angle: 90,
|
||||||
|
position: "insideRight",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
<Bar yAxisId="left" dataKey="orders" fill="#3b82f6" radius={[2, 2, 0, 0]} name="Orders" />
|
<Bar
|
||||||
<Line yAxisId="right" type="monotone" dataKey="revenue" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 4 }} name="Revenue" />
|
yAxisId="left"
|
||||||
<Line yAxisId="right" type="monotone" dataKey="avgOrderValue" stroke="#a855f7" strokeWidth={2} strokeDasharray="5 5" dot={{ fill: '#a855f7', r: 3 }} name="Avg Order Value" />
|
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"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
yAxisId="right"
|
||||||
|
type="monotone"
|
||||||
|
dataKey="avgOrderValue"
|
||||||
|
stroke="#a855f7"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray="5 5"
|
||||||
|
dot={{ fill: "#a855f7", r: 3 }}
|
||||||
|
name="Avg Order Value"
|
||||||
|
/>
|
||||||
</ComposedChart>
|
</ComposedChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,20 +910,30 @@ export default function AdminAnalytics() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Calculate totals for the selected period */}
|
{/* Calculate totals for the selected period */}
|
||||||
{analyticsData?.orders?.dailyOrders && analyticsData?.revenue?.dailyRevenue && (
|
{analyticsData?.orders?.dailyOrders &&
|
||||||
|
analyticsData?.revenue?.dailyRevenue && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||||
<div className="bg-muted/50 p-4 rounded-lg">
|
<div className="bg-muted/50 p-4 rounded-lg">
|
||||||
<div className="text-sm font-medium mb-1">Total Revenue</div>
|
<div className="text-sm font-medium mb-1">
|
||||||
|
Total Revenue
|
||||||
|
</div>
|
||||||
<div className="text-2xl font-bold text-green-600">
|
<div className="text-2xl font-bold text-green-600">
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
analyticsData.revenue.dailyRevenue.reduce((sum, day) => sum + (day.amount || 0), 0)
|
analyticsData.revenue.dailyRevenue.reduce(
|
||||||
|
(sum, day) => sum + (day.amount || 0),
|
||||||
|
0,
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">Total Orders</div>
|
<div className="text-sm font-medium mb-1">
|
||||||
|
Total Orders
|
||||||
|
</div>
|
||||||
<div className="text-2xl font-bold text-blue-600">
|
<div className="text-2xl font-bold text-blue-600">
|
||||||
{analyticsData.orders.dailyOrders.reduce((sum, day) => sum + (day.count || 0), 0).toLocaleString()}
|
{analyticsData.orders.dailyOrders
|
||||||
|
.reduce((sum, day) => sum + (day.count || 0), 0)
|
||||||
|
.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -663,13 +955,22 @@ export default function AdminAnalytics() {
|
|||||||
<div className="flex items-center justify-center h-80">
|
<div className="flex items-center justify-center h-80">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<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>
|
||||||
<p className="text-sm text-muted-foreground">Loading chart data...</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Loading chart data...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : analyticsData?.vendors?.dailyGrowth && analyticsData.vendors.dailyGrowth.length > 0 ? (
|
) : analyticsData?.vendors?.dailyGrowth &&
|
||||||
|
analyticsData.vendors.dailyGrowth.length > 0 ? (
|
||||||
<div className="h-80">
|
<div className="h-80">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RechartsBarChart data={transformChartData(analyticsData.vendors.dailyGrowth, 'count')} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
<RechartsBarChart
|
||||||
|
data={transformChartData(
|
||||||
|
analyticsData.vendors.dailyGrowth,
|
||||||
|
"count",
|
||||||
|
)}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="formattedDate"
|
dataKey="formattedDate"
|
||||||
@@ -680,7 +981,11 @@ export default function AdminAnalytics() {
|
|||||||
/>
|
/>
|
||||||
<YAxis tick={{ fontSize: 12 }} />
|
<YAxis tick={{ fontSize: 12 }} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="#8b5cf6"
|
||||||
|
radius={[2, 2, 0, 0]}
|
||||||
|
/>
|
||||||
</RechartsBarChart>
|
</RechartsBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -693,40 +998,62 @@ export default function AdminAnalytics() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-4 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">Active Vendors</div>
|
<div className="text-sm font-medium mb-1">Active Vendors</div>
|
||||||
<div className="text-2xl font-bold">{analyticsData?.vendors?.active?.toLocaleString() || '0'}</div>
|
<div className="text-2xl font-bold">
|
||||||
|
{analyticsData?.vendors?.active?.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">Active Stores</div>
|
<div className="text-sm font-medium mb-1">Active Stores</div>
|
||||||
<div className="text-2xl font-bold">{analyticsData?.vendors?.activeStores?.toLocaleString() || '0'}</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>
|
||||||
|
|
||||||
{/* Top Vendors by Revenue */}
|
{/* Top Vendors by Revenue */}
|
||||||
{analyticsData?.vendors?.topVendors && analyticsData.vendors.topVendors.length > 0 && (
|
{analyticsData?.vendors?.topVendors &&
|
||||||
|
analyticsData.vendors.topVendors.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Top Vendors by Revenue</h3>
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
|
Top Vendors by Revenue
|
||||||
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{analyticsData.vendors.topVendors.map((vendor, index) => (
|
{analyticsData.vendors.topVendors.map((vendor, index) => (
|
||||||
<div key={vendor.vendorId} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
|
<div
|
||||||
|
key={vendor.vendorId}
|
||||||
|
className="flex items-center justify-between p-3 bg-muted/30 rounded-lg"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
|
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{vendor.vendorName}</div>
|
<div className="font-medium">
|
||||||
<div className="text-xs text-muted-foreground">{vendor.orderCount} orders</div>
|
{vendor.vendorName}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{vendor.orderCount} orders
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="font-semibold text-green-600">{formatCurrency(vendor.totalRevenue)}</div>
|
<div className="font-semibold text-green-600">
|
||||||
|
{formatCurrency(vendor.totalRevenue)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ export function NotificationProviderWrapper({ children }: NotificationProviderWr
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import {
|
||||||
import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator, Copy } from "lucide-react";
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
Trash,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Calculator,
|
||||||
|
Copy,
|
||||||
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Product } from "@/models/products";
|
import { Product } from "@/models/products";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -24,79 +39,73 @@ const ProductTable = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onToggleEnabled,
|
onToggleEnabled,
|
||||||
onProfitAnalysis,
|
onProfitAnalysis,
|
||||||
getCategoryNameById
|
getCategoryNameById,
|
||||||
}: ProductTableProps) => {
|
}: ProductTableProps) => {
|
||||||
|
// Separate enabled and disabled products
|
||||||
|
const enabledProducts = products.filter((p) => p.enabled !== false);
|
||||||
|
const disabledProducts = products.filter((p) => p.enabled === false);
|
||||||
|
|
||||||
const sortedProducts = [...products].sort((a, b) => {
|
const sortByCategory = (productList: Product[]) => {
|
||||||
|
return [...productList].sort((a, b) => {
|
||||||
const categoryNameA = getCategoryNameById(a.category);
|
const categoryNameA = getCategoryNameById(a.category);
|
||||||
const categoryNameB = getCategoryNameById(b.category);
|
const categoryNameB = getCategoryNameById(b.category);
|
||||||
return categoryNameA.localeCompare(categoryNameB);
|
return categoryNameA.localeCompare(categoryNameB);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedEnabledProducts = sortByCategory(enabledProducts);
|
||||||
|
const sortedDisabledProducts = sortByCategory(disabledProducts);
|
||||||
|
|
||||||
const getStockIcon = (product: Product) => {
|
const getStockIcon = (product: Product) => {
|
||||||
if (!product.stockTracking) return null;
|
if (!product.stockTracking) return null;
|
||||||
|
|
||||||
if (product.stockStatus === 'out_of_stock') {
|
if (product.stockStatus === "out_of_stock") {
|
||||||
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
||||||
} else if (product.stockStatus === 'low_stock') {
|
} else if (product.stockStatus === "low_stock") {
|
||||||
return <AlertCircle className="h-4 w-4 text-amber-500" />;
|
return <AlertCircle className="h-4 w-4 text-amber-500" />;
|
||||||
} else {
|
} else {
|
||||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderProductRow = (product: Product, isDisabled: boolean = false) => (
|
||||||
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
<TableRow
|
||||||
<Table className="relative">
|
key={product._id}
|
||||||
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
className={`transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70 ${isDisabled ? "opacity-60" : ""}`}
|
||||||
<TableRow className="hover:bg-transparent">
|
>
|
||||||
<TableHead className="w-[200px]">Product</TableHead>
|
|
||||||
<TableHead className="hidden sm:table-cell text-center">Category</TableHead>
|
|
||||||
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
|
|
||||||
<TableHead className="text-center">Stock</TableHead>
|
|
||||||
<TableHead className="hidden lg:table-cell text-center">Enabled</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loading ? (
|
|
||||||
Array.from({ length: 1 }).map((_, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
<TableCell>Loading...</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : sortedProducts.length > 0 ? (
|
|
||||||
sortedProducts.map((product) => (
|
|
||||||
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
||||||
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
||||||
{getCategoryNameById(product.category)}
|
{getCategoryNameById(product.category)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden sm:table-cell text-center">{getCategoryNameById(product.category)}</TableCell>
|
<TableCell className="hidden sm:table-cell text-center">
|
||||||
<TableCell className="hidden md:table-cell text-center">{product.unitType}</TableCell>
|
{getCategoryNameById(product.category)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell text-center">
|
||||||
|
{product.unitType}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{product.stockTracking ? (
|
{product.stockTracking ? (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{getStockIcon(product)}
|
{getStockIcon(product)}
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{product.currentStock !== undefined ? product.currentStock : 0} {product.unitType}
|
{product.currentStock !== undefined ? product.currentStock : 0}{" "}
|
||||||
|
{product.unitType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="text-xs">Not Tracked</Badge>
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Not Tracked
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden lg:table-cell text-center">
|
<TableCell className="hidden lg:table-cell text-center">
|
||||||
<Switch
|
<Switch
|
||||||
checked={product.enabled !== false}
|
checked={product.enabled !== false}
|
||||||
onCheckedChange={(checked) => onToggleEnabled(product._id as string, checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleEnabled(product._id as string, checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right flex justify-end space-x-1">
|
<TableCell className="text-right flex justify-end space-x-1">
|
||||||
@@ -104,7 +113,9 @@ const ProductTable = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onProfitAnalysis(product._id as string, product.name)}
|
onClick={() =>
|
||||||
|
onProfitAnalysis(product._id as string, product.name)
|
||||||
|
}
|
||||||
className="text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
className="text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
||||||
title="Profit Analysis"
|
title="Profit Analysis"
|
||||||
>
|
>
|
||||||
@@ -122,7 +133,12 @@ const ProductTable = ({
|
|||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="ghost" size="sm" onClick={() => onEdit(product)} title="Edit Product">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEdit(product)}
|
||||||
|
title="Edit Product"
|
||||||
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -136,17 +152,70 @@ const ProductTable = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTableHeader = () => (
|
||||||
|
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
||||||
|
<TableRow className="hover:bg-transparent">
|
||||||
|
<TableHead className="w-[200px]">Product</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell text-center">
|
||||||
|
Category
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
|
||||||
|
<TableHead className="text-center">Stock</TableHead>
|
||||||
|
<TableHead className="hidden lg:table-cell text-center">
|
||||||
|
Enabled
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Enabled Products Table */}
|
||||||
|
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
||||||
|
<Table className="relative">
|
||||||
|
{renderTableHeader()}
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
Array.from({ length: 1 }).map((_, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
<TableCell>Loading...</TableCell>
|
||||||
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
) : sortedEnabledProducts.length > 0 ? (
|
||||||
|
sortedEnabledProducts.map((product) => renderProductRow(product))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
<TableCell colSpan={6} className="h-24 text-center">
|
||||||
No products found.
|
No enabled products found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Disabled Products Section */}
|
||||||
|
{!loading && disabledProducts.length > 0 && (
|
||||||
|
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden bg-gray-50/30 dark:bg-zinc-900/30">
|
||||||
|
<Table className="relative">
|
||||||
|
{renderTableHeader()}
|
||||||
|
<TableBody>
|
||||||
|
{sortedDisabledProducts.map((product) =>
|
||||||
|
renderProductRow(product, true),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ export interface Product {
|
|||||||
_id?: string;
|
_id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
unitType: 'pcs' | 'gr' | 'kg' | 'ml' | 'oz' | 'lb';
|
unitType: "pcs" | "gr" | "kg" | "ml" | "oz" | "lb";
|
||||||
category: string;
|
category: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
// Stock management fields
|
// Stock management fields
|
||||||
stockTracking?: boolean;
|
stockTracking?: boolean;
|
||||||
currentStock?: number;
|
currentStock?: number;
|
||||||
lowStockThreshold?: number;
|
lowStockThreshold?: number;
|
||||||
stockStatus?: 'in_stock' | 'low_stock' | 'out_of_stock';
|
stockStatus?: "in_stock" | "low_stock" | "out_of_stock";
|
||||||
pricing: Array<{
|
pricing: Array<{
|
||||||
minQuantity: number;
|
minQuantity: number;
|
||||||
pricePerUnit: number;
|
pricePerUnit: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "66e9543",
|
"commitHash": "74f3a2c",
|
||||||
"buildTime": "2025-12-31T07:04:51.067Z"
|
"buildTime": "2026-01-05T21:55:11.573Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user