-
+
+ {/* Mini Stat Cards */}
+
-
-
-
-
+
+ {/* Mini Charts Grid */}
+
+ {/* Mini Sales Chart */}
+
+
+ {/* Mini Realtime Analytics */}
+
diff --git a/dashboard/src/components/dashboard/MiniSalesChart.jsx b/dashboard/src/components/dashboard/MiniSalesChart.jsx
new file mode 100644
index 0000000..0f5853a
--- /dev/null
+++ b/dashboard/src/components/dashboard/MiniSalesChart.jsx
@@ -0,0 +1,245 @@
+import React, { useState, useEffect, useCallback, memo } from "react";
+import axios from "axios";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from "recharts";
+import { DateTime } from "luxon";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { AlertCircle, TrendingUp, DollarSign, ShoppingCart } from "lucide-react";
+import { formatCurrency, CustomTooltip, processData, StatCard } from "./SalesChart.jsx";
+
+const SkeletonChart = () => (
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+
+
+
+);
+
+const MiniStatCard = memo(({ title, value, icon: Icon, colorClass, iconColor, iconBackground, background }) => (
+
+
+
+ {title}
+
+ {Icon && (
+
+ )}
+
+
+
+ {value}
+
+
+
+));
+
+MiniStatCard.displayName = "MiniStatCard";
+
+const MiniSalesChart = ({ className = "" }) => {
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [summaryStats, setSummaryStats] = useState({
+ totalRevenue: 0,
+ totalOrders: 0,
+ });
+
+ const fetchData = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await axios.get("/api/klaviyo/events/stats/details", {
+ params: {
+ timeRange: "last30days",
+ metric: "revenue",
+ daily: true,
+ },
+ });
+
+ if (!response.data) {
+ throw new Error("Invalid response format");
+ }
+
+ const stats = Array.isArray(response.data)
+ ? response.data
+ : response.data.stats || [];
+
+ const processedData = processData(stats);
+
+ // Calculate totals
+ const totals = stats.reduce((acc, day) => ({
+ totalRevenue: acc.totalRevenue + (Number(day.revenue) || 0),
+ totalOrders: acc.totalOrders + (Number(day.orders) || 0),
+ }), { totalRevenue: 0, totalOrders: 0 });
+
+ setData(processedData);
+ setSummaryStats(totals);
+ setError(null);
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ setError(error.message);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchData();
+ const intervalId = setInterval(fetchData, 300000);
+ return () => clearInterval(intervalId);
+ }, [fetchData]);
+
+ const formatXAxis = (value) => {
+ if (!value) return "";
+ const date = new Date(value);
+ return date.toLocaleDateString([], {
+ month: "short",
+ day: "numeric"
+ });
+ };
+
+ if (error) {
+ return (
+
+
+ Error
+ Failed to load sales data: {error}
+
+ );
+ }
+
+ return (
+
+ {/* Stat Cards */}
+
+
+
+
+
+ {/* Chart */}
+ {loading ? (
+
+ ) : !data.length ? (
+
+
+
+
No sales data available
+
+
+ ) : (
+
+
+
+
+
+ formatCurrency(value, false)}
+ className="text-[10px] text-gray-300"
+ tick={{ fill: "currentColor" }}
+ />
+ value.toLocaleString()}
+ className="text-[10px] text-gray-300"
+ tick={{ fill: "currentColor" }}
+ />
+ } />
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default MiniSalesChart;
\ No newline at end of file
diff --git a/dashboard/src/components/dashboard/SalesChart.jsx b/dashboard/src/components/dashboard/SalesChart.jsx
index eedbeb5..98bbedb 100644
--- a/dashboard/src/components/dashboard/SalesChart.jsx
+++ b/dashboard/src/components/dashboard/SalesChart.jsx
@@ -109,16 +109,15 @@ const calculatePreviousPeriodDates = (timeRange, startDate, endDate) => {
return null;
};
-// Enhanced helper function for consistent currency formatting with explicit rounding
-const formatCurrency = (value, useFractionDigits = true) => {
- if (typeof value !== "number") return "$0.00";
- const roundedValue = parseFloat(value.toFixed(useFractionDigits ? 2 : 0));
+// Move formatCurrency to top and export it
+export const formatCurrency = (value, minimumFractionDigits = 0) => {
+ if (!value || isNaN(value)) return "$0";
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
- minimumFractionDigits: useFractionDigits ? 2 : 0,
- maximumFractionDigits: useFractionDigits ? 2 : 0,
- }).format(roundedValue);
+ minimumFractionDigits,
+ maximumFractionDigits: minimumFractionDigits,
+ }).format(value);
};
// Add a helper function for percentage formatting
@@ -139,7 +138,7 @@ const METRIC_COLORS = {
};
// Memoize the StatCard component
-const StatCard = memo(
+export const StatCard = memo(
({
title,
value,
@@ -189,99 +188,34 @@ const StatCard = memo(
)
);
-// Add display name for debugging
StatCard.displayName = "StatCard";
-const CustomTooltip = ({ active, payload, label }) => {
+// Export CustomTooltip
+export const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const date = new Date(label);
const formattedDate = date.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
- year: "numeric",
});
- // Group metrics by type (current vs previous)
- const currentMetrics = payload.filter(
- (p) => !p.dataKey.toLowerCase().includes("prev")
- );
- const previousMetrics = payload.filter((p) =>
- p.dataKey.toLowerCase().includes("prev")
- );
-
return (
-
-
-
- {formattedDate}
-
+
+
+ {formattedDate}
+ {payload.map((entry, index) => {
+ const value = entry.dataKey.toLowerCase().includes('revenue') || entry.dataKey === 'avgOrderValue'
+ ? formatCurrency(entry.value)
+ : entry.value.toLocaleString();
-
- {currentMetrics.map((entry, index) => {
- const value =
- entry.dataKey.toLowerCase().includes("revenue") ||
- entry.dataKey === "avgOrderValue" ||
- entry.dataKey === "movingAverage" ||
- entry.dataKey === "aovMovingAverage"
- ? formatCurrency(entry.value)
- : entry.value.toLocaleString();
-
- return (
-
-
- {entry.name}:
-
- {value}
-
- );
- })}
-
-
- {previousMetrics.length > 0 && (
- <>
-
-
-
- Previous Period
-
- {previousMetrics.map((entry, index) => {
- const value =
- entry.dataKey.toLowerCase().includes("revenue") ||
- entry.dataKey.includes("avgOrderValue")
- ? formatCurrency(entry.value)
- : entry.value.toLocaleString();
-
- return (
-
-
- {entry.name.replace("Previous ", "")}:
-
- {value}
-
- );
- })}
+ return (
+
+ {entry.name}:
+ {value}
- >
- )}
+ );
+ })}
);
@@ -332,7 +266,8 @@ const calculate7DayAverage = (data) => {
});
};
-const processData = (stats = []) => {
+// Export processData
+export const processData = (stats = []) => {
if (!Array.isArray(stats)) return [];
// First, convert the stats array into the base format