-
diff --git a/dashboard/src/components/dashboard/MiniStatCards.jsx b/dashboard/src/components/dashboard/MiniStatCards.jsx
new file mode 100644
index 0000000..6cd8cd7
--- /dev/null
+++ b/dashboard/src/components/dashboard/MiniStatCards.jsx
@@ -0,0 +1,352 @@
+import React, { useState, useEffect, useCallback, memo } from "react";
+import axios from "axios";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { DateTime } from "luxon";
+import { TIME_RANGES } from "@/lib/constants";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import {
+ DollarSign,
+ ShoppingCart,
+ Package,
+ AlertCircle,
+ CircleDollarSign,
+} from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
+
+// Import the detail view components and utilities from StatCards
+import {
+ RevenueDetails,
+ OrdersDetails,
+ AverageOrderDetails,
+ ShippingDetails,
+ StatCard,
+ DetailDialog,
+ formatCurrency,
+ formatPercentage,
+ SkeletonCard,
+} from "./StatCards";
+
+const MiniStatCards = ({
+ timeRange: initialTimeRange = "today",
+ startDate,
+ endDate,
+ title = "Quick Stats",
+ description = "",
+ compact = false
+}) => {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [lastUpdate, setLastUpdate] = useState(null);
+ const [timeRange, setTimeRange] = useState(initialTimeRange);
+ const [selectedMetric, setSelectedMetric] = useState(null);
+ const [detailDataLoading, setDetailDataLoading] = useState({});
+ const [detailData, setDetailData] = useState({});
+ const [projection, setProjection] = useState(null);
+ const [projectionLoading, setProjectionLoading] = useState(false);
+
+ // Reuse the trend calculation functions
+ const calculateTrend = useCallback((current, previous) => {
+ if (!current || !previous) return null;
+ const trend = current >= previous ? "up" : "down";
+ const diff = Math.abs(current - previous);
+ const percentage = (diff / previous) * 100;
+
+ return {
+ trend,
+ value: percentage,
+ current,
+ previous,
+ };
+ }, []);
+
+ const calculateRevenueTrend = useCallback(() => {
+ if (!stats?.prevPeriodRevenue && stats?.prevPeriodRevenue !== 0) return null;
+ const currentRevenue = stats.periodProgress < 100 ? stats.projectedRevenue : stats.revenue;
+ const prevRevenue = stats.prevPeriodRevenue;
+
+ if (!currentRevenue || !prevRevenue) return null;
+
+ const trend = currentRevenue >= prevRevenue ? "up" : "down";
+ const diff = Math.abs(currentRevenue - prevRevenue);
+ const percentage = (diff / prevRevenue) * 100;
+
+ return {
+ trend,
+ value: percentage,
+ current: currentRevenue,
+ previous: prevRevenue,
+ };
+ }, [stats]);
+
+ const calculateOrderTrend = useCallback(() => {
+ if (!stats?.prevPeriodOrders) return null;
+ return calculateTrend(stats.orderCount, stats.prevPeriodOrders);
+ }, [stats, calculateTrend]);
+
+ const calculateAOVTrend = useCallback(() => {
+ if (!stats?.prevPeriodAOV) return null;
+ return calculateTrend(stats.averageOrderValue, stats.prevPeriodAOV);
+ }, [stats, calculateTrend]);
+
+ // Initial load effect
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ setStats(null);
+
+ const params = timeRange === "custom" ? { startDate, endDate } : { timeRange };
+ const response = await axios.get("/api/klaviyo/events/stats", { params });
+
+ if (!isMounted) return;
+
+ setStats(response.data.stats);
+ setLastUpdate(DateTime.now().setZone("America/New_York"));
+ setError(null);
+ } catch (error) {
+ console.error("Error loading data:", error);
+ if (isMounted) {
+ setError(error.message);
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ loadData();
+ return () => {
+ isMounted = false;
+ };
+ }, [timeRange, startDate, endDate]);
+
+ // Load smart projection separately
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadProjection = async () => {
+ if (!stats?.periodProgress || stats.periodProgress >= 100) return;
+
+ try {
+ setProjectionLoading(true);
+ const params = timeRange === "custom" ? { startDate, endDate } : { timeRange };
+ const response = await axios.get("/api/klaviyo/events/projection", { params });
+
+ if (!isMounted) return;
+ setProjection(response.data);
+ } catch (error) {
+ console.error("Error loading projection:", error);
+ } finally {
+ if (isMounted) {
+ setProjectionLoading(false);
+ }
+ }
+ };
+
+ loadProjection();
+ return () => {
+ isMounted = false;
+ };
+ }, [timeRange, startDate, endDate, stats?.periodProgress]);
+
+ // Auto-refresh for 'today' view
+ useEffect(() => {
+ if (timeRange !== "today") return;
+
+ const interval = setInterval(async () => {
+ try {
+ const [statsResponse, projectionResponse] = await Promise.all([
+ axios.get("/api/klaviyo/events/stats", { params: { timeRange: "today" } }),
+ axios.get("/api/klaviyo/events/projection", { params: { timeRange: "today" } }),
+ ]);
+
+ setStats(statsResponse.data.stats);
+ setProjection(projectionResponse.data);
+ setLastUpdate(DateTime.now().setZone("America/New_York"));
+ } catch (error) {
+ console.error("Error auto-refreshing stats:", error);
+ }
+ }, 60000);
+
+ return () => clearInterval(interval);
+ }, [timeRange]);
+
+ if (loading && !stats) {
+ return (
+
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ Error
+ Failed to load stats: {error}
+
+ );
+ }
+
+ if (!stats) return null;
+
+ const revenueTrend = calculateRevenueTrend();
+ const orderTrend = calculateOrderTrend();
+ const aovTrend = calculateAOVTrend();
+
+ return (
+
+
+
+
+
+ {lastUpdate && !loading && (
+
+ Last updated {lastUpdate.toFormat("h:mm a")}
+ {projection?.confidence > 0 && !projectionLoading && (
+
+
+
+
+ ({Math.round(projection.confidence * 100)}%)
+
+
+
+ Confidence level of revenue projection
+
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ Proj:
+ {projectionLoading ? (
+
+ ) : (
+ formatCurrency(projection?.projectedRevenue || stats.projectedRevenue)
+ )}
+
+ ) : null
+ }
+ progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined}
+ trend={projectionLoading && stats?.periodProgress < 100 ? undefined : revenueTrend?.trend}
+ trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null}
+ colorClass="text-green-600 dark:text-green-400"
+ icon={DollarSign}
+ iconColor="text-green-500"
+ onDetailsClick={() => setSelectedMetric("revenue")}
+ isLoading={loading || !stats}
+ />
+
+ setSelectedMetric("orders")}
+ isLoading={loading || !stats}
+ />
+
+ setSelectedMetric("average_order")}
+ isLoading={loading || !stats}
+ />
+
+ setSelectedMetric("shipping")}
+ isLoading={loading || !stats}
+ />
+
+
+
+ );
+};
+
+export default MiniStatCards;
\ No newline at end of file
diff --git a/dashboard/src/components/dashboard/StatCards.jsx b/dashboard/src/components/dashboard/StatCards.jsx
index 900edab..8a07091 100644
--- a/dashboard/src/components/dashboard/StatCards.jsx
+++ b/dashboard/src/components/dashboard/StatCards.jsx
@@ -2138,4 +2138,17 @@ const StatCards = ({
);
};
+// Export components and utilities for MiniStatCards
+export {
+ RevenueDetails,
+ OrdersDetails,
+ AverageOrderDetails,
+ ShippingDetails,
+ StatCard,
+ DetailDialog,
+ formatCurrency,
+ formatPercentage,
+ SkeletonCard,
+};
+
export default StatCards;