diff --git a/inventory/package-lock.json b/inventory/package-lock.json index 599ff16..0672665 100644 --- a/inventory/package-lock.json +++ b/inventory/package-lock.json @@ -52,6 +52,7 @@ "date-fns": "^3.6.0", "diff": "^7.0.0", "framer-motion": "^12.4.4", + "immer": "^11.1.3", "input-otp": "^1.4.1", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21", @@ -5749,6 +5750,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", diff --git a/inventory/package.json b/inventory/package.json index 35db14b..65a6871 100644 --- a/inventory/package.json +++ b/inventory/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:deploy": "tsc -b && COPY_BUILD=true vite build", "lint": "eslint .", "preview": "vite preview", "mount": "../mountremote.command" @@ -55,6 +56,7 @@ "date-fns": "^3.6.0", "diff": "^7.0.0", "framer-motion": "^12.4.4", + "immer": "^11.1.3", "input-otp": "^1.4.1", "js-levenshtein": "^1.1.6", "lodash": "^4.17.21", diff --git a/inventory/src/components/dashboard/AircallDashboard.jsx b/inventory/src/components/dashboard/AircallDashboard.jsx index e8146ae..a921813 100644 --- a/inventory/src/components/dashboard/AircallDashboard.jsx +++ b/inventory/src/components/dashboard/AircallDashboard.jsx @@ -1,12 +1,6 @@ // components/AircallDashboard.jsx import React, { useState, useEffect } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - CardDescription, -} from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Select, SelectContent, @@ -14,8 +8,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, @@ -25,47 +17,39 @@ import { TableRow, } from "@/components/ui/table"; import { - PhoneCall, - PhoneMissed, - Clock, - UserCheck, - PhoneIncoming, - PhoneOutgoing, - ArrowUpDown, - Timer, - Loader2, - Download, - Search, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Progress } from "@/components/ui/progress"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { - LineChart, - Line, + BarChart, + Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, - BarChart, - Bar, } from "recharts"; -const COLORS = { - inbound: "hsl(262.1 83.3% 57.8%)", // Purple - outbound: "hsl(142.1 76.2% 36.3%)", // Green - missed: "hsl(47.9 95.8% 53.1%)", // Yellow - answered: "hsl(142.1 76.2% 36.3%)", // Green - duration: "hsl(221.2 83.2% 53.3%)", // Blue - hourly: "hsl(321.2 81.1% 41.2%)", // Pink +// Import shared components and tokens +import { + DashboardChartTooltip, + DashboardStatCard, + DashboardStatCardSkeleton, + DashboardSectionHeader, + DashboardErrorState, + ChartSkeleton, + TableSkeleton, + CARD_STYLES, + SCROLL_STYLES, + METRIC_COLORS, +} from "@/components/dashboard/shared"; +import { Phone, Clock, Zap, Timer } from "lucide-react"; + +// Aircall-specific colors using the standardized palette +const CHART_COLORS = { + inbound: METRIC_COLORS.aov, // Purple for inbound + outbound: METRIC_COLORS.revenue, // Green for outbound + missed: METRIC_COLORS.comparison, // Amber for missed + answered: METRIC_COLORS.revenue, // Green for answered + duration: METRIC_COLORS.orders, // Blue for duration + hourly: METRIC_COLORS.tertiary, // Pink for hourly }; const TIME_RANGES = [ @@ -89,41 +73,6 @@ const formatDuration = (seconds) => { return `${minutes}m ${remainingSeconds}s`; }; -const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => ( - - - {title} - - - -
{value}
- {subtitle && ( -

{subtitle}

- )} -
-
-); - -const CustomTooltip = ({ active, payload, label }) => { - if (active && payload && payload.length) { - return ( - - -

{label}

- {payload.map((entry, index) => ( -

- {`${entry.name}: ${entry.value}`} -

- ))} -
-
- ); - } - return null; -}; - - - const AgentPerformanceTable = ({ agents, onSort }) => { const [sortConfig, setSortConfig] = useState({ key: "total", @@ -144,19 +93,19 @@ const AgentPerformanceTable = ({ agents, onSort }) => { Agent - handleSort("total")}>Total Calls - handleSort("answered")}>Answered - handleSort("missed")}>Missed - handleSort("average_duration")}>Average Duration + handleSort("total")} className="cursor-pointer">Total Calls + handleSort("answered")} className="cursor-pointer">Answered + handleSort("missed")} className="cursor-pointer">Missed + handleSort("average_duration")} className="cursor-pointer">Average Duration {agents.map((agent) => ( - - {agent.name} + + {agent.name} {agent.total} - {agent.answered} - {agent.missed} + {agent.answered} + {agent.missed} {formatDuration(agent.average_duration)} ))} @@ -165,81 +114,6 @@ const AgentPerformanceTable = ({ agents, onSort }) => { ); }; -const SkeletonMetricCard = () => ( - - - - -
- - -
-
-
-); - -const SkeletonChart = ({ type = "line" }) => ( -
-
-
- {type === "bar" ? ( -
- {[...Array(24)].map((_, i) => ( -
- ))} -
- ) : ( -
- {[...Array(5)].map((_, i) => ( -
- ))} -
-
- )} -
-
-
-); - -const SkeletonTable = ({ rows = 5 }) => ( - - - - - - - - - - - - {[...Array(rows)].map((_, i) => ( - - - - - - - - ))} - -
-); - const AircallDashboard = () => { const [timeRange, setTimeRange] = useState("last7days"); const [metrics, setMetrics] = useState(null); @@ -252,7 +126,6 @@ const AircallDashboard = () => { }); const safeArray = (arr) => (Array.isArray(arr) ? arr : []); - const safeObject = (obj) => (obj && typeof obj === "object" ? obj : {}); const sortedAgents = metrics?.by_users ? Object.values(metrics.by_users).sort((a, b) => { @@ -261,44 +134,12 @@ const AircallDashboard = () => { }) : []; - const formatDate = (dateString) => { - try { - // Parse the date string (YYYY-MM-DD) - const [year, month, day] = dateString.split('-').map(Number); - - // Create a date object in ET timezone - const date = new Date(Date.UTC(year, month - 1, day)); - - // Format the date in ET timezone - return new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - year: "numeric", - timeZone: "America/New_York" - }).format(date); - } catch (error) { - console.error("Date formatting error:", error, { dateString }); - return "Invalid Date"; - } - }; - - - const handleExport = () => { - const timestamp = new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(new Date()); - - exportToCSV(filteredAgents, `aircall-agent-metrics-${timestamp}`); - }; - const chartData = { hourly: metrics?.by_hour ? metrics.by_hour.map((count, hour) => ({ - hour: new Date(2000, 0, 1, hour).toLocaleString('en-US', { - hour: 'numeric', - hour12: true + hour: new Date(2000, 0, 1, hour).toLocaleString('en-US', { + hour: 'numeric', + hour12: true }).toUpperCase(), calls: count || 0, })) @@ -322,16 +163,6 @@ const AircallDashboard = () => { })), }; - const peakHour = metrics?.by_hour - ? metrics.by_hour.indexOf(Math.max(...metrics.by_hour)) - : null; - - const busyAgent = sortedAgents?.length > 0 ? sortedAgents[0] : null; - - const bestAnswerRate = sortedAgents - ?.filter((agent) => agent.total > 0) - ?.sort((a, b) => b.answered / b.total - a.answered / a.total)[0]; - const fetchData = async () => { try { setIsLoading(true); @@ -356,11 +187,12 @@ const AircallDashboard = () => { if (error) { return ( - + -
- Error loading call data: {error} -
+
); @@ -368,15 +200,12 @@ const AircallDashboard = () => { return (
- - -
-
- Calls -
- + + - + @@ -387,91 +216,73 @@ const AircallDashboard = () => { ))} -
-
+ } + /> {/* Metric Cards */}
{isLoading ? ( [...Array(4)].map((_, i) => ( - + )) ) : metrics ? ( <> - - - Total Calls -
{metrics.total}
-
-
- ↑ {metrics.by_direction.inbound} inbound -
-
- ↓ {metrics.by_direction.outbound} outbound -
-
-
-
- - - Answer Rate -
- {`${((metrics.by_status.answered / metrics.total) * 100).toFixed(1)}%`} -
-
-
- {metrics.by_status.answered} answered -
-
- {metrics.by_status.missed} missed -
-
-
-
- - - Peak Hour -
- {metrics?.by_hour ? new Date(2000, 0, 1, metrics.by_hour.indexOf(Math.max(...metrics.by_hour))).toLocaleString('en-US', { hour: 'numeric', hour12: true }).toUpperCase() : 'N/A'} -
-
- Busiest Agent: {sortedAgents[0]?.name || "N/A"} -
-
-
- - - Avg Duration - - - -
-
- {formatDuration(metrics.average_duration)} -
-
- {metrics?.daily_data?.length > 0 - ? `${Math.round(metrics.total / metrics.daily_data.length)} calls/day` - : "N/A"} -
-
-
- -
-

Duration Distribution

- {metrics?.duration_distribution?.map((d, i) => ( -
- {d.range} - {d.count} calls -
- ))} -
-
-
-
-
-
+ + ↑ {metrics.by_direction.inbound} in + ↓ {metrics.by_direction.outbound} out + + } + icon={Phone} + iconColor="blue" + /> + + + {metrics.by_status.answered} answered + {metrics.by_status.missed} missed + + } + icon={Zap} + iconColor="green" + /> + + + + 0 + ? `${Math.round(metrics.total / metrics.daily_data.length)} calls/day` + : "N/A" + } + tooltip={ + metrics?.duration_distribution + ? `Duration Distribution: ${metrics.duration_distribution.map(d => `${d.range}: ${d.count}`).join(', ')}` + : undefined + } + icon={Timer} + iconColor="teal" + /> ) : null}
@@ -481,30 +292,32 @@ const AircallDashboard = () => { {/* Charts Row */}
{/* Daily Call Volume */} - - - Daily Call Volume - + + {isLoading ? ( - + ) : ( - + - } /> + } /> - - + + )} @@ -512,29 +325,31 @@ const AircallDashboard = () => { {/* Hourly Distribution */} - - - Hourly Distribution - + + {isLoading ? ( - + ) : ( - + - } /> - + } /> + )} @@ -545,15 +360,13 @@ const AircallDashboard = () => { {/* Tables Row */}
{/* Agent Performance */} - - - Agent Performance - + + {isLoading ? ( - + ) : ( -
+
setAgentSort({ key, direction })} @@ -564,29 +377,27 @@ const AircallDashboard = () => { {/* Missed Call Reasons Table */} - - - Missed Call Reasons - + + {isLoading ? ( - + ) : ( -
+
- Reason - Count + Reason + Count {chartData.missedReasons.map((reason, index) => ( - + {reason.reason} - + {reason.count} diff --git a/inventory/src/components/dashboard/AnalyticsDashboard.jsx b/inventory/src/components/dashboard/AnalyticsDashboard.jsx index cafaf4c..b4fefc6 100644 --- a/inventory/src/components/dashboard/AnalyticsDashboard.jsx +++ b/inventory/src/components/dashboard/AnalyticsDashboard.jsx @@ -8,7 +8,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { LineChart, Line, @@ -18,72 +17,25 @@ import { Tooltip, Legend, ResponsiveContainer, - ReferenceLine, } from "recharts"; -import { Loader2, TrendingUp, AlertCircle } from "lucide-react"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { TrendingUp } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { CARD_STYLES, TYPOGRAPHY } from "@/lib/dashboard/designTokens"; +import { + DashboardStatCard, + ChartSkeleton, + DashboardEmptyState, + TOOLTIP_STYLES, +} from "@/components/dashboard/shared"; -// Add helper function for currency formatting -const formatCurrency = (value, useFractionDigits = true) => { - if (typeof value !== "number") return "$0.00"; - const roundedValue = parseFloat(value.toFixed(useFractionDigits ? 2 : 0)); - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: useFractionDigits ? 2 : 0, - maximumFractionDigits: useFractionDigits ? 2 : 0, - }).format(roundedValue); -}; - -// Add skeleton components -const SkeletonChart = () => ( -
-
-
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* Chart line */} -
-
-
-
-
-
-
-
-); +// Note: Using ChartSkeleton from @/components/dashboard/shared const SkeletonStats = () => (
{[...Array(4)].map((_, i) => ( - + @@ -96,46 +48,7 @@ const SkeletonStats = () => (
); -const SkeletonButtons = () => ( -
- {[...Array(4)].map((_, i) => ( - - ))} -
-); - -// Add StatCard component -const StatCard = ({ - title, - value, - description, - trend, - trendValue, - colorClass = "text-gray-900 dark:text-gray-100", -}) => ( - - - {title} - {trend && ( - - {trendValue} - - )} - - -
{value}
- {description && ( -
{description}
- )} -
-
-); +// Note: Using shared DashboardStatCard from @/components/dashboard/shared // Add color constants const METRIC_COLORS = { @@ -252,41 +165,13 @@ export const AnalyticsDashboard = () => { const summaryStats = calculateSummaryStats(); - const CustomTooltip = ({ active, payload, label }) => { - if (active && payload && payload.length) { - return ( - - -

- {label instanceof Date ? label.toLocaleDateString() : label} -

-
- {payload.map((entry, index) => ( -
- {entry.name}: - - {entry.value.toLocaleString()} - -
- ))} -
-
-
- ); - } - return null; - }; - return ( - +
- + Analytics Overview
@@ -301,9 +186,9 @@ export const AnalyticsDashboard = () => { Details - + - Daily Details + Daily Details
{Object.entries(metrics).map(([key, value]) => ( @@ -328,7 +213,7 @@ export const AnalyticsDashboard = () => {
-
+
@@ -399,37 +284,37 @@ export const AnalyticsDashboard = () => { ) : summaryStats ? (
- - - -
) : null} @@ -499,19 +384,15 @@ export const AnalyticsDashboard = () => { {loading ? ( - + ) : !data.length ? ( -
-
- -
No analytics data available
-
- Try selecting a different time range -
-
-
+ ) : ( -
+
{ className="text-xs text-muted-foreground" tick={{ fill: "currentColor" }} /> - } /> + { + if (!active || !payload?.length) return null; + const date = payload[0]?.payload?.date; + const formattedDate = date instanceof Date + ? date.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + : String(date); + return ( +
+

{formattedDate}

+
+ {payload.map((entry, i) => ( +
+
+ + {entry.name} +
+ + {entry.value.toLocaleString()} + +
+ ))} +
+
+ ); + }} + /> {metrics.activeUsers && ( { // Loading State Component const LoadingState = () => ( -
+
{[...Array(8)].map((_, i) => ( -
+
@@ -173,13 +177,13 @@ const LoadingState = () => ( // Empty State Component const EmptyState = () => (
-
+
-

+

No activity yet today

-

+

Recent activity will appear here as it happens

@@ -227,11 +231,11 @@ const OrderStatusTags = ({ details }) => ( ); const ProductCard = ({ product }) => ( -
+
-

+

{product.ProductName || "Unnamed Product"}

{product.ItemStatus === "Pre-Order" && ( @@ -242,13 +246,13 @@ const ProductCard = ({ product }) => (
{product.Brand && ( -
+
{product.Brand}
)} {product.SKU && ( -
+
SKU: {product.SKU}
@@ -256,14 +260,14 @@ const ProductCard = ({ product }) => (
-
+
{formatCurrency(product.ItemPrice)}
-
+
Qty: {product.Quantity || product.QuantityOrdered || 1}
{product.RowTotal && ( -
+
Total: {formatCurrency(product.RowTotal)}
)} @@ -308,10 +312,10 @@ const PromotionalInfo = ({ details }) => { }; const OrderSummary = ({ details }) => ( -
+
-

+

Subtotal

@@ -336,7 +340,7 @@ const OrderSummary = ({ details }) => (
-

+

Shipping

@@ -354,7 +358,7 @@ const OrderSummary = ({ details }) => (
-
+
Total @@ -377,7 +381,7 @@ const OrderSummary = ({ details }) => ( const ShippingInfo = ({ details }) => (
-

+

Shipping Address

@@ -396,7 +400,7 @@ const ShippingInfo = ({ details }) => (
{details.TrackingNumber && (
-

+

Tracking Information

@@ -412,75 +416,6 @@ const ShippingInfo = ({ details }) => (
); -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}

- -
- {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 null; -}; - const EventDialog = ({ event, children }) => { const eventType = EVENT_TYPES[event.metric_id]; if (!eventType) return children; @@ -681,19 +616,19 @@ const EventDialog = ({ event, children }) => { <>
- + {toTitleCase(details.ShippingName)} - - + + #{details.OrderId}
-
+
{formatShipMethodSimple(details.ShipMethod)} {event.event_properties?.ShippedBy && ( <> - + Shipped by {event.event_properties.ShippedBy} )} @@ -872,8 +807,8 @@ export { EventDialog }; const EventCard = ({ event }) => { const eventType = EVENT_TYPES[event.metric_id] || { label: "Unknown Event", - color: "bg-gray-500", - textColor: "text-gray-600 dark:text-gray-400", + color: "bg-slate-500", + textColor: "text-muted-foreground", }; const Icon = EVENT_ICONS[event.metric_id] || Package; @@ -886,9 +821,9 @@ const EventCard = ({ event }) => { return ( - + - + Financial Details
@@ -1204,7 +1156,7 @@ const FinancialOverview = () => {
-
+
@@ -1353,33 +1305,22 @@ const FinancialOverview = () => { ) : error ? ( - - - Error - - Failed to load financial data: {error} - - + ) : !hasData ? ( -
-
- -
- No financial data available -
-
- Try selecting a different time range -
-
-
+ ) : ( <> -
+
{!hasActiveMetrics ? ( - + ) : ( @@ -1502,153 +1443,46 @@ type FinancialStatCardConfig = { title: string; value: string; description?: string; - trend: TrendSummary | null; - accentClass: string; + trendValue?: number | null; + trendInverted?: boolean; + iconColor: "blue" | "orange" | "emerald" | "purple"; tooltip?: string; - isLoading?: boolean; - showDescription?: boolean; }; +const ICON_MAP = { + income: DollarSign, + cogs: Package, + profit: PiggyBank, + margin: Percent, +} as const; + function FinancialStatGrid({ cards, }: { cards: FinancialStatCardConfig[]; }) { return ( - -
- {cards.map((card) => ( - - ))} -
-
- ); -} - -function FinancialStatCard({ - title, - value, - description, - trend, - accentClass, - tooltip, - isLoading, - showDescription, -}: { - title: string; - value: string; - description?: string; - trend: TrendSummary | null; - accentClass: string; - tooltip?: string; - isLoading?: boolean; - showDescription?: boolean; -}) { - const shouldShowDescription = isLoading ? showDescription !== false : Boolean(description); - - return ( - - -
- {isLoading ? ( - <> - - Placeholder - - - - - - - - - - ) : ( - <> - {title} - {tooltip ? ( - - - - - - {tooltip} - - - ) : null} - - )} -
- {isLoading ? ( - - - - Placeholder - - - - ) : trend?.label ? ( - - {trend.direction === "up" ? ( - - ) : trend.direction === "down" ? ( - - ) : ( - - )} - {trend.label} - - ) : null} -
- -
- {isLoading ? ( - - 0 - - - ) : ( - value - )} -
- {isLoading ? ( - shouldShowDescription ? ( -
- - Placeholder text - - -
- ) : null - ) : description ? ( -
{description}
- ) : null} -
-
+
+ {cards.map((card) => ( + + ))} +
); } @@ -1656,16 +1490,7 @@ function SkeletonStats() { return (
{Array.from({ length: 4 }).map((_, index) => ( - + ))}
); @@ -1673,7 +1498,7 @@ function SkeletonStats() { function SkeletonChart() { return ( -
+
{/* Grid lines */} @@ -1722,14 +1547,6 @@ function SkeletonChart() { ); } -function EmptyChartState({ message }: { message: string }) { - return ( -
- - {message} -
- ); -} const FinancialTooltip = ({ active, payload, label }: TooltipProps) => { if (!active || !payload?.length) { @@ -1755,9 +1572,9 @@ const FinancialTooltip = ({ active, payload, label }: TooltipProps entry !== undefined); return ( -
-

{resolvedLabel}

-
+
+

{resolvedLabel}

+
{orderedPayload.map((entry, index) => { const key = (entry.dataKey ?? "") as ChartSeriesKey; const rawValue = entry.value; @@ -1769,7 +1586,7 @@ const FinancialTooltip = ({ active, payload, label }: TooltipProps 0 && !isFuturePoint) { const percentage = (rawValue / income) * 100; @@ -1782,14 +1599,17 @@ const FinancialTooltip = ({ active, payload, label }: TooltipProps - - {SERIES_LABELS[key] ?? entry.name ?? key} - - +
+
+ + + {SERIES_LABELS[key] ?? entry.name ?? key} + +
+ {formattedValue}{percentageOfRevenue}
diff --git a/inventory/src/components/dashboard/GorgiasOverview.jsx b/inventory/src/components/dashboard/GorgiasOverview.jsx index eb0d375..413fdb3 100644 --- a/inventory/src/components/dashboard/GorgiasOverview.jsx +++ b/inventory/src/components/dashboard/GorgiasOverview.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Select, SelectTrigger, @@ -17,20 +17,24 @@ import { } from "@/components/ui/table"; import { Skeleton } from "@/components/ui/skeleton"; import { - Clock, - Star, - MessageSquare, Mail, Send, - Loader2, ArrowUp, ArrowDown, Zap, Timer, BarChart3, ClipboardCheck, + Star, } from "lucide-react"; import axios from "axios"; +import { CARD_STYLES } from "@/lib/dashboard/designTokens"; +import { + DashboardStatCard, + DashboardStatCardSkeleton, + DashboardSectionHeader, + DashboardErrorState, +} from "@/components/dashboard/shared"; const TIME_RANGES = { "today": "Today", @@ -81,104 +85,6 @@ const getDateRange = (days) => { }; }; -const MetricCard = ({ - title, - value, - delta, - suffix = "", - icon: Icon, - colorClass = "blue", - more_is_better = true, - loading = false, -}) => { - const getDeltaColor = (d) => { - if (d === 0) return "text-gray-600 dark:text-gray-400"; - const isPositive = d > 0; - return isPositive === more_is_better - ? "text-green-600 dark:text-green-500" - : "text-red-600 dark:text-red-500"; - }; - - const formatDelta = (d) => { - if (d === undefined || d === null) return null; - if (d === 0) return "0"; - return Math.abs(d) + suffix; - }; - - return ( - - -
-
- {loading ? ( - <> - -
- - -
- - ) : ( - <> -

{title}

-
-

- {typeof value === "number" - ? value.toLocaleString() + suffix - : value} -

- {delta !== undefined && delta !== 0 && ( -
- {delta > 0 ? ( - - ) : ( - - )} - - {formatDelta(delta)} - -
- )} -
- - )} -
- {!loading && Icon && ( - - )} - {loading && ( - - )} -
-
-
- ); -}; - -const SkeletonMetricCard = () => ( - - -
-
- -
- - -
-
- -
-
-
-); - const TableSkeleton = () => (
@@ -295,147 +201,146 @@ const GorgiasOverview = () => { if (error) { return ( - + -
- {error} -
+
); } return ( - - -
-

- Customer Service -

-
- -
-
-
+ + setTimeRange(value)} + > + + + {TIME_RANGES[timeRange]} + + + + {[ + ["today", "Today"], + ["7", "Last 7 Days"], + ["14", "Last 14 Days"], + ["30", "Last 30 Days"], + ["90", "Last 90 Days"], + ].map(([value, label]) => ( + + {label} + + ))} + + + } + />
{/* Message & Response Metrics */} {loading ? ( [...Array(7)].map((_, i) => ( - + )) ) : ( <> -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
+ + + + + + + )}
{/* Channel Distribution */} - - -

- Channel Distribution -

-
+ + {loading ? ( @@ -443,10 +348,10 @@ const GorgiasOverview = () => {
- Channel - Total - % - Change + Channel + Total + % + Change @@ -454,7 +359,7 @@ const GorgiasOverview = () => { .sort((a, b) => b.total - a.total) .map((channel, index) => ( - + {channel.name} @@ -494,12 +399,8 @@ const GorgiasOverview = () => { {/* Agent Performance */} - - -

- Agent Performance -

-
+ + {loading ? ( @@ -507,10 +408,10 @@ const GorgiasOverview = () => {
- Agent - Closed - Rating - Change + Agent + Closed + Rating + Change @@ -518,7 +419,7 @@ const GorgiasOverview = () => { .filter((agent) => agent.name !== "Unassigned") .map((agent, index) => ( - + {agent.name} diff --git a/inventory/src/components/dashboard/Header.jsx b/inventory/src/components/dashboard/Header.jsx index bf21646..bbe2d1d 100644 --- a/inventory/src/components/dashboard/Header.jsx +++ b/inventory/src/components/dashboard/Header.jsx @@ -40,6 +40,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { CARD_STYLES } from "@/lib/dashboard/designTokens"; const CraftsIcon = () => (
- - - - - - - + {[...Array(15)].map((_, i) => (
+ + + + + +
@@ -110,12 +115,6 @@ const TableSkeleton = () => (
); -// Error alert component -const ErrorAlert = ({ description }) => ( -
- {description} -
-); // MetricCell component for displaying campaign metrics const MetricCell = ({ @@ -232,21 +231,14 @@ const KlaviyoCampaigns = ({ className }) => { if (isLoading) { return ( - - -
- - - -
-
- - -
- -
-
-
+ + } + timeSelector={
} + /> @@ -255,81 +247,80 @@ const KlaviyoCampaigns = ({ className }) => { } return ( - - {error && } - -
- - Klaviyo Campaigns - -
-
- - - -
- + + {error && ( + + )} + + + +
-
-
+ } + timeSelector={ + + } + /> - - - - - - - + {filteredCampaigns.map((campaign) => ( { ) : ( )} -
+
{campaign.name}
@@ -419,7 +410,7 @@ const KlaviyoCampaigns = ({ className }) => {

{campaign.name}

{campaign.subject}

diff --git a/inventory/src/components/dashboard/MetaCampaigns.jsx b/inventory/src/components/dashboard/MetaCampaigns.jsx index 635b104..40f498a 100644 --- a/inventory/src/components/dashboard/MetaCampaigns.jsx +++ b/inventory/src/components/dashboard/MetaCampaigns.jsx @@ -1,11 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Select, SelectContent, @@ -15,7 +9,6 @@ import { } from "@/components/ui/select"; import { Instagram, - Loader2, Users, DollarSign, Eye, @@ -25,10 +18,16 @@ import { Target, ShoppingCart, MessageCircle, - Hash, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; +import { CARD_STYLES } from "@/lib/dashboard/designTokens"; +import { + DashboardStatCard, + DashboardStatCardSkeleton, + DashboardSectionHeader, + DashboardErrorState, +} from "@/components/dashboard/shared"; // Helper functions for formatting const formatCurrency = (value, decimalPlaces = 2) => @@ -49,40 +48,6 @@ const formatNumber = (value, decimalPlaces = 0) => { const formatPercent = (value, decimalPlaces = 2) => `${(value || 0).toFixed(decimalPlaces)}%`; -const summaryCard = (label, value, options = {}) => { - const { - isMonetary = false, - isPercentage = false, - decimalPlaces = 0, - icon: Icon, - iconColor, - } = options; - - let displayValue; - if (isMonetary) { - displayValue = formatCurrency(value, decimalPlaces); - } else if (isPercentage) { - displayValue = formatPercent(value, decimalPlaces); - } else { - displayValue = formatNumber(value, decimalPlaces); - } - - return ( - - -
-
-

{label}

-

{displayValue}

-
- {Icon && ( - - )} -
-
-
- ); -}; const MetricCell = ({ value, label, sublabel, isMonetary = false, isPercentage = false, decimalPlaces = 0 }) => { const formattedValue = isMonetary @@ -243,38 +208,22 @@ const processCampaignData = (campaign) => { }; }; -const SkeletonMetricCard = () => ( - - -
-
- -
- -
-
- -
-
-
-); - const SkeletonTable = () => (
+ + + + + +
- - + {[...Array(8)].map((_, i) => ( - ))} - + {[...Array(5)].map((_, rowIndex) => (
+
+
@@ -443,24 +392,17 @@ const MetaCampaigns = () => { if (loading) { return ( - - -
- - Meta Ads Performance - - -
+ + } + /> +
{[...Array(12)].map((_, i) => ( - + ))}
@@ -473,25 +415,25 @@ const MetaCampaigns = () => { if (error) { return ( - + -
- {error} -
+
); } return ( - - -
- - Meta Ads Performance - + + - + @@ -502,82 +444,102 @@ const MetaCampaigns = () => { Last 90 days -
+ } + /> +
- {[ - { - label: "Active Campaigns", - value: summaryMetrics?.totalCampaigns, - options: { icon: Target, iconColor: "text-purple-500" }, - }, - { - label: "Total Spend", - value: summaryMetrics?.totalSpend, - options: { isMonetary: true, decimalPlaces: 0, icon: DollarSign, iconColor: "text-green-500" }, - }, - { - label: "Total Reach", - value: summaryMetrics?.totalReach, - options: { icon: Users, iconColor: "text-blue-500" }, - }, - { - label: "Total Impressions", - value: summaryMetrics?.totalImpressions, - options: { icon: Eye, iconColor: "text-indigo-500" }, - }, - { - label: "Avg Frequency", - value: summaryMetrics?.avgFrequency, - options: { decimalPlaces: 2, icon: Repeat, iconColor: "text-cyan-500" }, - }, - { - label: "Total Engagements", - value: summaryMetrics?.totalPostEngagements, - options: { icon: MessageCircle, iconColor: "text-pink-500" }, - }, - { - label: "Avg CPM", - value: summaryMetrics?.avgCpm, - options: { isMonetary: true, decimalPlaces: 2, icon: DollarSign, iconColor: "text-emerald-500" }, - }, - { - label: "Avg CTR", - value: summaryMetrics?.avgCtr, - options: { isPercentage: true, decimalPlaces: 2, icon: BarChart, iconColor: "text-orange-500" }, - }, - { - label: "Avg CPC", - value: summaryMetrics?.avgCpc, - options: { isMonetary: true, decimalPlaces: 2, icon: MousePointer, iconColor: "text-rose-500" }, - }, - { - label: "Total Link Clicks", - value: summaryMetrics?.totalLinkClicks, - options: { icon: MousePointer, iconColor: "text-amber-500" }, - }, - { - label: "Total Purchases", - value: summaryMetrics?.totalPurchases, - options: { icon: ShoppingCart, iconColor: "text-teal-500" }, - }, - { - label: "Purchase Value", - value: summaryMetrics?.totalPurchaseValue, - options: { isMonetary: true, decimalPlaces: 0, icon: DollarSign, iconColor: "text-lime-500" }, - }, - ].map((card) => ( -
- {summaryCard(card.label, card.value, card.options)} -
- ))} + + + + + + + + + + + +
- - + - - - - - - - - - + {sortedCampaigns.map((campaign) => ( { > - - - - - + {[...Array(20)].map((_, i) => ( ))} @@ -178,12 +179,12 @@ const ProductGrid = ({ if (loading) { return ( - +
- + {description && ( @@ -210,14 +211,14 @@ const ProductGrid = ({ } return ( - +
- {title} + {title} {description && ( - {description} + {description} )}
@@ -279,27 +280,22 @@ const ProductGrid = ({
{error ? ( - - - Error - - Failed to load products: {error} - - + ) : !products?.length ? ( -
- -

No product data available

-

Try selecting a different time range

-
+ ) : (
+
+ + + + + + + +
-
+
diff --git a/inventory/src/components/dashboard/MiniEventFeed.jsx b/inventory/src/components/dashboard/MiniEventFeed.jsx index 1d61cb1..0b8c424 100644 --- a/inventory/src/components/dashboard/MiniEventFeed.jsx +++ b/inventory/src/components/dashboard/MiniEventFeed.jsx @@ -22,10 +22,10 @@ import { ChevronRight, } from "lucide-react"; import { format } from "date-fns"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Skeleton } from "@/components/ui/skeleton"; import { EventDialog } from "./EventFeed.jsx"; import { Button } from "@/components/ui/button"; +import { DashboardErrorState } from "@/components/dashboard/shared"; const METRIC_IDS = { PLACED_ORDER: "Y8cqcF", @@ -439,13 +439,7 @@ const MiniEventFeed = ({ {loading && !events.length ? ( ) : error ? ( - - - Error - - Failed to load event feed: {error} - - + ) : !events || events.length === 0 ? (
diff --git a/inventory/src/components/dashboard/MiniRealtimeAnalytics.jsx b/inventory/src/components/dashboard/MiniRealtimeAnalytics.jsx index 3f3ed04..61f8cdf 100644 --- a/inventory/src/components/dashboard/MiniRealtimeAnalytics.jsx +++ b/inventory/src/components/dashboard/MiniRealtimeAnalytics.jsx @@ -11,41 +11,11 @@ import { import { AlertTriangle, Users, Activity } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { format } from "date-fns"; -import { - summaryCard, - SkeletonSummaryCard, - SkeletonBarChart, - processBasicData, -} from "./RealtimeAnalytics"; +import { processBasicData } from "./RealtimeAnalytics"; +import { DashboardStatCardMini, DashboardStatCardMiniSkeleton, TOOLTIP_THEMES } from "@/components/dashboard/shared"; import { Skeleton } from "@/components/ui/skeleton"; +import { METRIC_COLORS } from "@/lib/dashboard/designTokens"; -const SkeletonCard = ({ colorScheme = "sky" }) => ( - - - -
- -
-
-
-
-
-
- - -
-
- -
-
- -
-
-
-
-
- -); const MiniRealtimeAnalytics = () => { const [basicData, setBasicData] = useState({ @@ -119,8 +89,8 @@ const MiniRealtimeAnalytics = () => { return (
- - + +
@@ -168,34 +138,22 @@ const MiniRealtimeAnalytics = () => { return (
- {summaryCard( - "Last 30 Minutes", - "Active users", - basicData.last30MinUsers, - { - colorClass: "text-sky-200", - titleClass: "text-sky-100 font-bold text-md", - descriptionClass: "pt-2 text-sky-200 text-md font-semibold", - background: "h-[150px] pt-2 bg-gradient-to-br from-sky-900 to-sky-800", - icon: Users, - iconColor: "text-sky-900", - iconBackground: "bg-sky-300" - } - )} - {summaryCard( - "Last 5 Minutes", - "Active users", - basicData.last5MinUsers, - { - colorClass: "text-sky-200", - titleClass: "text-sky-100 font-bold text-md", - descriptionClass: "pt-2 text-sky-200 text-md font-semibold", - background: "h-[150px] pt-2 bg-gradient-to-br from-sky-900 to-sky-800", - icon: Activity, - iconColor: "text-sky-900", - iconBackground: "bg-sky-300" - } - )} + +
@@ -219,28 +177,25 @@ const MiniRealtimeAnalytics = () => { { if (active && payload && payload.length) { + const styles = TOOLTIP_THEMES.sky; return ( - - -

- {payload[0].payload.timestamp} -

-
- - Active Users: - - - {payload[0].value} - +
+

+ {payload[0].payload.timestamp} +

+
+
+ Active Users + {payload[0].value}
- - +
+
); } return null; }} /> - +
diff --git a/inventory/src/components/dashboard/MiniSalesChart.jsx b/inventory/src/components/dashboard/MiniSalesChart.jsx index 1ac2ce3..b311484 100644 --- a/inventory/src/components/dashboard/MiniSalesChart.jsx +++ b/inventory/src/components/dashboard/MiniSalesChart.jsx @@ -1,12 +1,8 @@ -import React, { useState, useEffect, useCallback, memo } from "react"; -import axios from "axios"; +import React, { useState, useEffect, useCallback } from "react"; import { acotService } from "@/services/dashboard/acotService"; import { Card, CardContent, - CardDescription, - CardHeader, - CardTitle, } from "@/components/ui/card"; import { LineChart, @@ -17,141 +13,16 @@ import { 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, Truck, PiggyBank, ArrowUp,ArrowDown, Banknote, Package } from "lucide-react"; -import { formatCurrency, CustomTooltip, processData, StatCard } from "./SalesChart.jsx"; - -const SkeletonChart = () => ( -
-
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* Chart lines */} -
-
-
-
-
-
-
-); - -const MiniStatCard = memo(({ - title, - value, - icon: Icon, - colorClass, - iconColor, - iconBackground, - background, - previousValue, - trend, - trendValue, - onClick, - active = true, - titleClass = "text-sm font-bold text-gray-100", - descriptionClass = "text-sm font-semibold text-gray-200" -}) => ( - - - - {title} - - {Icon && ( -
-
- -
- )} - - -
-
-
- {value} -
-
- Prev: {previousValue} - {trend && ( - - {trend === "up" ? ( - - ) : ( - - )} - {trendValue} - - )} -
-
-
-
- -)); - -MiniStatCard.displayName = "MiniStatCard"; - -const SkeletonCard = ({ colorScheme = "emerald" }) => ( - - - -
- -
-
-
-
- -
- - -
- -
- - -
-
-
- -); +import { AlertCircle, PiggyBank, Truck } from "lucide-react"; +import { formatCurrency, processData } from "./SalesChart.jsx"; +import { METRIC_COLORS } from "@/lib/dashboard/designTokens"; +import { + DashboardStatCardMini, + DashboardStatCardMiniSkeleton, + ChartSkeleton, + TOOLTIP_THEMES, +} from "@/components/dashboard/shared"; const MiniSalesChart = ({ className = "" }) => { const [data, setData] = useState([]); @@ -269,19 +140,46 @@ const MiniSalesChart = ({ className = "" }) => { ); } + // Helper to calculate trend direction + const getRevenueTrend = () => { + const current = summaryStats.periodProgress < 100 + ? (projection?.projectedRevenue || summaryStats.totalRevenue) + : summaryStats.totalRevenue; + return current >= summaryStats.prevRevenue ? "up" : "down"; + }; + + const getRevenueTrendValue = () => { + const current = summaryStats.periodProgress < 100 + ? (projection?.projectedRevenue || summaryStats.totalRevenue) + : summaryStats.totalRevenue; + return `${Math.abs(Math.round((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 projected = Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress)); + const current = summaryStats.periodProgress < 100 ? projected : summaryStats.totalOrders; + return `${Math.abs(Math.round((current - summaryStats.prevOrders) / summaryStats.prevOrders * 100))}%`; + }; + if (loading && !data) { return (
{/* Stat Cards */}
- - + +
{/* Chart Card */} - +
@@ -294,56 +192,38 @@ const MiniSalesChart = ({ className = "" }) => {
{loading ? ( <> - - + + ) : ( <> - = summaryStats.prevRevenue ? "up" : "down") - : (summaryStats.totalRevenue >= summaryStats.prevRevenue ? "up" : "down") - } - trendValue={ - summaryStats.periodProgress < 100 - ? `${Math.abs(Math.round(((projection?.projectedRevenue || summaryStats.totalRevenue) - summaryStats.prevRevenue) / summaryStats.prevRevenue * 100))}%` - : `${Math.abs(Math.round(((summaryStats.totalRevenue - summaryStats.prevRevenue) / summaryStats.prevRevenue) * 100))}%` - } - colorClass="text-emerald-300" - titleClass="text-emerald-300 font-bold text-md" - descriptionClass="text-emerald-300 text-md font-semibold pb-1" + description={`Prev: ${formatCurrency(summaryStats.prevRevenue, false)}`} + trend={{ + direction: getRevenueTrend(), + value: getRevenueTrendValue(), + }} icon={PiggyBank} - iconColor="text-emerald-900" iconBackground="bg-emerald-300" + gradient="slate" + className={!visibleMetrics.revenue ? 'opacity-50' : ''} onClick={() => toggleMetric('revenue')} - active={visibleMetrics.revenue} /> - = summaryStats.prevOrders ? "up" : "down") - : (summaryStats.totalOrders >= summaryStats.prevOrders ? "up" : "down") - } - trendValue={ - summaryStats.periodProgress < 100 - ? `${Math.abs(Math.round(((Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress))) - summaryStats.prevOrders) / summaryStats.prevOrders * 100))}%` - : `${Math.abs(Math.round(((summaryStats.totalOrders - summaryStats.prevOrders) / summaryStats.prevOrders) * 100))}%` - } - colorClass="text-blue-300" - titleClass="text-blue-300 font-bold text-md" - descriptionClass="text-blue-300 text-md font-semibold pb-1" + description={`Prev: ${summaryStats.prevOrders.toLocaleString()}`} + trend={{ + direction: getOrdersTrend(), + value: getOrdersTrendValue(), + }} icon={Truck} - iconColor="text-blue-900" iconBackground="bg-blue-300" + gradient="slate" + className={!visibleMetrics.orders ? 'opacity-50' : ''} onClick={() => toggleMetric('orders')} - active={visibleMetrics.orders} /> )} @@ -354,40 +234,7 @@ const MiniSalesChart = ({ className = "" }) => {
{loading ? ( -
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* Chart lines */} -
-
-
-
-
-
+ ) : ( { content={({ active, payload }) => { if (active && payload && payload.length) { const timestamp = new Date(payload[0].payload.timestamp); + const styles = TOOLTIP_THEMES.stone; return ( - - -

- {timestamp.toLocaleDateString([], { - weekday: "short", - month: "short", - day: "numeric" - })} -

+
+

+ {timestamp.toLocaleDateString([], { + weekday: "short", + month: "short", + day: "numeric" + })} +

+
{payload .filter(entry => visibleMetrics[entry.dataKey]) .map((entry, index) => ( -
- - {entry.name}: +
+ + {entry.name} - - {entry.dataKey === 'revenue' + + {entry.dataKey === 'revenue' ? formatCurrency(entry.value) : entry.value.toLocaleString()}
))} - - +
+
); } return null; @@ -458,7 +306,7 @@ const MiniSalesChart = ({ className = "" }) => { type="monotone" dataKey="revenue" name="Revenue" - stroke="#10b981" + stroke={METRIC_COLORS.revenue} strokeWidth={2} dot={false} /> @@ -469,7 +317,7 @@ const MiniSalesChart = ({ className = "" }) => { type="monotone" dataKey="orders" name="Orders" - stroke="#3b82f6" + stroke={METRIC_COLORS.orders} strokeWidth={2} dot={false} /> diff --git a/inventory/src/components/dashboard/MiniStatCards.jsx b/inventory/src/components/dashboard/MiniStatCards.jsx index b9771e0..1a11121 100644 --- a/inventory/src/components/dashboard/MiniStatCards.jsx +++ b/inventory/src/components/dashboard/MiniStatCards.jsx @@ -1,20 +1,11 @@ -import React, { useState, useEffect, useCallback, memo } from "react"; -import axios from "axios"; +import React, { useState, useEffect, useCallback } from "react"; import { acotService } from "@/services/dashboard/acotService"; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Dialog, DialogContent, @@ -22,7 +13,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { DateTime } from "luxon"; -import { TIME_RANGES } from "@/lib/dashboard/constants"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { DollarSign, @@ -30,23 +20,7 @@ import { Package, AlertCircle, CircleDollarSign, - Loader2, } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, - TooltipProvider, -} from "@/components/ui/tooltip"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; // Import the detail view components and utilities from StatCards import { @@ -54,163 +28,28 @@ import { OrdersDetails, AverageOrderDetails, ShippingDetails, - StatCard, DetailDialog, formatCurrency, formatPercentage, - SkeletonCard, } from "./StatCards"; +import { + DashboardStatCardMini, + DashboardStatCardMiniSkeleton, + ChartSkeleton, + TableSkeleton, + DashboardErrorState, +} from "@/components/dashboard/shared"; -// Mini skeleton components -const MiniSkeletonChart = ({ type = "line" }) => ( -
-
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {type === "bar" ? ( -
- {[...Array(24)].map((_, i) => ( -
- ))} -
- ) : ( -
-
-
-
-
- )} -
-
-); - -const MiniSkeletonTable = ({ rows = 8, colorScheme = "orange" }) => ( -
- - - - - - - - - - - - - - - - {[...Array(rows)].map((_, i) => ( - - - - - - - - - - - - ))} - -
-
-); +// Helper to map metric to colorVariant +const getColorVariant = (metric) => { + switch (metric) { + case 'revenue': return 'emerald'; + case 'orders': return 'blue'; + case 'average_order': return 'violet'; + case 'shipping': return 'orange'; + default: return 'default'; + } +}; const MiniStatCards = ({ timeRange: initialTimeRange = "today", @@ -421,101 +260,16 @@ const MiniStatCards = ({ if (loading && !stats) { return (
- - - - - -
-
- -
- - -
- -
- - -
-
-
- - - - - - - -
-
- -
- - -
- -
- - -
-
-
- - - - - - - -
-
- -
- - -
- -
- - -
-
-
- - - - - - - -
-
- -
- - -
- -
- - -
-
-
- + + + +
); } if (error) { - return ( - - - Error - Failed to load stats: {error} - - ); + return ; } if (!stats) return null; @@ -527,100 +281,68 @@ const MiniStatCards = ({ return ( <>
- - Proj: - {projectionLoading ? ( -
- -
- ) : ( - formatCurrency( - projection?.projectedRevenue || stats.projectedRevenue - ) - )} -
- ) : null + stats?.periodProgress < 100 + ? `Proj: ${formatCurrency(projection?.projectedRevenue || stats.projectedRevenue)}` + : undefined } - progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined} - trend={projectionLoading && stats?.periodProgress < 100 ? undefined : revenueTrend?.trend} - trendValue={ - projectionLoading && stats?.periodProgress < 100 ? ( -
- - -
- ) : revenueTrend?.value ? ( - formatPercentage(revenueTrend.value) - ) : null + trend={ + revenueTrend?.trend && !projectionLoading + ? { direction: revenueTrend.trend, value: formatPercentage(revenueTrend.value) } + : undefined } - colorClass="text-emerald-200" - titleClass="text-emerald-100 font-bold text-md" - descriptionClass="text-emerald-200 text-md font-semibold" icon={DollarSign} - iconColor="text-emerald-900" iconBackground="bg-emerald-300" - onDetailsClick={() => setSelectedMetric("revenue")} - isLoading={loading || !stats} - variant="mini" - background="h-[150px] bg-gradient-to-br from-emerald-900 to-emerald-800" + gradient="emerald" + className="h-[150px]" + onClick={() => setSelectedMetric("revenue")} /> - setSelectedMetric("orders")} - isLoading={loading || !stats} - variant="mini" - background="h-[150px] bg-gradient-to-br from-blue-900 to-blue-800" + gradient="blue" + className="h-[150px]" + onClick={() => setSelectedMetric("orders")} /> - setSelectedMetric("average_order")} - isLoading={loading || !stats} - variant="mini" - background="h-[150px] bg-gradient-to-br from-violet-900 to-violet-800" + gradient="violet" + className="h-[150px]" + onClick={() => setSelectedMetric("average_order")} /> - setSelectedMetric("shipping")} - isLoading={loading || !stats} - variant="mini" - background="h-[150px] bg-gradient-to-br from-orange-900 to-orange-800" + gradient="orange" + className="h-[150px]" + onClick={() => setSelectedMetric("shipping")} />
@@ -633,7 +355,7 @@ const MiniStatCards = ({ selectedMetric === 'orders' ? 'bg-blue-50 dark:bg-blue-950/30' : selectedMetric === 'average_order' ? 'bg-violet-50 dark:bg-violet-950/30' : selectedMetric === 'shipping' ? 'bg-orange-50 dark:bg-orange-950/30' : - 'bg-white dark:bg-gray-950' + 'bg-card' } backdrop-blur-md border-none`}>
@@ -657,20 +379,18 @@ const MiniStatCards = ({ {detailDataLoading[selectedMetric] ? (
{selectedMetric === "shipping" ? ( - ) : ( <> - {selectedMetric === "orders" && (
@@ -683,7 +403,12 @@ const MiniStatCards = ({ }`}> Hourly Distribution - +
)} diff --git a/inventory/src/components/dashboard/Navigation.jsx b/inventory/src/components/dashboard/Navigation.jsx index 6ea25ed..15d88d3 100644 --- a/inventory/src/components/dashboard/Navigation.jsx +++ b/inventory/src/components/dashboard/Navigation.jsx @@ -226,7 +226,7 @@ const Navigation = () => { > { ))}
-
+
- + + + + +
- - - - - + {filteredProducts.map((product) => ( {product.name} @@ -376,7 +372,7 @@ const ProductGrid = ({ -
- + + + + +
+ {product.totalQuantity} diff --git a/inventory/src/components/dashboard/RealtimeAnalytics.jsx b/inventory/src/components/dashboard/RealtimeAnalytics.jsx index a4085f7..d00490c 100644 --- a/inventory/src/components/dashboard/RealtimeAnalytics.jsx +++ b/inventory/src/components/dashboard/RealtimeAnalytics.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { BarChart, Bar, @@ -7,19 +7,13 @@ import { YAxis, Tooltip, ResponsiveContainer, - PieChart, - Pie, - Cell, } from "recharts"; -import { Loader2, AlertTriangle } from "lucide-react"; import { Tooltip as UITooltip, TooltipContent, TooltipTrigger, TooltipProvider, } from "@/components/ui/tooltip"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, @@ -30,141 +24,51 @@ import { TableCell, } from "@/components/ui/table"; import { format } from "date-fns"; -import { Skeleton } from "@/components/ui/skeleton"; -export const METRIC_COLORS = { +// Import shared components and tokens +import { + DashboardChartTooltip, + DashboardSectionHeader, + DashboardStatCard, + StatCardSkeleton, + ChartSkeleton, + TableSkeleton, + DashboardErrorState, + CARD_STYLES, + TYPOGRAPHY, + SCROLL_STYLES, + METRIC_COLORS, +} from "@/components/dashboard/shared"; + +// Realtime-specific colors using the standardized palette +const REALTIME_COLORS = { activeUsers: { - color: "#8b5cf6", - className: "text-purple-600 dark:text-purple-400", + color: METRIC_COLORS.aov, // Purple + className: "text-chart-aov", }, pages: { - color: "#10b981", - className: "text-emerald-600 dark:text-emerald-400", + color: METRIC_COLORS.revenue, // Emerald + className: "text-chart-revenue", }, sources: { - color: "#f59e0b", - className: "text-amber-600 dark:text-amber-400", + color: METRIC_COLORS.comparison, // Amber + className: "text-chart-comparison", }, }; -export const summaryCard = (label, sublabel, value, options = {}) => { - const { - colorClass = "text-gray-900 dark:text-gray-100", - titleClass = "text-sm font-medium text-gray-500 dark:text-gray-400", - descriptionClass = "text-sm text-gray-600 dark:text-gray-300", - background = "bg-white dark:bg-gray-900/60", - icon: Icon, - iconColor, - iconBackground - } = options; - - return ( - - - - {label} - - {Icon && ( -
-
- -
- )} - - -
-
-
- {value.toLocaleString()} -
-
- {sublabel} -
-
-
-
- - ); -}; +// Export for backwards compatibility +export { REALTIME_COLORS as METRIC_COLORS }; export const SkeletonSummaryCard = () => ( - - - - - - - - - + ); export const SkeletonBarChart = () => ( -
-
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* Bars */} -
- {[...Array(30)].map((_, i) => ( -
- ))} -
-
-
+ ); export const SkeletonTable = () => ( -
- - - - - - - - - - - - - {[...Array(8)].map((_, i) => ( - - - - - - - - - ))} - -
-
+ ); export const processBasicData = (data) => { @@ -223,10 +127,9 @@ export const QuotaInfo = ({ tokenQuota }) => { const { remaining: projectHourlyRemaining = 0, - consumed: projectHourlyConsumed = 0, } = projectHourly; - const { remaining: dailyRemaining = 0, consumed: dailyConsumed = 0 } = daily; + const { remaining: dailyRemaining = 0 } = daily; const { remaining: errorsRemaining = 10, consumed: errorsConsumed = 0 } = serverErrors; @@ -244,9 +147,9 @@ export const QuotaInfo = ({ tokenQuota }) => { const getStatusColor = (percentage) => { const numericPercentage = parseFloat(percentage); if (isNaN(numericPercentage) || numericPercentage < 20) - return "text-red-500 dark:text-red-400"; - if (numericPercentage < 40) return "text-yellow-500 dark:text-yellow-400"; - return "text-green-500 dark:text-green-400"; + return "text-trend-negative"; + if (numericPercentage < 40) return "text-chart-comparison"; + return "text-trend-positive"; }; return ( @@ -258,39 +161,37 @@ export const QuotaInfo = ({ tokenQuota }) => {
-
-
-
-
- Project Hourly -
-
- {projectHourlyRemaining.toLocaleString()} / 14,000 remaining -
+
+
+
+ Project Hourly
-
-
- Daily -
-
- {dailyRemaining.toLocaleString()} / 200,000 remaining -
+
+ {projectHourlyRemaining.toLocaleString()} / 14,000 remaining
-
-
- Server Errors -
-
- {errorsConsumed} / 10 used this hour -
+
+
+
+ Daily
-
-
- Thresholded Requests -
-
- {thresholdConsumed} / 120 used this hour -
+
+ {dailyRemaining.toLocaleString()} / 200,000 remaining +
+
+
+
+ Server Errors +
+
+ {errorsConsumed} / 10 used this hour +
+
+
+
+ Thresholded Requests +
+
+ {thresholdConsumed} / 120 used this hour
@@ -298,6 +199,27 @@ export const QuotaInfo = ({ tokenQuota }) => { ); }; +// Custom tooltip for the realtime chart +const RealtimeTooltip = ({ active, payload }) => { + if (active && payload && payload.length) { + const timestamp = new Date( + Date.now() + payload[0].payload.minute * 60000 + ); + return ( + + ); + } + return null; +}; + export const RealtimeAnalytics = () => { const [basicData, setBasicData] = useState({ last30MinUsers: 0, @@ -422,24 +344,13 @@ export const RealtimeAnalytics = () => { }; }, [isPaused]); - const togglePause = () => { - setIsPaused(!isPaused); - }; - if (loading && !basicData && !detailedData) { return ( - - -
- - Real-Time Analytics - - -
-
+ + -
+
@@ -447,7 +358,7 @@ export const RealtimeAnalytics = () => {
{[...Array(3)].map((_, i) => ( - +
))}
@@ -458,51 +369,45 @@ export const RealtimeAnalytics = () => { } return ( - - -
- - Real-Time Analytics - -
- - - -
- Last updated:{" "} - {format(new Date(basicData.lastUpdated), "h:mm a")} -
-
- - - -
-
-
-
-
+ + + + +
+ Last updated:{" "} + {basicData.lastUpdated && format(new Date(basicData.lastUpdated), "h:mm a")} +
+
+ + + +
+ + } + /> {error && ( - - - {error} - + )}
- {summaryCard( - "Last 30 minutes", - "Active users", - basicData.last30MinUsers, - { colorClass: METRIC_COLORS.activeUsers.className } - )} - {summaryCard( - "Last 5 minutes", - "Active users", - basicData.last5MinUsers, - { colorClass: METRIC_COLORS.activeUsers.className } - )} + +
@@ -513,7 +418,7 @@ export const RealtimeAnalytics = () => { -
+
{ value + "m"} - className="text-xs" - tick={{ fill: "currentColor" }} + className="text-xs fill-muted-foreground" + tickLine={false} + axisLine={false} /> - - { - if (active && payload && payload.length) { - const timestamp = new Date( - Date.now() + payload[0].payload.minute * 60000 - ); - return ( - - -

- {format(timestamp, "h:mm a")} -

-
- - Active Users: - - - {payload[0].value.toLocaleString()} - -
-
-
- ); - } - return null; - }} + + } /> + -
-
+
- - + + Page - + Active Users {detailedData.currentPages.map((page, index) => ( - - + + {page.path} {page.activeUsers} @@ -595,26 +479,26 @@ export const RealtimeAnalytics = () => { -
+
- - + + Source - + Active Users {detailedData.sources.map((source, index) => ( - - + + {source.source} {source.activeUsers} diff --git a/inventory/src/components/dashboard/SalesChart.jsx b/inventory/src/components/dashboard/SalesChart.jsx index 51a0232..585b3ce 100644 --- a/inventory/src/components/dashboard/SalesChart.jsx +++ b/inventory/src/components/dashboard/SalesChart.jsx @@ -24,8 +24,6 @@ import { TrendingDown, Info, AlertCircle, - ArrowUp, - ArrowDown, } from "lucide-react"; import { LineChart, @@ -70,7 +68,19 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { + CARD_STYLES, + TYPOGRAPHY, + METRIC_COLORS as SHARED_METRIC_COLORS, +} from "@/lib/dashboard/designTokens"; +import { + DashboardStatCard, + ChartSkeleton, + TableSkeleton, + DashboardEmptyState, + DashboardErrorState, + TOOLTIP_STYLES, +} from "@/components/dashboard/shared"; const METRIC_IDS = { PLACED_ORDER: "Y8cqcF", @@ -127,70 +137,17 @@ const formatPercentage = (value) => { return `${Math.abs(Math.round(value))}%`; }; -// Add color mapping for metrics +// Add color mapping for metrics - using shared tokens where applicable const METRIC_COLORS = { - revenue: "#8b5cf6", - orders: "#10b981", - avgOrderValue: "#9333ea", - movingAverage: "#f59e0b", - prevRevenue: "#f97316", - prevOrders: "#0ea5e9", - prevAvgOrderValue: "#f59e0b", + revenue: SHARED_METRIC_COLORS.aov, // Purple for revenue + orders: SHARED_METRIC_COLORS.revenue, // Emerald for orders + avgOrderValue: "#9333ea", // Deep purple for AOV + movingAverage: SHARED_METRIC_COLORS.comparison, // Amber for moving average + prevRevenue: SHARED_METRIC_COLORS.expense, // Orange for prev revenue + prevOrders: SHARED_METRIC_COLORS.secondary, // Cyan for prev orders + prevAvgOrderValue: SHARED_METRIC_COLORS.comparison, // Amber for prev AOV }; -// Memoize the StatCard component -export const StatCard = memo( - ({ - title, - value, - description, - trend, - trendValue, - valuePrefix = "", - valueSuffix = "", - trendPrefix = "", - trendSuffix = "", - className = "", - colorClass = "text-gray-900 dark:text-gray-100", - }) => ( - - - {title} - {trend && ( - - {trend === "up" ? ( - - ) : ( - - )} - {trendPrefix} - {trendValue} - {trendSuffix} - - )} - - -
- {valuePrefix} - {value} - {valueSuffix} -
- {description && ( -
{description}
- )} -
-
- ) -); - -StatCard.displayName = "StatCard"; - // Export CustomTooltip export const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { @@ -202,23 +159,29 @@ export const CustomTooltip = ({ active, payload, label }) => { }); return ( - - -

{formattedDate}

+
+

{formattedDate}

+
{payload.map((entry, index) => { const value = entry.dataKey.toLowerCase().includes('revenue') || entry.dataKey === 'avgOrderValue' ? formatCurrency(entry.value) : entry.value.toLocaleString(); return ( -
- {entry.name}: - {value} +
+
+ + {entry.name} +
+ {value}
); })} - - +
+
); } return null; @@ -394,54 +357,64 @@ const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = const aovDiff = Math.abs(currentAOV - prevAvgOrderValue); const aovPercentage = (aovDiff / prevAvgOrderValue) * 100; + // Convert trend direction to numeric value for DashboardStatCard + const getNumericTrend = (trendDir, percentage) => { + if (projectionLoading && periodProgress < 100) return undefined; + if (!isFinite(percentage)) return undefined; + return trendDir === "up" ? percentage : -percentage; + }; + return (
- - - -
); @@ -458,50 +431,12 @@ const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = SummaryStats.displayName = "SummaryStats"; -// Add these skeleton components near the top of the file -const SkeletonChart = () => ( -
-
-
- {/* Grid lines */} - {[...Array(6)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(7)].map((_, i) => ( - - ))} -
- {/* Chart line */} -
-
-
-
-
-
-); +// Note: Using ChartSkeleton and TableSkeleton from @/components/dashboard/shared const SkeletonStats = () => (
{[...Array(4)].map((_, i) => ( - + @@ -515,19 +450,6 @@ const SkeletonStats = () => (
); -const SkeletonTable = () => ( -
-
- {[...Array(7)].map((_, i) => ( -
- - -
- ))} -
-
-); - const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); @@ -644,12 +566,12 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { : 0; return ( - +
- + {title}
@@ -661,9 +583,9 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { Details - + - + Daily Details
@@ -739,7 +661,7 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
-
+
@@ -960,38 +882,23 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { {loading ? (
- +
- {showDailyTable && } + {showDailyTable && }
) : error ? ( - - - Error - - Failed to load sales data: {error} - - + ) : !data.length ? ( -
-
- -
- No sales data available -
-
- Try selecting a different time range -
-
-
+ ) : ( <> -
+
{ @@ -131,15 +128,21 @@ const TimeSeriesChart = ({ content={({ active, payload, label }) => { if (!active || !payload?.length) return null; return ( -
-

+

+

{DateTime.fromISO(label).toFormat("LLL d")}

- {payload.map((entry, i) => ( -

- {entry.name}: {valueFormatter(entry.value)} -

- ))} +
+ {payload.map((entry, i) => ( +
+
+ + {entry.name} +
+ {valueFormatter(entry.value)} +
+ ))} +
); }} @@ -243,44 +246,8 @@ const OrdersDetails = ({ data }) => { ); }; -// Add additional chart components -const BarList = ({ data, valueFormatter = (v) => v }) => ( -
- {data.map((item, i) => ( -
-
- {item.name} - {valueFormatter(item.value)} -
-
-
-
-
- ))} -
-); - -const StatGrid = ({ stats }) => ( -
- {stats.map((stat, i) => ( - -
{stat.label}
-
{stat.value}
- {stat.description && ( -
- {stat.description} -
- )} -
- ))} -
-); - // Add detail view components -const AverageOrderDetails = ({ data, orderCount }) => { +const AverageOrderDetails = ({ data }) => { if (!data?.length) return (
@@ -693,9 +660,17 @@ const PeakHourDetails = ({ data }) => { hour12: true, }); return ( -
-

{time}

-

Orders: {payload[0].value}

+
+

{time}

+
+
+
+ + Orders +
+ {payload[0].value} +
+
); }} @@ -930,20 +905,26 @@ const OrderRangeDetails = ({ data }) => { if (!active || !payload?.length) return null; const data = payload[0].payload; return ( -
-

{data.range}

-

- Orders: {data.count.toLocaleString()} -

-

- Revenue: {formatCurrency(data.total)} -

-

- % of Orders: {data.percentage}% -

-

- % of Revenue: {data.revenuePercentage}% -

+
+

{data.range}

+
+
+ Orders + {data.count.toLocaleString()} +
+
+ Revenue + {formatCurrency(data.total)} +
+
+ % of Orders + {data.percentage}% +
+
+ % of Revenue + {data.revenuePercentage}% +
+
); }} @@ -962,148 +943,6 @@ const OrderRangeDetails = ({ data }) => { ); }; -const StatCard = ({ - title, - value, - description, - trend, - trendValue, - valuePrefix = "", - valueSuffix = "", - trendPrefix = "", - trendSuffix = "", - className = "", - colorClass = "text-gray-900 dark:text-gray-100", - titleClass, - descriptionClass, - icon: Icon, - iconColor = "text-gray-500", - iconBackground, - onClick, - info, - onDetailsClick, - isLoading = false, - progress, - variant = "default", - background -}) => { - const variants = { - default: "bg-white dark:bg-gray-900/60", - mini: background || "bg-gradient-to-br from-gray-800 to-gray-900 backdrop-blur-md" - }; - - const titleVariants = { - default: "text-sm font-medium text-gray-600 dark:text-gray-300", - mini: titleClass || "text-sm font-bold text-gray-100" - }; - - const valueVariants = { - default: "text-2xl font-bold", - mini: "text-3xl font-extrabold" - }; - - const descriptionVariants = { - default: "text-sm text-muted-foreground", - mini: descriptionClass || "text-sm font-semibold text-gray-200" - }; - - const trendColorVariants = { - default: { - up: "text-emerald-600 dark:text-emerald-400", - down: "text-rose-600 dark:text-rose-400" - }, - mini: { - up: "text-emerald-900", - down: "text-rose-900" - } - }; - - const iconVariants = { - default: iconColor, - mini: iconColor || colorClass - }; - - return ( - - -
- - {title} - - {info && ( - - )} -
- {Icon && ( -
- {variant === 'mini' && iconBackground && ( -
- )} - -
- )} - - - {isLoading ? ( - <> - - - - ) : ( -
-
-
- {valuePrefix} - {value} - {valueSuffix} -
- {description && ( -
- {description} - {trend && ( - - {trend === "up" ? ( - - ) : ( - - )} - {trendPrefix} - {trendValue} - {trendSuffix} - - )} -
- )} -
-
- )} -
- - ); -}; - // Add this before the StatCards component const useDataCache = () => { const [cache, setCache] = useState({}); @@ -1142,212 +981,7 @@ const MemoizedShippingDetails = memo(ShippingDetails); const MemoizedPeakHourDetails = memo(PeakHourDetails); const MemoizedCancellationsDetails = memo(CancellationsDetails); -// Add this before the StatCards component -const useDebouncedEffect = (effect, deps, delay) => { - useEffect(() => { - const handler = setTimeout(() => effect(), delay); - return () => clearTimeout(handler); - }, [...deps, delay]); -}; - -// Add these skeleton components near the top of the file -const SkeletonCard = () => ( - - -
- - -
- -
- -
- -
- - -
-
-
-
-); - -const SkeletonChart = ({ type = "line" }) => ( -
-
- {/* Grid lines */} - {[...Array(5)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(5)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {type === "bar" ? ( -
- {[...Array(24)].map((_, i) => ( -
- ))} -
- ) : ( -
-
-
-
-
- )} -
-
-); - -const SkeletonTable = ({ rows = 8 }) => ( -
-
- - - - - - - - - - - - - - - {[...Array(rows)].map((_, i) => ( - - - - - - - - - - - - ))} - -
-
-); - -const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => { - const { - totalRevenue = 0, - totalOrders = 0, - avgOrderValue = 0, - bestDay = null, - prevRevenue = 0, - prevOrders = 0, - prevAvgOrderValue = 0, - periodProgress = 100 - } = stats; - - // Calculate projected values when period is incomplete - const currentRevenue = periodProgress < 100 ? (projection?.projectedRevenue || totalRevenue) : totalRevenue; - const revenueTrend = currentRevenue >= prevRevenue ? "up" : "down"; - const revenueDiff = Math.abs(currentRevenue - prevRevenue); - const revenuePercentage = (revenueDiff / prevRevenue) * 100; - - // Calculate order trends - const currentOrders = periodProgress < 100 ? Math.round(totalOrders * (100 / periodProgress)) : totalOrders; - const ordersTrend = currentOrders >= prevOrders ? "up" : "down"; - const ordersDiff = Math.abs(currentOrders - prevOrders); - const ordersPercentage = (ordersDiff / prevOrders) * 100; - - // Calculate AOV trends - const currentAOV = currentOrders ? currentRevenue / currentOrders : avgOrderValue; - const aovTrend = currentAOV >= prevAvgOrderValue ? "up" : "down"; - const aovDiff = Math.abs(currentAOV - prevAvgOrderValue); - const aovPercentage = (aovDiff / prevAvgOrderValue) * 100; - - return ( -
- - - - - - - -
- ); -}); +// Note: Using ChartSkeleton and TableSkeleton from @/components/dashboard/shared const StatCards = ({ timeRange: initialTimeRange = "today", @@ -1727,40 +1361,37 @@ const StatCards = ({ case "revenue": case "orders": case "average_order": - return ; + return ; case "refunds": case "cancellations": case "order_range": case "pre_orders": case "local_pickup": case "on_hold": - return ; + return ; case "brands_categories": case "shipping": - return ; + return ; case "peak_hour": - return ; + return ; default: return
Loading...
; } } if (!cachedData && error) { - return ( - - - Error - Failed to load stats: {error} - - ); + return ; } - if (!cachedData) + if (!cachedData) { return ( -
- No data available for the selected time range. -
+ ); + } switch (metric) { case "revenue": @@ -1812,7 +1443,7 @@ const StatCards = ({ ].includes(selectedMetric); if (isLoading) { - return ; + return ; } switch (selectedMetric) { @@ -1862,12 +1493,12 @@ const StatCards = ({ if (loading && !stats) { return ( - +
- + {title} {description && ( @@ -1886,7 +1517,7 @@ const StatCards = ({
{[...Array(12)].map((_, i) => ( - + ))}
@@ -1896,12 +1527,12 @@ const StatCards = ({ if (error) { return ( - +
- + {title} {description && ( @@ -1933,11 +1564,7 @@ const StatCards = ({
- - - Error - Failed to load stats: {error} - + ); @@ -1951,7 +1578,7 @@ const StatCards = ({ const isSingleDay = ["today", "yesterday"].includes(timeRange); return ( - +
@@ -2009,258 +1636,179 @@ const StatCards = ({
- -
- {stats?.periodProgress < 100 ? ( -
- Proj: - Projected: - {projectionLoading ? ( -
- -
- ) : ( - formatCurrency( - projection?.projectedRevenue || stats.projectedRevenue - ) - )} -
+ subtitle={ + stats?.periodProgress < 100 ? ( + + Proj: + Projected: + {projectionLoading ? ( + ) : ( -
- Prev: - Previous: - {formatCurrency(stats.prevPeriodRevenue || 0)} -
+ formatCurrency(projection?.projectedRevenue || stats.projectedRevenue) )} -
-
+ + ) : ( + Previous: {formatCurrency(stats?.prevPeriodRevenue || 0)} + ) } - progress={ - stats?.periodProgress < 100 ? stats.periodProgress : undefined + trend={ + projectionLoading && stats?.periodProgress < 100 + ? undefined + : revenueTrend?.value + ? { value: revenueTrend.value, moreIsBetter: revenueTrend.trend === "up" } + : undefined } - trend={projectionLoading && stats?.periodProgress < 100 ? undefined : revenueTrend?.trend} - trendValue={ - projectionLoading && stats?.periodProgress < 100 ? ( -
- - -
- ) : revenueTrend?.value ? ( - - - - {formatPercentage(revenueTrend.value)} - - -

Previous Period: {formatCurrency(stats.prevPeriodRevenue || 0)}

-
-
-
- ) : null - } - colorClass="text-green-600 dark:text-green-400" icon={DollarSign} - iconColor="text-green-500" - iconBackground="bg-green-500" - onDetailsClick={() => setSelectedMetric("revenue")} - isLoading={loading || !stats} + iconColor="green" + onClick={() => setSelectedMetric("revenue")} + loading={loading || !stats} /> - setSelectedMetric("orders")} - isLoading={loading || !stats} + iconColor="blue" + onClick={() => setSelectedMetric("orders")} + loading={loading || !stats} /> - setSelectedMetric("average_order")} - isLoading={loading || !stats} + iconColor="purple" + onClick={() => setSelectedMetric("average_order")} + loading={loading || !stats} /> - setSelectedMetric("brands_categories")} - isLoading={loading || !stats} + iconColor="indigo" + onClick={() => setSelectedMetric("brands_categories")} + loading={loading || !stats} /> - setSelectedMetric("shipping")} - isLoading={loading || !stats} + iconColor="teal" + onClick={() => setSelectedMetric("shipping")} + loading={loading || !stats} /> - setSelectedMetric("pre_orders")} - isLoading={loading || !stats} + iconColor="yellow" + onClick={() => setSelectedMetric("pre_orders")} + loading={loading || !stats} /> - setSelectedMetric("local_pickup")} - isLoading={loading || !stats} + iconColor="cyan" + onClick={() => setSelectedMetric("local_pickup")} + loading={loading || !stats} /> - setSelectedMetric("on_hold")} - isLoading={loading || !stats} + iconColor="red" + onClick={() => setSelectedMetric("on_hold")} + loading={loading || !stats} /> {isSingleDay ? ( - setSelectedMetric("peak_hour")} - isLoading={loading || !stats} + iconColor="pink" + onClick={() => setSelectedMetric("peak_hour")} + loading={loading || !stats} /> ) : ( - )} - setSelectedMetric("refunds")} - isLoading={loading || !stats} + iconColor="orange" + onClick={() => setSelectedMetric("refunds")} + loading={loading || !stats} /> - setSelectedMetric("cancellations")} - isLoading={loading || !stats} + iconColor="rose" + onClick={() => setSelectedMetric("cancellations")} + loading={loading || !stats} /> - setSelectedMetric("order_range")} - isLoading={loading || !stats} + iconColor="violet" + onClick={() => setSelectedMetric("order_range")} + loading={loading || !stats} />
@@ -2289,13 +1837,9 @@ export { OrdersDetails, AverageOrderDetails, ShippingDetails, - StatCard, DetailDialog, formatCurrency, formatPercentage, - SkeletonCard, - SkeletonChart, - SkeletonTable, }; export default StatCards; diff --git a/inventory/src/components/dashboard/TypeformDashboard.jsx b/inventory/src/components/dashboard/TypeformDashboard.jsx index dd35a9d..4f8710c 100644 --- a/inventory/src/components/dashboard/TypeformDashboard.jsx +++ b/inventory/src/components/dashboard/TypeformDashboard.jsx @@ -3,7 +3,6 @@ import axios from "axios"; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; @@ -17,10 +16,17 @@ import { } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Skeleton } from "@/components/ui/skeleton"; -import { AlertCircle } from "lucide-react"; import { format } from "date-fns"; +import { CARD_STYLES } from "@/lib/dashboard/designTokens"; +import { + DashboardSectionHeader, + DashboardErrorState, + DashboardBadge, + ChartSkeleton, + TableSkeleton, + SimpleTooltip, + TOOLTIP_STYLES, +} from "@/components/dashboard/shared"; import { BarChart, Bar, @@ -30,7 +36,6 @@ import { Tooltip, ResponsiveContainer, Cell, - ReferenceLine, } from "recharts"; // Get form IDs from environment variables @@ -44,74 +49,12 @@ const FORM_NAMES = { [FORM_IDS.FORM_2]: "Winback Survey", }; -// Loading skeleton components -const SkeletonChart = () => ( -
-
- {[...Array(5)].map((_, i) => ( -
- ))} -
- {[...Array(5)].map((_, i) => ( - - ))} -
-
- {[...Array(3)].map((_, i) => ( - - ))} -
-
-
-); - -const SkeletonTable = () => ( -
- - - - - - - - - - - - - - - - {[...Array(5)].map((_, i) => ( - - - - - - - - - - - - ))} - -
-
-); - const ResponseFeed = ({ responses, title, renderSummary }) => ( - - {title} - + -
+
{responses.items.map((response) => (
{renderSummary(response)} @@ -138,24 +81,18 @@ const ProductRelevanceFeed = ({ responses }) => ( {response.hidden?.email ? ( {response.hidden?.name || "Anonymous"} ) : ( - + {response.hidden?.name || "Anonymous"} )} - + {answer?.boolean ? "Yes" : "No"} - +