Stat cards fixes, mini component tweaks

This commit is contained in:
2026-02-04 12:24:11 -05:00
parent a703019b0b
commit fd14af0f9e
9 changed files with 2139 additions and 335 deletions

1106
docs/prod_registry.class.php Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { import {
Package, Package,
Truck, Truck,
@@ -16,7 +15,6 @@ import {
XCircle, XCircle,
DollarSign, DollarSign,
Activity, Activity,
AlertCircle,
FileText, FileText,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -66,7 +64,7 @@ const EVENT_TYPES = {
gradient: "from-red-800 to-red-700", gradient: "from-red-800 to-red-700",
}, },
[METRIC_IDS.PAYMENT_REFUNDED]: { [METRIC_IDS.PAYMENT_REFUNDED]: {
label: "Payment Refunded", label: "Payment Refund",
color: "bg-orange-200", color: "bg-orange-200",
textColor: "text-orange-50", textColor: "text-orange-50",
iconColor: "text-orange-800", iconColor: "text-orange-800",
@@ -94,22 +92,22 @@ const EVENT_ICONS = {
const LoadingState = () => ( const LoadingState = () => (
<div className="flex gap-3 px-4"> <div className="flex gap-3 px-4">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<Card key={i} className="w-[210px] h-[125px] shrink-0 bg-gradient-to-br from-gray-900 to-gray-800 backdrop-blur-sm"> <Card key={i} className="w-[210px] h-[125px] shrink-0 bg-gradient-to-br from-gray-900 to-gray-800 backdrop-blur-md border-white/10">
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-3 py-2 pb-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-3 py-2 pb-0">
<div className="flex items-baseline justify-between w-full pr-1"> <div className="flex items-baseline justify-between w-full pr-1">
<Skeleton className="h-4 w-20 bg-gray-700" /> <Skeleton className="h-3 w-20 bg-white/20" />
<Skeleton className="h-3 w-14 bg-gray-700" /> <Skeleton className="h-3 w-14 bg-white/20" />
</div> </div>
<div className="relative p-2"> <div className="relative p-2">
<div className="absolute inset-0 rounded-full bg-gray-300" /> <div className="absolute inset-0 rounded-full bg-white/20" />
<Skeleton className="h-4 w-4 bg-gray-700 relative rounded-full" /> <Skeleton className="h-4 w-4 bg-white/10 relative rounded-full" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-3 pt-1"> <CardContent className="p-3 pt-1">
<div className="space-y-2"> <div className="space-y-2">
<Skeleton className="h-7 w-36 bg-gray-700" /> <Skeleton className="h-7 w-36 bg-white/20" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-4 w-28 bg-gray-700" /> <Skeleton className="h-4 w-28 bg-white/10" />
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -120,12 +118,12 @@ const LoadingState = () => (
// Empty State Component // Empty State Component
const EmptyState = () => ( const EmptyState = () => (
<Card className="w-[210px] h-[125px] bg-gradient-to-br from-gray-900 to-gray-800 backdrop-blur-sm"> <Card className="w-[210px] h-[125px] bg-gradient-to-br from-gray-900 to-gray-800 backdrop-blur-md border-white/10">
<CardContent className="flex flex-col items-center justify-center h-full text-center p-4"> <CardContent className="flex flex-col items-center justify-center h-full text-center p-4">
<div className="bg-gray-800 rounded-full p-2 mb-2"> <div className="bg-white/10 rounded-full p-2 mb-2">
<Activity className="h-4 w-4 text-gray-400" /> <Activity className="h-4 w-4 text-gray-300" />
</div> </div>
<p className="text-sm text-gray-400 font-medium"> <p className="text-xs font-medium text-gray-300 uppercase tracking-wide">
No recent activity No recent activity
</p> </p>
</CardContent> </CardContent>
@@ -141,14 +139,14 @@ const EventCard = ({ event }) => {
return ( return (
<EventDialog event={event}> <EventDialog event={event}>
<Card className={`w-[210px] border-none shrink-0 hover:brightness-110 cursor-pointer transition-colors h-[125px] bg-gradient-to-br ${eventType.gradient} backdrop-blur-sm`}> <Card className={`w-[230px] border-white/10 shrink-0 hover:brightness-110 cursor-pointer transition-all h-[125px] bg-gradient-to-br ${eventType.gradient} backdrop-blur-md`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-3 py-2 pb-0"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-3 py-2 pb-0">
<div className="flex items-baseline justify-between w-full pr-1"> <div className="flex items-baseline justify-between w-full pr-1">
<CardTitle className={`text-sm font-bold ${eventType.textColor}`}> <CardTitle className={`text-xs font-medium ${eventType.textColor} uppercase tracking-wide`}>
{eventType.label} {eventType.label}
</CardTitle> </CardTitle>
{event.datetime && ( {event.datetime && (
<CardDescription className={`text-xs ${eventType.textColor} opacity-80`}> <CardDescription className={`text-xs ${eventType.textColor} opacity-70`}>
{format(new Date(event.datetime), "h:mm a")} {format(new Date(event.datetime), "h:mm a")}
</CardDescription> </CardDescription>
)} )}

View File

@@ -85,7 +85,7 @@ const MiniRealtimeAnalytics = () => {
); );
} }
if (loading) { if (loading && !basicData.byMinute?.length) {
return ( return (
<div> <div>
<div className="grid grid-cols-2 gap-2 mt-1 mb-2"> <div className="grid grid-cols-2 gap-2 mt-1 mb-2">
@@ -141,18 +141,18 @@ const MiniRealtimeAnalytics = () => {
<DashboardStatCardMini <DashboardStatCardMini
title="Last 30 Minutes" title="Last 30 Minutes"
value={basicData.last30MinUsers} value={basicData.last30MinUsers}
description="Active users" subtitle="Active users"
gradient="sky" gradient="sky"
icon={Users} icon={Users}
iconBackground="bg-sky-300" iconBackground="bg-sky-400"
/> />
<DashboardStatCardMini <DashboardStatCardMini
title="Last 5 Minutes" title="Last 5 Minutes"
value={basicData.last5MinUsers} value={basicData.last5MinUsers}
description="Active users" subtitle="Active users"
gradient="sky" gradient="sky"
icon={Activity} icon={Activity}
iconBackground="bg-sky-300" iconBackground="bg-sky-400"
/> />
</div> </div>

View File

@@ -140,31 +140,20 @@ const MiniSalesChart = ({ className = "" }) => {
); );
} }
// Helper to calculate trend direction // Helper to calculate trend values (positive = up, negative = down)
const getRevenueTrend = () => {
const current = summaryStats.periodProgress < 100
? (projection?.projectedRevenue || summaryStats.totalRevenue)
: summaryStats.totalRevenue;
return current >= summaryStats.prevRevenue ? "up" : "down";
};
const getRevenueTrendValue = () => { const getRevenueTrendValue = () => {
const current = summaryStats.periodProgress < 100 const current = summaryStats.periodProgress < 100
? (projection?.projectedRevenue || summaryStats.totalRevenue) ? (projection?.projectedRevenue || summaryStats.totalRevenue)
: summaryStats.totalRevenue; : summaryStats.totalRevenue;
return `${Math.abs(Math.round((current - summaryStats.prevRevenue) / summaryStats.prevRevenue * 100))}%`; if (!summaryStats.prevRevenue) return 0;
}; return ((current - summaryStats.prevRevenue) / summaryStats.prevRevenue) * 100;
const getOrdersTrend = () => {
const projected = Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress));
const current = summaryStats.periodProgress < 100 ? projected : summaryStats.totalOrders;
return current >= summaryStats.prevOrders ? "up" : "down";
}; };
const getOrdersTrendValue = () => { const getOrdersTrendValue = () => {
const projected = Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress)); const projected = Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress));
const current = summaryStats.periodProgress < 100 ? projected : summaryStats.totalOrders; const current = summaryStats.periodProgress < 100 ? projected : summaryStats.totalOrders;
return `${Math.abs(Math.round((current - summaryStats.prevOrders) / summaryStats.prevOrders * 100))}%`; if (!summaryStats.prevOrders) return 0;
return ((current - summaryStats.prevOrders) / summaryStats.prevOrders) * 100;
}; };
if (loading && !data) { if (loading && !data) {
@@ -190,7 +179,7 @@ const MiniSalesChart = ({ className = "" }) => {
<div className="space-y-2"> <div className="space-y-2">
{/* Stat Cards */} {/* Stat Cards */}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{loading ? ( {loading && !data?.length ? (
<> <>
<DashboardStatCardMiniSkeleton gradient="slate" /> <DashboardStatCardMiniSkeleton gradient="slate" />
<DashboardStatCardMiniSkeleton gradient="slate" /> <DashboardStatCardMiniSkeleton gradient="slate" />
@@ -200,13 +189,10 @@ const MiniSalesChart = ({ className = "" }) => {
<DashboardStatCardMini <DashboardStatCardMini
title="30 Days Revenue" title="30 Days Revenue"
value={formatCurrency(summaryStats.totalRevenue, false)} value={formatCurrency(summaryStats.totalRevenue, false)}
description={`Prev: ${formatCurrency(summaryStats.prevRevenue, false)}`} subtitle={`Prev: ${formatCurrency(summaryStats.prevRevenue, false)}`}
trend={{ trend={{ value: getRevenueTrendValue() }}
direction: getRevenueTrend(),
value: getRevenueTrendValue(),
}}
icon={PiggyBank} icon={PiggyBank}
iconBackground="bg-emerald-300" iconBackground="bg-emerald-400"
gradient="slate" gradient="slate"
className={!visibleMetrics.revenue ? 'opacity-50' : ''} className={!visibleMetrics.revenue ? 'opacity-50' : ''}
onClick={() => toggleMetric('revenue')} onClick={() => toggleMetric('revenue')}
@@ -214,13 +200,10 @@ const MiniSalesChart = ({ className = "" }) => {
<DashboardStatCardMini <DashboardStatCardMini
title="30 Days Orders" title="30 Days Orders"
value={summaryStats.totalOrders.toLocaleString()} value={summaryStats.totalOrders.toLocaleString()}
description={`Prev: ${summaryStats.prevOrders.toLocaleString()}`} subtitle={`Prev: ${summaryStats.prevOrders.toLocaleString()}`}
trend={{ trend={{ value: getOrdersTrendValue() }}
direction: getOrdersTrend(),
value: getOrdersTrendValue(),
}}
icon={Truck} icon={Truck}
iconBackground="bg-blue-300" iconBackground="bg-blue-400"
gradient="slate" gradient="slate"
className={!visibleMetrics.orders ? 'opacity-50' : ''} className={!visibleMetrics.orders ? 'opacity-50' : ''}
onClick={() => toggleMetric('orders')} onClick={() => toggleMetric('orders')}
@@ -233,7 +216,7 @@ const MiniSalesChart = ({ className = "" }) => {
<Card className="bg-gradient-to-br from-slate-700 to-slate-600 backdrop-blur-sm"> <Card className="bg-gradient-to-br from-slate-700 to-slate-600 backdrop-blur-sm">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="h-[216px]"> <div className="h-[216px]">
{loading ? ( {loading && !data?.length ? (
<ChartSkeleton height="sm" withCard={false} /> <ChartSkeleton height="sm" withCard={false} />
) : ( ) : (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">

View File

@@ -30,7 +30,6 @@ import {
ShippingDetails, ShippingDetails,
DetailDialog, DetailDialog,
formatCurrency, formatCurrency,
formatPercentage,
} from "./StatCards"; } from "./StatCards";
import { import {
DashboardStatCardMini, DashboardStatCardMini,
@@ -112,8 +111,14 @@ const MiniStatCards = ({
const calculateOrderTrend = useCallback(() => { const calculateOrderTrend = useCallback(() => {
if (!stats?.prevPeriodOrders) return null; if (!stats?.prevPeriodOrders) return null;
return calculateTrend(stats.orderCount, stats.prevPeriodOrders);
}, [stats, calculateTrend]); // If period is incomplete, use projected orders for fair comparison
const currentOrders = stats.periodProgress < 100
? (projection?.projectedOrders || Math.round(stats.orderCount / (stats.periodProgress / 100)))
: stats.orderCount;
return calculateTrend(currentOrders, stats.prevPeriodOrders);
}, [stats, projection, calculateTrend]);
const calculateAOVTrend = useCallback(() => { const calculateAOVTrend = useCallback(() => {
if (!stats?.prevPeriodAOV) return null; if (!stats?.prevPeriodAOV) return null;
@@ -284,18 +289,18 @@ const MiniStatCards = ({
<DashboardStatCardMini <DashboardStatCardMini
title="Today's Revenue" title="Today's Revenue"
value={formatCurrency(stats?.revenue || 0)} value={formatCurrency(stats?.revenue || 0)}
description={ subtitle={
stats?.periodProgress < 100 stats?.periodProgress < 100
? `Proj: ${formatCurrency(projection?.projectedRevenue || stats.projectedRevenue)}` ? `Proj: ${formatCurrency(projection?.projectedRevenue || stats.projectedRevenue)}`
: undefined : undefined
} }
trend={ trend={
revenueTrend?.trend && !projectionLoading revenueTrend?.trend && !projectionLoading
? { direction: revenueTrend.trend, value: formatPercentage(revenueTrend.value) } ? { value: revenueTrend.trend === "up" ? revenueTrend.value : -revenueTrend.value }
: undefined : undefined
} }
icon={DollarSign} icon={DollarSign}
iconBackground="bg-emerald-300" iconBackground="bg-emerald-400"
gradient="emerald" gradient="emerald"
className="h-[150px]" className="h-[150px]"
onClick={() => setSelectedMetric("revenue")} onClick={() => setSelectedMetric("revenue")}
@@ -304,14 +309,16 @@ const MiniStatCards = ({
<DashboardStatCardMini <DashboardStatCardMini
title="Today's Orders" title="Today's Orders"
value={stats?.orderCount} value={stats?.orderCount}
description={`${stats?.itemCount} total items`} subtitle={`${stats?.itemCount} total items`}
trend={ trend={
orderTrend?.trend projectionLoading && stats?.periodProgress < 100
? { direction: orderTrend.trend, value: formatPercentage(orderTrend.value) } ? undefined
: undefined : orderTrend?.trend
? { value: orderTrend.trend === "up" ? orderTrend.value : -orderTrend.value }
: undefined
} }
icon={ShoppingCart} icon={ShoppingCart}
iconBackground="bg-blue-300" iconBackground="bg-blue-400"
gradient="blue" gradient="blue"
className="h-[150px]" className="h-[150px]"
onClick={() => setSelectedMetric("orders")} onClick={() => setSelectedMetric("orders")}
@@ -321,14 +328,14 @@ const MiniStatCards = ({
title="Today's AOV" title="Today's AOV"
value={stats?.averageOrderValue?.toFixed(2)} value={stats?.averageOrderValue?.toFixed(2)}
valuePrefix="$" valuePrefix="$"
description={`${stats?.averageItemsPerOrder?.toFixed(1)} items per order`} subtitle={`${stats?.averageItemsPerOrder?.toFixed(1)} items/order`}
trend={ trend={
aovTrend?.trend aovTrend?.trend
? { direction: aovTrend.trend, value: formatPercentage(aovTrend.value) } ? { value: aovTrend.trend === "up" ? aovTrend.value : -aovTrend.value }
: undefined : undefined
} }
icon={CircleDollarSign} icon={CircleDollarSign}
iconBackground="bg-violet-300" iconBackground="bg-violet-400"
gradient="violet" gradient="violet"
className="h-[150px]" className="h-[150px]"
onClick={() => setSelectedMetric("average_order")} onClick={() => setSelectedMetric("average_order")}
@@ -337,9 +344,9 @@ const MiniStatCards = ({
<DashboardStatCardMini <DashboardStatCardMini
title="Shipped Today" title="Shipped Today"
value={stats?.shipping?.shippedCount || 0} value={stats?.shipping?.shippedCount || 0}
description={`${stats?.shipping?.locations?.total || 0} locations`} subtitle={`${stats?.shipping?.locations?.total || 0} locations`}
icon={Package} icon={Package}
iconBackground="bg-orange-300" iconBackground="bg-orange-400"
gradient="orange" gradient="orange"
className="h-[150px]" className="h-[150px]"
onClick={() => setSelectedMetric("shipping")} onClick={() => setSelectedMetric("shipping")}

View File

@@ -237,8 +237,7 @@ const OrdersDetails = ({ data }) => {
dataKey="orders" dataKey="orders"
name="Orders" name="Orders"
type="bar" type="bar"
color=" color="hsl(221.2 83.2% 53.3%)"
"
/> />
</div> </div>
)} )}
@@ -376,7 +375,7 @@ const BrandsCategoriesDetails = ({ data }) => {
</TableHeader> </TableHeader>
<TableBody className="max-h-[40vh] md:max-h-[60vh] overflow-auto"> <TableBody className="max-h-[40vh] md:max-h-[60vh] overflow-auto">
{brandsList.map((brand) => ( {brandsList.map((brand) => (
<TableRow key={brand.name}> <TableRow key={brand.id || brand.name}>
<TableCell className="font-medium">{brand.name}</TableCell> <TableCell className="font-medium">{brand.name}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{brand.count?.toLocaleString()} {brand.count?.toLocaleString()}
@@ -407,7 +406,7 @@ const BrandsCategoriesDetails = ({ data }) => {
</TableHeader> </TableHeader>
<TableBody className="max-h-[40vh] md:max-h-[60vh] overflow-auto"> <TableBody className="max-h-[40vh] md:max-h-[60vh] overflow-auto">
{categoriesList.map((category) => ( {categoriesList.map((category) => (
<TableRow key={category.name}> <TableRow key={category.id || category.name}>
<TableCell className="font-medium">{category.name}</TableCell> <TableCell className="font-medium">{category.name}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{category.count?.toLocaleString()} {category.count?.toLocaleString()}
@@ -563,9 +562,9 @@ const OrderTypeDetails = ({ data, type }) => {
); );
const timeSeriesData = data.map((day) => ({ const timeSeriesData = data.map((day) => ({
timestamp: day.timestamp, timestamp: day.timestamp || day.date,
count: day.count, count: day.count ?? day.orders, // Backend returns 'orders'
value: day.value, value: day.value ?? day.revenue, // Backend returns 'revenue'
percentage: day.percentage, percentage: day.percentage,
})); }));
@@ -623,10 +622,11 @@ const PeakHourDetails = ({ data }) => {
</div> </div>
); );
// hourlyOrders is now an array of {hour, count} objects in chronological order (rolling 24hrs)
const hourlyData = const hourlyData =
data[0]?.hourlyOrders?.map((count, hour) => ({ data[0]?.hourlyOrders?.map((item) => ({
timestamp: hour, // Use raw hour number for x-axis timestamp: item.hour, // The actual hour (0-23)
orders: count, orders: item.count,
})) || []; })) || [];
return ( return (
@@ -996,13 +996,11 @@ const StatCards = ({
const [lastUpdate, setLastUpdate] = useState(null); const [lastUpdate, setLastUpdate] = useState(null);
const [timeRange, setTimeRange] = useState(initialTimeRange); const [timeRange, setTimeRange] = useState(initialTimeRange);
const [selectedMetric, setSelectedMetric] = useState(null); const [selectedMetric, setSelectedMetric] = useState(null);
const [dateRange, setDateRange] = useState(null);
const [detailDataLoading, setDetailDataLoading] = useState({}); const [detailDataLoading, setDetailDataLoading] = useState({});
const [detailData, setDetailData] = useState({}); const [detailData, setDetailData] = useState({});
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [projection, setProjection] = useState(null); const [projection, setProjection] = useState(null);
const [projectionLoading, setProjectionLoading] = useState(false); const [projectionLoading, setProjectionLoading] = useState(false);
const { setCacheData, getCacheData, clearCache } = useDataCache(); const { setCacheData, getCacheData } = useDataCache();
// Function to determine if we should use last30days for trend charts // Function to determine if we should use last30days for trend charts
const shouldUseLast30Days = useCallback( const shouldUseLast30Days = useCallback(
@@ -1218,8 +1216,14 @@ const StatCards = ({
const calculateOrderTrend = useCallback(() => { const calculateOrderTrend = useCallback(() => {
if (!stats?.prevPeriodOrders) return null; if (!stats?.prevPeriodOrders) return null;
return calculateTrend(stats.orderCount, stats.prevPeriodOrders);
}, [stats, calculateTrend]); // If period is incomplete, use projected orders for fair comparison
const currentOrders = stats.periodProgress < 100
? (projection?.projectedOrders || Math.round(stats.orderCount / (stats.periodProgress / 100)))
: stats.orderCount;
return calculateTrend(currentOrders, stats.prevPeriodOrders);
}, [stats, projection, calculateTrend]);
const calculateAOVTrend = useCallback(() => { const calculateAOVTrend = useCallback(() => {
if (!stats?.prevPeriodAOV) return null; if (!stats?.prevPeriodAOV) return null;
@@ -1242,7 +1246,6 @@ const StatCards = ({
if (!isMounted) return; if (!isMounted) return;
setDateRange(response.timeRange);
setStats(response.stats); setStats(response.stats);
setLastUpdate(DateTime.now().setZone("America/New_York")); setLastUpdate(DateTime.now().setZone("America/New_York"));
setError(null); setError(null);
@@ -1257,7 +1260,6 @@ const StatCards = ({
} finally { } finally {
if (isMounted) { if (isMounted) {
setLoading(false); setLoading(false);
setIsInitialLoad(false);
} }
} }
}; };
@@ -1321,69 +1323,30 @@ const StatCards = ({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [timeRange]); }, [timeRange]);
// Modified AsyncDetailView component // Fetch detail data when a metric is selected (if not already cached)
const AsyncDetailView = memo(({ metric, type, orderCount }) => { useEffect(() => {
const detailTimeRange = shouldUseLast30Days(metric) if (!selectedMetric) return;
// Skip metrics that use stats directly instead of fetched detail data
if (["brands_categories", "shipping", "peak_hour"].includes(selectedMetric)) {
return;
}
const detailTimeRange = shouldUseLast30Days(selectedMetric)
? "last30days" ? "last30days"
: timeRange; : timeRange;
const cachedData = const cachedData = detailData[selectedMetric] || getCacheData(detailTimeRange, selectedMetric);
detailData[metric] || getCacheData(detailTimeRange, metric); const isLoading = detailDataLoading[selectedMetric];
const isLoading = detailDataLoading[metric];
const isOrderTypeMetric = [
"pre_orders",
"local_pickup",
"on_hold",
].includes(metric);
useEffect(() => { if (!cachedData && !isLoading) {
let isMounted = true; const isOrderTypeMetric = ["pre_orders", "local_pickup", "on_hold"].includes(selectedMetric);
fetchDetailData(selectedMetric, isOrderTypeMetric ? selectedMetric : undefined);
const loadData = async () => {
if (!cachedData && !isLoading) {
// Pass type only for order type metrics
const data = await fetchDetailData(
metric,
isOrderTypeMetric ? metric : undefined
);
if (!isMounted) return;
// The state updates are handled in fetchDetailData
}
};
loadData();
return () => {
isMounted = false;
};
}, [metric, timeRange, isOrderTypeMetric]); // Depend on isOrderTypeMetric
if (isLoading || (!cachedData && !error)) {
switch (metric) {
case "revenue":
case "orders":
case "average_order":
return <ChartSkeleton type="line" height="default" withCard={false} />;
case "refunds":
case "cancellations":
case "order_range":
case "pre_orders":
case "local_pickup":
case "on_hold":
return <ChartSkeleton type="bar" height="default" withCard={false} />;
case "brands_categories":
case "shipping":
return <TableSkeleton rows={8} columns={3} />;
case "peak_hour":
return <ChartSkeleton type="bar" height="default" withCard={false} />;
default:
return <div className="text-muted-foreground">Loading...</div>;
}
} }
}, [selectedMetric, timeRange, shouldUseLast30Days, detailData, detailDataLoading, getCacheData, fetchDetailData]);
if (!cachedData && error) { // Modified getDetailComponent to use memoized components
return <DashboardErrorState error={`Failed to load stats: ${error}`} />; const getDetailComponent = useCallback(() => {
} if (!selectedMetric || !stats) {
if (!cachedData) {
return ( return (
<DashboardEmptyState <DashboardEmptyState
title="No data available" title="No data available"
@@ -1393,57 +1356,22 @@ const StatCards = ({
); );
} }
switch (metric) {
case "revenue":
return <MemoizedRevenueDetails data={cachedData} />;
case "orders":
return <MemoizedOrdersDetails data={cachedData} />;
case "average_order":
return (
<MemoizedAverageOrderDetails
data={cachedData}
orderCount={orderCount}
/>
);
case "refunds":
return <MemoizedRefundDetails data={cachedData} />;
case "cancellations":
return <MemoizedCancellationsDetails data={cachedData} />;
case "order_range":
return <MemoizedOrderRangeDetails data={cachedData} />;
case "pre_orders":
case "local_pickup":
case "on_hold":
return <MemoizedOrderTypeDetails data={cachedData} type={type} />;
default:
return (
<div className="text-muted-foreground">Invalid metric selected.</div>
);
}
});
AsyncDetailView.displayName = "AsyncDetailView";
// Modified getDetailComponent to use memoized components
const getDetailComponent = useCallback(() => {
if (!selectedMetric || !stats) {
return (
<div className="text-muted-foreground">
No data available for the selected time range.
</div>
);
}
const data = detailData[selectedMetric]; const data = detailData[selectedMetric];
const isLoading = detailDataLoading[selectedMetric]; const isLoading = detailDataLoading[selectedMetric];
const isOrderTypeMetric = [
"pre_orders",
"local_pickup",
"on_hold",
].includes(selectedMetric);
if (isLoading) { if (isLoading) {
return <ChartSkeleton height="default" withCard={false} />; // Show metric-specific loading skeletons
switch (selectedMetric) {
case "brands_categories":
case "shipping":
return <TableSkeleton rows={8} columns={3} />;
case "revenue":
case "orders":
case "average_order":
return <ChartSkeleton type="line" height="default" withCard={false} />;
default:
return <ChartSkeleton type="bar" height="default" withCard={false} />;
}
} }
switch (selectedMetric) { switch (selectedMetric) {
@@ -1659,7 +1587,7 @@ const StatCards = ({
projectionLoading && stats?.periodProgress < 100 projectionLoading && stats?.periodProgress < 100
? undefined ? undefined
: revenueTrend?.value : revenueTrend?.value
? { value: revenueTrend.value, moreIsBetter: revenueTrend.trend === "up" } ? { value: revenueTrend.trend === "up" ? revenueTrend.value : -revenueTrend.value, moreIsBetter: true }
: undefined : undefined
} }
icon={DollarSign} icon={DollarSign}
@@ -1672,7 +1600,13 @@ const StatCards = ({
title="Orders" title="Orders"
value={stats?.orderCount} value={stats?.orderCount}
subtitle={`${stats?.itemCount} total items`} subtitle={`${stats?.itemCount} total items`}
trend={orderTrend?.value ? { value: orderTrend.value, moreIsBetter: orderTrend.trend === "up" } : undefined} trend={
projectionLoading && stats?.periodProgress < 100
? undefined
: orderTrend?.value
? { value: orderTrend.trend === "up" ? orderTrend.value : -orderTrend.value, moreIsBetter: true }
: undefined
}
icon={ShoppingCart} icon={ShoppingCart}
iconColor="blue" iconColor="blue"
onClick={() => setSelectedMetric("orders")} onClick={() => setSelectedMetric("orders")}
@@ -1684,7 +1618,7 @@ const StatCards = ({
value={stats?.averageOrderValue?.toFixed(2)} value={stats?.averageOrderValue?.toFixed(2)}
valuePrefix="$" valuePrefix="$"
subtitle={`${stats?.averageItemsPerOrder?.toFixed(1)} items per order`} subtitle={`${stats?.averageItemsPerOrder?.toFixed(1)} items per order`}
trend={aovTrend?.value ? { value: aovTrend.value, moreIsBetter: aovTrend.trend === "up" } : undefined} trend={aovTrend?.value ? { value: aovTrend.trend === "up" ? aovTrend.value : -aovTrend.value, moreIsBetter: true } : undefined}
icon={CircleDollarSign} icon={CircleDollarSign}
iconColor="purple" iconColor="purple"
onClick={() => setSelectedMetric("average_order")} onClick={() => setSelectedMetric("average_order")}
@@ -1714,7 +1648,9 @@ const StatCards = ({
<DashboardStatCard <DashboardStatCard
title="Pre-Orders" title="Pre-Orders"
value={ value={
((stats?.orderTypes?.preOrders?.count / stats?.orderCount) * 100)?.toFixed(1) || "0" stats?.orderCount > 0
? ((stats?.orderTypes?.preOrders?.count / stats?.orderCount) * 100).toFixed(1)
: "0"
} }
valueSuffix="%" valueSuffix="%"
subtitle={`${stats?.orderTypes?.preOrders?.count || 0} orders`} subtitle={`${stats?.orderTypes?.preOrders?.count || 0} orders`}
@@ -1727,7 +1663,9 @@ const StatCards = ({
<DashboardStatCard <DashboardStatCard
title="Local Pickup" title="Local Pickup"
value={ value={
((stats?.orderTypes?.localPickup?.count / stats?.orderCount) * 100)?.toFixed(1) || "0" stats?.orderCount > 0
? ((stats?.orderTypes?.localPickup?.count / stats?.orderCount) * 100).toFixed(1)
: "0"
} }
valueSuffix="%" valueSuffix="%"
subtitle={`${stats?.orderTypes?.localPickup?.count || 0} orders`} subtitle={`${stats?.orderTypes?.localPickup?.count || 0} orders`}
@@ -1740,7 +1678,9 @@ const StatCards = ({
<DashboardStatCard <DashboardStatCard
title="On Hold" title="On Hold"
value={ value={
((stats?.orderTypes?.heldItems?.count / stats?.orderCount) * 100)?.toFixed(1) || "0" stats?.orderCount > 0
? ((stats?.orderTypes?.heldItems?.count / stats?.orderCount) * 100).toFixed(1)
: "0"
} }
valueSuffix="%" valueSuffix="%"
subtitle={`${stats?.orderTypes?.heldItems?.count || 0} orders`} subtitle={`${stats?.orderTypes?.heldItems?.count || 0} orders`}

View File

@@ -10,12 +10,18 @@
* value="$12,345" * value="$12,345"
* gradient="emerald" * gradient="emerald"
* icon={DollarSign} * icon={DollarSign}
* trend={{ value: 12.5, label: "vs last month" }}
* /> * />
*/ */
import React from "react"; import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TrendingUp, TrendingDown, type LucideIcon } from "lucide-react"; import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ArrowUp, ArrowDown, Minus, Info, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// ============================================================================= // =============================================================================
@@ -35,6 +41,17 @@ export type GradientVariant =
| "sky" | "sky"
| "custom"; | "custom";
export interface TrendProps {
/** The percentage or absolute change value */
value: number;
/** Optional label to show after the trend (e.g., "vs last month") */
label?: string;
/** Whether a higher value is better (affects color). Defaults to true. */
moreIsBetter?: boolean;
/** Suffix for the trend value (defaults to "%"). Use "" for no suffix. */
suffix?: string;
}
export interface DashboardStatCardMiniProps { export interface DashboardStatCardMiniProps {
/** Card title/label */ /** Card title/label */
title: string; title: string;
@@ -44,13 +61,10 @@ export interface DashboardStatCardMiniProps {
valuePrefix?: string; valuePrefix?: string;
/** Optional suffix for the value (e.g., "%") */ /** Optional suffix for the value (e.g., "%") */
valueSuffix?: string; valueSuffix?: string;
/** Optional description text or element */ /** Optional subtitle or description (can be string or JSX) */
description?: React.ReactNode; subtitle?: React.ReactNode;
/** Trend direction and value */ /** Optional trend indicator */
trend?: { trend?: TrendProps;
direction: "up" | "down";
value: string;
};
/** Optional icon component */ /** Optional icon component */
icon?: LucideIcon; icon?: LucideIcon;
/** Icon background color class (e.g., "bg-emerald-500/20") */ /** Icon background color class (e.g., "bg-emerald-500/20") */
@@ -61,6 +75,12 @@ export interface DashboardStatCardMiniProps {
className?: string; className?: string;
/** Click handler */ /** Click handler */
onClick?: () => void; onClick?: () => void;
/** Loading state */
loading?: boolean;
/** Tooltip text shown via info icon next to title */
tooltip?: string;
/** Additional content to render below the main value */
children?: React.ReactNode;
} }
// ============================================================================= // =============================================================================
@@ -81,6 +101,53 @@ const GRADIENT_PRESETS: Record<GradientVariant, string> = {
custom: "", custom: "",
}; };
// =============================================================================
// HELPER COMPONENTS
// =============================================================================
/**
* Get trend colors optimized for dark gradient backgrounds
*/
const getTrendColors = (value: number, moreIsBetter: boolean = true): string => {
const isPositive = value > 0;
const isGood = moreIsBetter ? isPositive : !isPositive;
if (value === 0) {
return "text-gray-400";
}
return isGood ? "text-emerald-400" : "text-rose-400";
};
interface TrendIndicatorProps {
value: number;
label?: string;
moreIsBetter?: boolean;
suffix?: string;
}
const TrendIndicator: React.FC<TrendIndicatorProps> = ({
value,
label,
moreIsBetter = true,
suffix = "%",
}) => {
const colorClass = getTrendColors(value, moreIsBetter);
const IconComponent = value > 0 ? ArrowUp : value < 0 ? ArrowDown : Minus;
// Format the value - round to integer for compact display (preserves sign for negatives)
const formattedValue = Math.round(value);
return (
<span className={cn("flex items-center gap-0.5 text-sm font-semibold", colorClass)}>
<IconComponent className="h-4 w-4" />
{value > 0 ? "+" : ""}
{formattedValue}
{suffix}
{label && <span className="text-gray-300 font-normal ml-1">{label}</span>}
</span>
);
};
// ============================================================================= // =============================================================================
// MAIN COMPONENT // MAIN COMPONENT
// ============================================================================= // =============================================================================
@@ -90,16 +157,41 @@ export const DashboardStatCardMini: React.FC<DashboardStatCardMiniProps> = ({
value, value,
valuePrefix, valuePrefix,
valueSuffix, valueSuffix,
description, subtitle,
trend, trend,
icon: Icon, icon: Icon,
iconBackground, iconBackground,
gradient = "slate", gradient = "slate",
className, className,
onClick, onClick,
loading = false,
tooltip,
children,
}) => { }) => {
const gradientClass = gradient === "custom" ? "" : GRADIENT_PRESETS[gradient]; const gradientClass = gradient === "custom" ? "" : GRADIENT_PRESETS[gradient];
// Loading state
if (loading) {
return (
<Card
className={cn(
gradientClass,
"backdrop-blur-md border-white/10",
className
)}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-3">
<div className="h-4 w-20 bg-white/20 animate-pulse rounded" />
{Icon && <div className="h-9 w-9 bg-white/20 animate-pulse rounded-full" />}
</CardHeader>
<CardContent className="p-4 pt-1">
<div className="h-9 w-28 bg-white/20 animate-pulse rounded mb-2" />
{subtitle && <div className="h-4 w-24 bg-white/10 animate-pulse rounded" />}
</CardContent>
</Card>
);
}
return ( return (
<Card <Card
className={cn( className={cn(
@@ -110,10 +202,28 @@ export const DashboardStatCardMini: React.FC<DashboardStatCardMiniProps> = ({
)} )}
onClick={onClick} onClick={onClick}
> >
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-3">
<CardTitle className="text-sm font-bold text-gray-100"> <div className="flex items-center gap-1.5">
{title} <CardTitle className="text-xs font-medium text-gray-100 uppercase tracking-wide">
</CardTitle> {title}
</CardTitle>
{tooltip && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="text-gray-400 hover:text-gray-200 transition-colors"
onClick={(e) => e.stopPropagation()}
>
<Info className="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p className="text-sm">{tooltip}</p>
</TooltipContent>
</Tooltip>
)}
</div>
{Icon && ( {Icon && (
<div className="relative p-2"> <div className="relative p-2">
{iconBackground && ( {iconBackground && (
@@ -121,11 +231,11 @@ export const DashboardStatCardMini: React.FC<DashboardStatCardMiniProps> = ({
className={cn("absolute inset-0 rounded-full", iconBackground)} className={cn("absolute inset-0 rounded-full", iconBackground)}
/> />
)} )}
<Icon className="h-5 w-5 text-white relative" /> <Icon className="h-4 w-4 text-white relative" />
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent className="p-4 pt-0"> <CardContent className="p-4 pt-1">
<div className="text-3xl font-extrabold text-white"> <div className="text-3xl font-extrabold text-white">
{valuePrefix} {valuePrefix}
{typeof value === "number" ? value.toLocaleString() : value} {typeof value === "number" ? value.toLocaleString() : value}
@@ -133,32 +243,24 @@ export const DashboardStatCardMini: React.FC<DashboardStatCardMiniProps> = ({
<span className="text-xl text-gray-300">{valueSuffix}</span> <span className="text-xl text-gray-300">{valueSuffix}</span>
)} )}
</div> </div>
{(description || trend) && ( {(subtitle || trend) && (
<div className="flex items-center gap-2 mt-1"> <div className="flex flex-wrap items-center justify-between gap-2 mt-3">
{trend && ( {subtitle && (
<span <span className="text-sm font-semibold text-gray-200">
className={cn( {subtitle}
"flex items-center gap-1 text-sm font-semibold",
trend.direction === "up"
? "text-emerald-300"
: "text-rose-300"
)}
>
{trend.direction === "up" ? (
<TrendingUp className="h-4 w-4" />
) : (
<TrendingDown className="h-4 w-4" />
)}
{trend.value}
</span> </span>
)} )}
{description && ( {trend && (
<span className="text-sm font-semibold text-gray-200"> <TrendIndicator
{description} value={trend.value}
</span> label={trend.label}
moreIsBetter={trend.moreIsBetter}
suffix={trend.suffix}
/>
)} )}
</div> </div>
)} )}
{children}
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -170,12 +272,14 @@ export const DashboardStatCardMini: React.FC<DashboardStatCardMiniProps> = ({
export interface DashboardStatCardMiniSkeletonProps { export interface DashboardStatCardMiniSkeletonProps {
gradient?: GradientVariant; gradient?: GradientVariant;
hasIcon?: boolean;
hasSubtitle?: boolean;
className?: string; className?: string;
} }
export const DashboardStatCardMiniSkeleton: React.FC< export const DashboardStatCardMiniSkeleton: React.FC<
DashboardStatCardMiniSkeletonProps DashboardStatCardMiniSkeletonProps
> = ({ gradient = "slate", className }) => { > = ({ gradient = "slate", hasIcon = true, hasSubtitle = true, className }) => {
const gradientClass = gradient === "custom" ? "" : GRADIENT_PRESETS[gradient]; const gradientClass = gradient === "custom" ? "" : GRADIENT_PRESETS[gradient];
return ( return (
@@ -186,13 +290,13 @@ export const DashboardStatCardMiniSkeleton: React.FC<
className className
)} )}
> >
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-3">
<div className="h-4 w-20 bg-white/20 animate-pulse rounded" /> <div className="h-4 w-20 bg-white/20 animate-pulse rounded" />
<div className="h-9 w-9 bg-white/20 animate-pulse rounded-full" /> {hasIcon && <div className="h-9 w-9 bg-white/20 animate-pulse rounded-full" />}
</CardHeader> </CardHeader>
<CardContent className="p-4 pt-0"> <CardContent className="p-4 pt-1">
<div className="h-9 w-28 bg-white/20 animate-pulse rounded mb-2" /> <div className="h-9 w-28 bg-white/20 animate-pulse rounded mb-2" />
<div className="h-4 w-24 bg-white/10 animate-pulse rounded" /> {hasSubtitle && <div className="h-4 w-24 bg-white/10 animate-pulse rounded" />}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@@ -1,7 +0,0 @@
#!/bin/zsh
#Clear previous mount in case its still there
umount '/Users/matt/Dev/inventory/inventory-server'
#Mount
sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 '/Users/matt/Dev/inventory/inventory-server/'