From 630945e901d69ae56016d2b72c4100396ae863da Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 18 Jan 2026 16:52:00 -0500 Subject: [PATCH] Move more of dashboard to shared components --- .../components/dashboard/AircallDashboard.jsx | 160 +++--- .../dashboard/AnalyticsDashboard.jsx | 480 +++++++++--------- .../dashboard/FinancialOverview.tsx | 395 ++++++-------- .../components/dashboard/GorgiasOverview.jsx | 263 ++++------ .../components/dashboard/KlaviyoCampaigns.jsx | 408 ++++++--------- .../components/dashboard/MetaCampaigns.jsx | 392 ++++++-------- .../dashboard/RealtimeAnalytics.jsx | 112 ++-- .../src/components/dashboard/SalesChart.jsx | 390 +++++--------- .../dashboard/TypeformDashboard.jsx | 82 ++- .../dashboard/UserBehaviorDashboard.jsx | 239 ++++----- .../dashboard/shared/DashboardSkeleton.tsx | 67 ++- .../dashboard/shared/DashboardTable.tsx | 61 ++- .../src/components/dashboard/shared/index.ts | 3 + inventory/tsconfig.tsbuildinfo | 2 +- 14 files changed, 1362 insertions(+), 1692 deletions(-) diff --git a/inventory/src/components/dashboard/AircallDashboard.jsx b/inventory/src/components/dashboard/AircallDashboard.jsx index a921813..0e11ab6 100644 --- a/inventory/src/components/dashboard/AircallDashboard.jsx +++ b/inventory/src/components/dashboard/AircallDashboard.jsx @@ -8,14 +8,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { BarChart, Bar, @@ -34,10 +26,9 @@ import { DashboardStatCardSkeleton, DashboardSectionHeader, DashboardErrorState, + DashboardTable, ChartSkeleton, - TableSkeleton, CARD_STYLES, - SCROLL_STYLES, METRIC_COLORS, } from "@/components/dashboard/shared"; import { Phone, Clock, Zap, Timer } from "lucide-react"; @@ -73,47 +64,6 @@ const formatDuration = (seconds) => { return `${minutes}m ${remainingSeconds}s`; }; -const AgentPerformanceTable = ({ agents, onSort }) => { - const [sortConfig, setSortConfig] = useState({ - key: "total", - direction: "desc", - }); - - const handleSort = (key) => { - const direction = - sortConfig.key === key && sortConfig.direction === "desc" - ? "asc" - : "desc"; - setSortConfig({ key, direction }); - onSort(key, direction); - }; - - return ( - - - - Agent - 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.total} - {agent.answered} - {agent.missed} - {formatDuration(agent.average_duration)} - - ))} - -
- ); -}; - const AircallDashboard = () => { const [timeRange, setTimeRange] = useState("last7days"); const [metrics, setMetrics] = useState(null); @@ -163,6 +113,58 @@ const AircallDashboard = () => { })), }; + // Column definitions for Agent Performance table + const agentColumns = [ + { + key: "name", + header: "Agent", + render: (value) => {value}, + }, + { + key: "total", + header: "Total Calls", + align: "right", + sortable: true, + render: (value) => {value}, + }, + { + key: "answered", + header: "Answered", + align: "right", + sortable: true, + render: (value) => {value}, + }, + { + key: "missed", + header: "Missed", + align: "right", + sortable: true, + render: (value) => {value}, + }, + { + key: "average_duration", + header: "Avg Duration", + align: "right", + sortable: true, + render: (value) => {formatDuration(value)}, + }, + ]; + + // Column definitions for Missed Reasons table + const missedReasonsColumns = [ + { + key: "reason", + header: "Reason", + render: (value) => {value}, + }, + { + key: "count", + header: "Count", + align: "right", + render: (value) => {value}, + }, + ]; + const fetchData = async () => { try { setIsLoading(true); @@ -363,16 +365,17 @@ const AircallDashboard = () => { - {isLoading ? ( - - ) : ( -
- setAgentSort({ key, direction })} - /> -
- )} + agent.id} + sortConfig={agentSort} + onSort={(key, direction) => setAgentSort({ key, direction })} + maxHeight="md" + compact + />
@@ -380,32 +383,15 @@ const AircallDashboard = () => { - {isLoading ? ( - - ) : ( -
- - - - Reason - Count - - - - {chartData.missedReasons.map((reason, index) => ( - - - {reason.reason} - - - {reason.count} - - - ))} - -
-
- )} + `${reason.reason}-${index}`} + maxHeight="md" + compact + />
diff --git a/inventory/src/components/dashboard/AnalyticsDashboard.jsx b/inventory/src/components/dashboard/AnalyticsDashboard.jsx index b4fefc6..76b274b 100644 --- a/inventory/src/components/dashboard/AnalyticsDashboard.jsx +++ b/inventory/src/components/dashboard/AnalyticsDashboard.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 { Select, SelectContent, @@ -22,12 +22,14 @@ 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 { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { + DashboardSectionHeader, DashboardStatCard, + DashboardStatCardSkeleton, + DashboardChartTooltip, ChartSkeleton, DashboardEmptyState, - TOOLTIP_STYLES, } from "@/components/dashboard/shared"; // Note: Using ChartSkeleton from @/components/dashboard/shared @@ -35,15 +37,7 @@ import { const SkeletonStats = () => (
{[...Array(4)].map((_, i) => ( - - - - - - - - - + ))}
); @@ -165,224 +159,226 @@ export const AnalyticsDashboard = () => { const summaryStats = calculateSummaryStats(); - return ( - - -
-
-
- - Analytics Overview - -
-
- {loading ? ( - - ) : ( - <> - - - - - - - Daily Details -
-
- {Object.entries(metrics).map(([key, value]) => ( - - ))} -
-
-
-
-
- - - - Date - {metrics.activeUsers && ( - Active Users - )} - {metrics.newUsers && ( - New Users - )} - {metrics.pageViews && ( - Page Views - )} - {metrics.conversions && ( - Conversions - )} - - - - {data.map((day) => ( - - {formatXAxis(day.date)} - {metrics.activeUsers && ( - - {day.activeUsers.toLocaleString()} - - )} - {metrics.newUsers && ( - - {day.newUsers.toLocaleString()} - - )} - {metrics.pageViews && ( - - {day.pageViews.toLocaleString()} - - )} - {metrics.conversions && ( - - {day.conversions.toLocaleString()} - - )} - - ))} - -
-
-
-
-
- - - )} + // Time selector for DashboardSectionHeader + const timeSelector = ( + + ); + + // Header actions: Details dialog + const headerActions = !loading ? ( + + + + + + + Daily Details +
+
+ {Object.entries(metrics).map(([key, value]) => ( + + ))}
- - {loading ? ( - - ) : summaryStats ? ( -
- - - - -
- ) : null} - -
-
- - - - -
+ +
+
+ + + + Date + {metrics.activeUsers && ( + Active Users + )} + {metrics.newUsers && ( + New Users + )} + {metrics.pageViews && ( + Page Views + )} + {metrics.conversions && ( + Conversions + )} + + + + {data.map((day) => ( + + {formatXAxis(day.date)} + {metrics.activeUsers && ( + + {day.activeUsers.toLocaleString()} + + )} + {metrics.newUsers && ( + + {day.newUsers.toLocaleString()} + + )} + {metrics.pageViews && ( + + {day.pageViews.toLocaleString()} + + )} + {metrics.conversions && ( + + {day.conversions.toLocaleString()} + + )} + + ))} + +
- + +
+ ) : ; - + // Label formatter for chart tooltip + const analyticsLabelFormatter = (label) => { + const date = label instanceof Date ? label : new Date(label); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + }; + + return ( + + + + + {/* Stats cards */} + {loading ? ( + + ) : summaryStats ? ( +
+ + + + +
+ ) : null} + + {/* Metric toggles */} +
+
+ + + + +
+
{loading ? ( ) : !data.length ? ( @@ -420,34 +416,12 @@ export const AnalyticsDashboard = () => { 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()} - -
- ))} -
-
- ); - }} + content={ + value.toLocaleString()} + /> + } /> {metrics.activeUsers && ( diff --git a/inventory/src/components/dashboard/FinancialOverview.tsx b/inventory/src/components/dashboard/FinancialOverview.tsx index 9169b20..426e647 100644 --- a/inventory/src/components/dashboard/FinancialOverview.tsx +++ b/inventory/src/components/dashboard/FinancialOverview.tsx @@ -1,11 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { acotService } from "@/services/dashboard/acotService"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Select, @@ -42,7 +37,6 @@ import { YAxis, } from "recharts"; import type { TooltipProps } from "recharts"; -import { Skeleton } from "@/components/ui/skeleton"; import { TrendingUp, DollarSign, Package, PiggyBank, Percent } from "lucide-react"; import PeriodSelectionPopover, { type QuickPreset, @@ -50,8 +44,10 @@ import PeriodSelectionPopover, { import type { CustomPeriod, NaturalLanguagePeriodResult } from "@/utils/naturalLanguagePeriod"; import { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { + DashboardSectionHeader, DashboardStatCard, DashboardStatCardSkeleton, + ChartSkeleton, DashboardEmptyState, DashboardErrorState, TOOLTIP_STYLES, @@ -1102,161 +1098,21 @@ const FinancialOverview = () => { }; - return ( - - -
-
-
- - Profit & Loss Overview - -
-
- {!error && ( - <> - - - - - - - - - Financial Details - -
-
- {SERIES_DEFINITIONS.map((series) => ( - - ))} -
- - -
-
-
-
- - - - - Date - - {metrics.income && ( - - Total Income - - )} - {metrics.cogs && ( - - COGS - - )} - {metrics.cogsPercentage && ( - - COGS % of Income - - )} - {metrics.profit && ( - - Gross Profit - - )} - {metrics.margin && ( - - Margin - - )} - - - - {detailRows.map((row) => ( - - - {row.label || "—"} - - {metrics.income && ( - - {row.isFuture ? "—" : formatCurrency(row.income, 0)} - - )} - {metrics.cogs && ( - - {row.isFuture ? "—" : formatCurrency(row.cogs, 0)} - - )} - {metrics.cogsPercentage && ( - - {row.isFuture ? "—" : formatPercentage(row.cogsPercentage, 1)} - - )} - {metrics.profit && ( - - {row.isFuture ? "—" : formatCurrency(row.profit, 0)} - - )} - {metrics.margin && ( - - {row.isFuture ? "—" : formatPercentage(row.margin, 1)} - - )} - - ))} - -
-
-
-
-
- - - - - )} -
-
- - {/* Show stats only if not in error state */} - {!error && - (loading ? ( - - ) : ( - cards.length > 0 && - ))} - - - {/* Show metric toggles only if not in error state */} - {!error && ( -
+ // Header actions: Details dialog and Period selector + const headerActions = !error ? ( + <> + + + + + + + + Financial Details + +
{SERIES_DEFINITIONS.map((series) => ( ))}
+ + +
+
+
+
+ + + + + Date + + {metrics.income && ( + + Total Income + + )} + {metrics.cogs && ( + + COGS + + )} + {metrics.cogsPercentage && ( + + COGS % of Income + + )} + {metrics.profit && ( + + Gross Profit + + )} + {metrics.margin && ( + + Margin + + )} + + + + {detailRows.map((row) => ( + + + {row.label || "—"} + + {metrics.income && ( + + {row.isFuture ? "—" : formatCurrency(row.income, 0)} + + )} + {metrics.cogs && ( + + {row.isFuture ? "—" : formatCurrency(row.cogs, 0)} + + )} + {metrics.cogsPercentage && ( + + {row.isFuture ? "—" : formatPercentage(row.cogsPercentage, 1)} + + )} + {metrics.profit && ( + + {row.isFuture ? "—" : formatCurrency(row.profit, 0)} + + )} + {metrics.margin && ( + + {row.isFuture ? "—" : formatPercentage(row.margin, 1)} + + )} + + ))} + +
+
+
+
+
- - + + + ) : null; -
+ return ( + + + + + {/* Show stats only if not in error state */} + {!error && ( + loading ? ( + + ) : ( + cards.length > 0 && + ) + )} + + {/* Show metric toggles only if not in error state */} + {!error && ( +
+
+ {SERIES_DEFINITIONS.map((series) => ( + + ))} +
+ + + + +
Group By:
-
+ +
- )} -
- - +
+ )} {loading ? (
- +
) : error ? ( @@ -1496,54 +1484,9 @@ function SkeletonStats() { ); } -function SkeletonChart() { +function SkeletonChartSection() { return ( -
-
-
- {/* Grid lines */} - {[...Array(6)].map((_, i) => ( -
- ))} - {/* Y-axis labels */} -
- {[...Array(6)].map((_, i) => ( - - ))} -
- {/* X-axis labels */} -
- {[...Array(7)].map((_, i) => ( - - ))} -
- {/* Chart area */} -
-
- {/* Simulated line chart */} -
-
-
-
-
+ ); } diff --git a/inventory/src/components/dashboard/GorgiasOverview.jsx b/inventory/src/components/dashboard/GorgiasOverview.jsx index 413fdb3..1c98e3d 100644 --- a/inventory/src/components/dashboard/GorgiasOverview.jsx +++ b/inventory/src/components/dashboard/GorgiasOverview.jsx @@ -7,15 +7,6 @@ import { SelectItem, SelectValue, } from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Skeleton } from "@/components/ui/skeleton"; import { Mail, Send, @@ -34,6 +25,7 @@ import { DashboardStatCardSkeleton, DashboardSectionHeader, DashboardErrorState, + DashboardTable, } from "@/components/dashboard/shared"; const TIME_RANGES = { @@ -51,17 +43,15 @@ const formatDuration = (seconds) => { }; const getDateRange = (days) => { - // Create date in Eastern Time const now = new Date(); const easternTime = new Date( now.toLocaleString("en-US", { timeZone: "America/New_York" }) ); - + if (days === "today") { - // For today, set the range to be the current day in Eastern Time const start = new Date(easternTime); start.setHours(0, 0, 0, 0); - + const end = new Date(easternTime); end.setHours(23, 59, 59, 999); @@ -70,43 +60,40 @@ const getDateRange = (days) => { end_datetime: end.toISOString() }; } - - // For other periods, calculate from end of previous day + const end = new Date(easternTime); end.setHours(23, 59, 59, 999); - + const start = new Date(easternTime); start.setDate(start.getDate() - Number(days)); start.setHours(0, 0, 0, 0); - + return { start_datetime: start.toISOString(), end_datetime: end.toISOString() }; }; -const TableSkeleton = () => ( - - - - - - - - - - - {[...Array(5)].map((_, i) => ( - - - - - - - ))} - -
-); +// Trend cell component with arrow and color +const TrendCell = ({ delta }) => { + if (delta === 0) return null; + + const isPositive = delta > 0; + const colorClass = isPositive + ? "text-green-600 dark:text-green-500" + : "text-red-600 dark:text-red-500"; + + return ( +
+ {isPositive ? ( + + ) : ( + + )} + {Math.abs(delta)}% +
+ ); +}; const GorgiasOverview = () => { const [timeRange, setTimeRange] = useState("7"); @@ -153,7 +140,6 @@ const GorgiasOverview = () => { useEffect(() => { loadStats(); - // Set up auto-refresh every 5 minutes const interval = setInterval(loadStats, 5 * 60 * 1000); return () => clearInterval(interval); }, [loadStats]); @@ -183,21 +169,79 @@ const GorgiasOverview = () => { }, {}); // Process channel data - const channels = data.channels?.map(line => ({ + const channels = (data.channels?.map(line => ({ name: line[0]?.value || '', total: line[1]?.value || 0, percentage: line[2]?.value || 0, delta: line[3]?.value || 0 - })) || []; + })) || []).sort((a, b) => b.total - a.total); // Process agent data - const agents = data.agents?.map(line => ({ + const agents = (data.agents?.map(line => ({ name: line[0]?.value || '', closed: line[1]?.value || 0, rating: line[2]?.value, percentage: line[3]?.value || 0, delta: line[4]?.value || 0 - })) || []; + })) || []).filter(agent => agent.name !== "Unassigned"); + + // Column definitions for Channel Distribution table + const channelColumns = [ + { + key: "name", + header: "Channel", + render: (value) => {value}, + }, + { + key: "total", + header: "Total", + align: "right", + render: (value) => {value}, + }, + { + key: "percentage", + header: "%", + align: "right", + render: (value) => {value}%, + }, + { + key: "delta", + header: "Change", + align: "right", + render: (value) => , + }, + ]; + + // Column definitions for Agent Performance table + const agentColumns = [ + { + key: "name", + header: "Agent", + render: (value) => {value}, + }, + { + key: "closed", + header: "Closed", + align: "right", + render: (value) => {value}, + }, + { + key: "rating", + header: "Rating", + align: "right", + render: (value) => ( + + {value ? `${value}/5` : "-"} + + ), + }, + { + key: "delta", + header: "Change", + align: "right", + render: (value) => , + }, + ]; if (error) { return ( @@ -245,7 +289,6 @@ const GorgiasOverview = () => {
- {/* Message & Response Metrics */} {loading ? ( [...Array(7)].map((_, i) => ( @@ -341,120 +384,32 @@ const GorgiasOverview = () => { {/* Channel Distribution */} - - {loading ? ( - - ) : ( - - - - Channel - Total - % - Change - - - - {channels - .sort((a, b) => b.total - a.total) - .map((channel, index) => ( - - - {channel.name} - - - {channel.total} - - - {channel.percentage}% - - 0 - ? "text-green-600 dark:text-green-500" - : channel.delta < 0 - ? "text-red-600 dark:text-red-500" - : "text-muted-foreground" - }`} - > -
- {channel.delta !== 0 && ( - <> - {channel.delta > 0 ? ( - - ) : ( - - )} - {Math.abs(channel.delta)}% - - )} -
-
-
- ))} -
-
- )} + + `${channel.name}-${index}`} + maxHeight="md" + compact + />
{/* Agent Performance */} - - {loading ? ( - - ) : ( - - - - Agent - Closed - Rating - Change - - - - {agents - .filter((agent) => agent.name !== "Unassigned") - .map((agent, index) => ( - - - {agent.name} - - - {agent.closed} - - - {agent.rating ? `${agent.rating}/5` : "-"} - - 0 - ? "text-green-600 dark:text-green-500" - : agent.delta < 0 - ? "text-red-600 dark:text-red-500" - : "text-muted-foreground" - }`} - > -
- {agent.delta !== 0 && ( - <> - {agent.delta > 0 ? ( - - ) : ( - - )} - {Math.abs(agent.delta)}% - - )} -
-
-
- ))} -
-
- )} + + `${agent.name}-${index}`} + maxHeight="md" + compact + />
@@ -463,4 +418,4 @@ const GorgiasOverview = () => { ); }; -export default GorgiasOverview; \ No newline at end of file +export default GorgiasOverview; diff --git a/inventory/src/components/dashboard/KlaviyoCampaigns.jsx b/inventory/src/components/dashboard/KlaviyoCampaigns.jsx index 338c2ad..d0cddc3 100644 --- a/inventory/src/components/dashboard/KlaviyoCampaigns.jsx +++ b/inventory/src/components/dashboard/KlaviyoCampaigns.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Tooltip, @@ -17,11 +17,12 @@ import { import { Button } from "@/components/ui/button"; import { TIME_RANGES } from "@/lib/dashboard/constants"; import { Mail, MessageSquare, BookOpen } from "lucide-react"; -import { Skeleton } from "@/components/ui/skeleton"; import { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { DashboardSectionHeader, DashboardErrorState, + DashboardTable, + TableSkeleton, } from "@/components/dashboard/shared"; // Helper functions for formatting @@ -41,83 +42,8 @@ const formatCurrency = (value) => { }).format(value); }; -// Loading skeleton component -const TableSkeleton = () => ( - - - - - - - - - - - - - {[...Array(15)].map((_, i) => ( - - - - - - - - - ))} - -
- - - - - - - - - - - -
-
- -
- - - -
-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-); - - -// MetricCell component for displaying campaign metrics -const MetricCell = ({ +// MetricCell content component for displaying campaign metrics (returns content, not ) +const MetricCellContent = ({ value, count, isMonetary = false, @@ -128,15 +54,15 @@ const MetricCell = ({ }) => { if (isSMS && hideForSMS) { return ( - +
N/A
-
- +
); } return ( - +
{isMonetary ? formatCurrency(value) : formatRate(value, isSMS, hideForSMS)}
@@ -146,7 +72,56 @@ const MetricCell = ({ totalRecipients > 0 && ` (${((count / totalRecipients) * 100).toFixed(2)}%)`}
- +
+ ); +}; + +// Campaign name cell with tooltip +const CampaignCell = ({ campaign }) => { + const isBlog = campaign.name?.includes("_Blog"); + const isSMS = campaign.channel === 'sms'; + + return ( + + + +
+
+ {isBlog ? ( + + ) : isSMS ? ( + + ) : ( + + )} +
+ {campaign.name} +
+
+
+ {campaign.subject} +
+
+ {campaign.send_time + ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED) + : "No date"} +
+
+
+ +

{campaign.name}

+

{campaign.subject}

+

+ {campaign.send_time + ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED) + : "No date"} +

+
+
+
); }; @@ -154,7 +129,6 @@ const KlaviyoCampaigns = ({ className }) => { const [campaigns, setCampaigns] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); const [selectedChannels, setSelectedChannels] = useState({ email: true, sms: true, blog: true }); const [selectedTimeRange, setSelectedTimeRange] = useState("last7days"); const [sortConfig, setSortConfig] = useState({ @@ -162,11 +136,8 @@ const KlaviyoCampaigns = ({ className }) => { direction: "desc", }); - const handleSort = (key) => { - setSortConfig((prev) => ({ - key, - direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc", - })); + const handleSort = (key, direction) => { + setSortConfig({ key, direction }); }; const fetchCampaigns = async () => { @@ -175,11 +146,11 @@ const KlaviyoCampaigns = ({ className }) => { const response = await fetch( `/api/klaviyo/reporting/campaigns/${selectedTimeRange}` ); - + if (!response.ok) { throw new Error(`Failed to fetch campaigns: ${response.status}`); } - + const data = await response.json(); setCampaigns(data.data || []); setError(null); @@ -193,14 +164,14 @@ const KlaviyoCampaigns = ({ className }) => { useEffect(() => { fetchCampaigns(); - const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); // Refresh every 10 minutes + const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); return () => clearInterval(interval); - }, [selectedTimeRange]); // Only refresh when time range changes + }, [selectedTimeRange]); // Sort campaigns const sortedCampaigns = [...campaigns].sort((a, b) => { const direction = sortConfig.direction === "desc" ? -1 : 1; - + switch (sortConfig.key) { case "send_time": return direction * (DateTime.fromISO(a.send_time) - DateTime.fromISO(b.send_time)); @@ -219,16 +190,100 @@ const KlaviyoCampaigns = ({ className }) => { } }); - // Filter campaigns by search term and channels + // Filter campaigns by channels const filteredCampaigns = sortedCampaigns.filter( (campaign) => { const isBlog = campaign?.name?.includes("_Blog"); const channelType = isBlog ? "blog" : campaign?.channel; - return campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase()) && - selectedChannels[channelType]; + return selectedChannels[channelType]; } ); + // Column definitions for DashboardTable + const columns = [ + { + key: "name", + header: "Campaign", + sortable: true, + sortKey: "send_time", + render: (_, campaign) => , + }, + { + key: "delivery_rate", + header: "Delivery", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "open_rate", + header: "Opens", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "click_rate", + header: "Clicks", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "click_to_open_rate", + header: "CTR", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "conversion_value", + header: "Orders", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + ]; + if (isLoading) { return ( @@ -240,7 +295,7 @@ const KlaviyoCampaigns = ({ className }) => { timeSelector={
} /> - + ); @@ -316,153 +371,20 @@ const KlaviyoCampaigns = ({ className }) => { } /> - - - - - - - - - - - - - - {filteredCampaigns.map((campaign) => ( - - - - - - - -

{campaign.name}

-

{campaign.subject}

-

- {campaign.send_time - ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED) - : "No date"} -

-
- - - - - - - - - ))} - -
- - - - - - - - - - - -
-
- {campaign.name?.includes("_Blog") ? ( - - ) : campaign.channel === 'sms' ? ( - - ) : ( - - )} -
- {campaign.name} -
-
-
- {campaign.subject} -
-
- {campaign.send_time - ? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED) - : "No date"} -
-
+ + campaign.id} + sortConfig={sortConfig} + onSort={handleSort} + maxHeight="md" + stickyHeader + bordered + /> ); }; -export default KlaviyoCampaigns; \ No newline at end of file +export default KlaviyoCampaigns; diff --git a/inventory/src/components/dashboard/MetaCampaigns.jsx b/inventory/src/components/dashboard/MetaCampaigns.jsx index 40f498a..7eb1984 100644 --- a/inventory/src/components/dashboard/MetaCampaigns.jsx +++ b/inventory/src/components/dashboard/MetaCampaigns.jsx @@ -19,14 +19,14 @@ import { ShoppingCart, MessageCircle, } 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, + DashboardTable, + TableSkeleton, } from "@/components/dashboard/shared"; // Helper functions for formatting @@ -48,8 +48,8 @@ const formatNumber = (value, decimalPlaces = 0) => { const formatPercent = (value, decimalPlaces = 2) => `${(value || 0).toFixed(decimalPlaces)}%`; - -const MetricCell = ({ value, label, sublabel, isMonetary = false, isPercentage = false, decimalPlaces = 0 }) => { +// MetricCell content component (returns content, not ) +const MetricCellContent = ({ value, label, sublabel, isMonetary = false, isPercentage = false, decimalPlaces = 0 }) => { const formattedValue = isMonetary ? formatCurrency(value, decimalPlaces) : isPercentage @@ -57,7 +57,7 @@ const MetricCell = ({ value, label, sublabel, isMonetary = false, isPercentage = : formatNumber(value, decimalPlaces); return ( - +
{formattedValue}
@@ -66,7 +66,7 @@ const MetricCell = ({ value, label, sublabel, isMonetary = false, isPercentage = {label || sublabel}
)} - +
); }; @@ -84,16 +84,27 @@ const getActionValue = (campaign, actionType) => { return 0; }; -const CampaignName = ({ name }) => { - if (name.startsWith("Instagram post: ")) { - return ( -
- - {name.replace("Instagram post: ", "")} +const CampaignNameCell = ({ campaign }) => { + const name = campaign.name; + const isInstagram = name.startsWith("Instagram post: "); + + return ( +
+
+ {isInstagram ? ( +
+ + {name.replace("Instagram post: ", "")} +
+ ) : ( + {name} + )}
- ); - } - return {name}; +
+ {campaign.objective} +
+
+ ); }; const getObjectiveAction = (campaignObjective) => { @@ -138,7 +149,6 @@ const processMetrics = (campaign) => { const cpm = parseFloat(insights.cpm || 0); const frequency = parseFloat(insights.frequency || 0); - // Purchase value and total purchases const purchaseValue = (insights.action_values || []) .filter(({ action_type }) => action_type === "purchase") .reduce((sum, { value }) => sum + parseFloat(value || 0), 0); @@ -147,7 +157,6 @@ const processMetrics = (campaign) => { .filter(({ action_type }) => action_type === "purchase") .reduce((sum, { value }) => sum + parseInt(value || 0), 0); - // Aggregate unique actions const actionMap = new Map(); (insights.actions || []).forEach(({ action_type, value }) => { const currentValue = actionMap.get(action_type) || 0; @@ -159,13 +168,11 @@ const processMetrics = (campaign) => { value, })); - // Map of cost per action const costPerActionMap = new Map(); (insights.cost_per_action_type || []).forEach(({ action_type, value }) => { costPerActionMap.set(action_type, parseFloat(value || 0)); }); - // Total post engagements const totalPostEngagements = actionMap.get("post_engagement") || 0; return { @@ -190,7 +197,6 @@ const processCampaignData = (campaign) => { const budget = calculateBudget(campaign); const { action_type, label } = getObjectiveAction(campaign.objective); - // Get cost per result from costPerActionMap const costPerResult = metrics.costPerActionMap.get(action_type) || 0; return { @@ -208,49 +214,6 @@ const processCampaignData = (campaign) => { }; }; -const SkeletonTable = () => ( -
- - - - - {[...Array(8)].map((_, i) => ( - - ))} - - - - {[...Array(5)].map((_, rowIndex) => ( - - - {[...Array(8)].map((_, colIndex) => ( - - ))} - - ))} - -
- - - -
-
- -
- - - -
-
-
-
- - -
-
-
-); - const MetaCampaigns = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -262,30 +225,24 @@ const MetaCampaigns = () => { direction: "desc", }); - const handleSort = (key) => { - setSortConfig((prev) => ({ - key, - direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc", - })); + const handleSort = (key, direction) => { + setSortConfig({ key, direction }); }; const computeDateRange = (timeframe) => { - // Create date in Eastern Time const now = new Date(); const easternTime = new Date( now.toLocaleString("en-US", { timeZone: "America/New_York" }) ); - easternTime.setHours(0, 0, 0, 0); // Set to start of day + easternTime.setHours(0, 0, 0, 0); let sinceDate, untilDate; if (timeframe === "today") { - // For today, both dates should be the current date in Eastern Time sinceDate = untilDate = new Date(easternTime); } else { - // For other periods, calculate the date range untilDate = new Date(easternTime); - untilDate.setDate(untilDate.getDate() - 1); // Yesterday + untilDate.setDate(untilDate.getDate() - 1); sinceDate = new Date(untilDate); sinceDate.setDate(sinceDate.getDate() - parseInt(timeframe) + 1); @@ -315,7 +272,6 @@ const MetaCampaigns = () => { accountInsights.json() ]); - // Process campaigns with the new processing logic const processedCampaigns = campaignsJson.map(processCampaignData); const activeCampaigns = processedCampaigns.filter(c => c.metrics.spend > 0); setCampaigns(activeCampaigns); @@ -328,7 +284,7 @@ const MetaCampaigns = () => { const totalPurchaseValue = activeCampaigns.reduce((sum, camp) => sum + camp.metrics.purchaseValue, 0); const totalLinkClicks = activeCampaigns.reduce((sum, camp) => sum + camp.metrics.clicks, 0); const totalPostEngagements = activeCampaigns.reduce((sum, camp) => sum + camp.metrics.totalPostEngagements, 0); - + const numCampaigns = activeCampaigns.length; const avgFrequency = activeCampaigns.reduce((sum, camp) => sum + camp.metrics.frequency, 0) / numCampaigns; const avgCpm = activeCampaigns.reduce((sum, camp) => sum + camp.metrics.cpm, 0) / numCampaigns; @@ -364,10 +320,9 @@ const MetaCampaigns = () => { // Sort campaigns const sortedCampaigns = [...campaigns].sort((a, b) => { const direction = sortConfig.direction === "desc" ? -1 : 1; - + switch (sortConfig.key) { case "date": - // Add date sorting using campaign ID (Meta IDs are chronological) return direction * (parseInt(b.id) - parseInt(a.id)); case "spend": return direction * ((a.metrics.spend || 0) - (b.metrics.spend || 0)); @@ -390,6 +345,118 @@ const MetaCampaigns = () => { } }); + // Column definitions for DashboardTable + const columns = [ + { + key: "name", + header: "Campaign", + sortable: true, + sortKey: "date", + render: (_, campaign) => , + }, + { + key: "spend", + header: "Spend", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "reach", + header: "Reach", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "impressions", + header: "Impressions", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "cpm", + header: "CPM", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "ctr", + header: "CTR", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "results", + header: "Results", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "value", + header: "Value", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + { + key: "engagements", + header: "Engagements", + align: "center", + sortable: true, + render: (_, campaign) => ( + + ), + }, + ]; + if (loading) { return ( @@ -407,7 +474,7 @@ const MetaCampaigns = () => {
- +
); @@ -535,165 +602,20 @@ const MetaCampaigns = () => {
- - - - - - - - - - - - - - - - - {sortedCampaigns.map((campaign) => ( - - - - - - - - - - - - - - - - - - - - ))} - -
- - - - - - - - - - - - - - - - - -
-
-
- -
-
- {campaign.objective} -
-
-
+ + campaign.id} + sortConfig={sortConfig} + onSort={handleSort} + maxHeight="md" + stickyHeader + bordered + /> ); }; -export default MetaCampaigns; \ No newline at end of file +export default MetaCampaigns; diff --git a/inventory/src/components/dashboard/RealtimeAnalytics.jsx b/inventory/src/components/dashboard/RealtimeAnalytics.jsx index d00490c..12ccc54 100644 --- a/inventory/src/components/dashboard/RealtimeAnalytics.jsx +++ b/inventory/src/components/dashboard/RealtimeAnalytics.jsx @@ -15,14 +15,6 @@ import { TooltipProvider, } from "@/components/ui/tooltip"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Table, - TableHeader, - TableHead, - TableBody, - TableRow, - TableCell, -} from "@/components/ui/table"; import { format } from "date-fns"; // Import shared components and tokens @@ -30,13 +22,13 @@ import { DashboardChartTooltip, DashboardSectionHeader, DashboardStatCard, + DashboardTable, StatCardSkeleton, ChartSkeleton, TableSkeleton, DashboardErrorState, CARD_STYLES, TYPOGRAPHY, - SCROLL_STYLES, METRIC_COLORS, } from "@/components/dashboard/shared"; @@ -344,6 +336,36 @@ export const RealtimeAnalytics = () => { }; }, [isPaused]); + // Column definitions for pages table + const pagesColumns = [ + { + key: "path", + header: "Page", + render: (value) => {value}, + }, + { + key: "activeUsers", + header: "Active Users", + align: "right", + render: (value) => {value}, + }, + ]; + + // Column definitions for sources table + const sourcesColumns = [ + { + key: "source", + header: "Source", + render: (value) => {value}, + }, + { + key: "activeUsers", + header: "Active Users", + align: "right", + render: (value) => {value}, + }, + ]; + if (loading && !basicData && !detailedData) { return ( @@ -448,64 +470,28 @@ export const RealtimeAnalytics = () => { -
- - - - - Page - - - Active Users - - - - - {detailedData.currentPages.map((page, index) => ( - - - {page.path} - - - {page.activeUsers} - - - ))} - -
+
+ `${page.path}-${index}`} + maxHeight="sm" + compact + />
-
- - - - - Source - - - Active Users - - - - - {detailedData.sources.map((source, index) => ( - - - {source.source} - - - {source.activeUsers} - - - ))} - -
+
+ `${source.source}-${index}`} + maxHeight="sm" + compact + />
diff --git a/inventory/src/components/dashboard/SalesChart.jsx b/inventory/src/components/dashboard/SalesChart.jsx index 585b3ce..85b0e52 100644 --- a/inventory/src/components/dashboard/SalesChart.jsx +++ b/inventory/src/components/dashboard/SalesChart.jsx @@ -1,13 +1,6 @@ -import React, { useState, useEffect, useMemo, useCallback, memo } from "react"; -import axios from "axios"; +import React, { useState, useEffect, useCallback, memo } from "react"; import { acotService } from "@/services/dashboard/acotService"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Select, SelectContent, @@ -15,16 +8,8 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; -import { - Loader2, - TrendingUp, - TrendingDown, - Info, - AlertCircle, -} from "lucide-react"; +import { TrendingUp } from "lucide-react"; import { LineChart, Line, @@ -36,13 +21,7 @@ import { Legend, ReferenceLine, } from "recharts"; -import { - TIME_RANGES, - GROUP_BY_OPTIONS, - formatDateForInput, - parseDateFromInput, -} from "@/lib/dashboard/constants"; -import { Checkbox } from "@/components/ui/checkbox"; +import { TIME_RANGES } from "@/lib/dashboard/constants"; import { Table, TableHeader, @@ -51,75 +30,26 @@ import { TableBody, TableCell, } from "@/components/ui/table"; -import { debounce } from "lodash"; import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { Separator } from "@/components/ui/separator"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - CARD_STYLES, - TYPOGRAPHY, - METRIC_COLORS as SHARED_METRIC_COLORS, -} from "@/lib/dashboard/designTokens"; +import { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { + DashboardSectionHeader, DashboardStatCard, + DashboardStatCardSkeleton, + DashboardChartTooltip, ChartSkeleton, - TableSkeleton, DashboardEmptyState, DashboardErrorState, - TOOLTIP_STYLES, } from "@/components/dashboard/shared"; -const METRIC_IDS = { - PLACED_ORDER: "Y8cqcF", - PAYMENT_REFUNDED: "R7XUYh", -}; - -// Map current periods to their previous equivalents -const PREVIOUS_PERIOD_MAP = { - today: "yesterday", - thisWeek: "lastWeek", - thisMonth: "lastMonth", - last7days: "previous7days", - last30days: "previous30days", - last90days: "previous90days", - yesterday: "twoDaysAgo", -}; - -// Add helper function to calculate previous period dates -const calculatePreviousPeriodDates = (timeRange, startDate, endDate) => { - if (timeRange && timeRange !== "custom") { - return { - timeRange: PREVIOUS_PERIOD_MAP[timeRange], - }; - } else if (startDate && endDate) { - const start = new Date(startDate); - const end = new Date(endDate); - const duration = end.getTime() - start.getTime(); - - const prevEnd = new Date(start.getTime() - 1); - const prevStart = new Date(prevEnd.getTime() - duration); - - return { - startDate: prevStart.toISOString(), - endDate: prevEnd.toISOString(), - }; - } - return null; -}; - // Move formatCurrency to top and export it export const formatCurrency = (value, minimumFractionDigits = 0) => { if (!value || isNaN(value)) return "$0"; @@ -131,60 +61,23 @@ export const formatCurrency = (value, minimumFractionDigits = 0) => { }).format(value); }; -// Add a helper function for percentage formatting -const formatPercentage = (value) => { - if (typeof value !== "number") return "0%"; - return `${Math.abs(Math.round(value))}%`; -}; - -// Add color mapping for metrics - using shared tokens where applicable -const METRIC_COLORS = { - 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 -}; - -// 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", - }); - - return ( -
-

{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} -
- ); - })} -
-
- ); +// Sales chart tooltip formatter - formats revenue/AOV as currency, others as numbers +const salesValueFormatter = (value, name) => { + const nameLower = (name || "").toLowerCase(); + if (nameLower.includes('revenue') || nameLower.includes('order value') || nameLower.includes('average')) { + return formatCurrency(value); } - return null; + return typeof value === 'number' ? value.toLocaleString() : value; +}; + +// Sales chart label formatter - formats timestamp as readable date +const salesLabelFormatter = (label) => { + const date = new Date(label); + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); }; const calculate7DayAverage = (data) => { @@ -434,18 +327,9 @@ SummaryStats.displayName = "SummaryStats"; // Note: Using ChartSkeleton and TableSkeleton from @/components/dashboard/shared const SkeletonStats = () => ( -
+
{[...Array(4)].map((_, i) => ( - - - - - - - - - - + ))}
); @@ -565,19 +449,28 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { ? data.reduce((sum, day) => sum + day.revenue, 0) / data.length : 0; - return ( - - -
-
-
- - {title} - -
-
- {!error && ( - + // Time selector for DashboardSectionHeader + const timeSelector = ( + + ); + + // Actions (Details dialog) for DashboardSectionHeader + const headerActions = !error ? ( +
- )} - -
-
+ ) : null; - {/* Show stats only if not in error state */} - {!error && - (loading ? ( - - ) : ( - - ))} + return ( + + - {/* Show metric toggles only if not in error state */} - {!error && ( -
-
- - - - -
- - - + + {/* Show stats only if not in error state */} + {!error && ( + loading ? ( + + ) : ( + + ) + )} + {/* Show metric toggles only if not in error state */} + {!error && ( +
+
+ + +
- )} -
- - + + + + +
+ )} {loading ? (
@@ -927,7 +809,7 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => { className="text-xs text-muted-foreground" tick={{ fill: "currentColor" }} /> - } /> + } /> (
{(reasonsAnswer?.choices?.labels || []).map((label, idx) => ( - + {label} - + ))} {reasonsAnswer?.choices?.other && ( - + {reasonsAnswer.choices.other} - + )}
{feedbackAnswer?.text && ( @@ -325,6 +317,28 @@ const TypeformDashboard = () => { const newestResponse = getNewestResponse(); + // Column definitions for reasons table + const reasonsColumns = [ + { + key: "reason", + header: "Reason", + render: (value) => {value}, + }, + { + key: "count", + header: "Count", + align: "right", + render: (value) => {value}, + }, + { + key: "percentage", + header: "%", + align: "right", + width: "w-[80px]", + render: (value) => {value}%, + }, + ]; + if (error) { return ( @@ -554,41 +568,13 @@ const TypeformDashboard = () => { -
- - - - - Reason - - - Count - - - % - - - - - {metrics.winback.reasons.map((reason, index) => ( - - - {reason.reason} - - - {reason.count} - - - {reason.percentage}% - - - ))} - -
-
+ `${reason.reason}-${index}`} + maxHeight="md" + compact + />
diff --git a/inventory/src/components/dashboard/UserBehaviorDashboard.jsx b/inventory/src/components/dashboard/UserBehaviorDashboard.jsx index 1fd3697..6de6cac 100644 --- a/inventory/src/components/dashboard/UserBehaviorDashboard.jsx +++ b/inventory/src/components/dashboard/UserBehaviorDashboard.jsx @@ -8,14 +8,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { PieChart, Pie, @@ -26,6 +18,8 @@ import { import { CARD_STYLES } from "@/lib/dashboard/designTokens"; import { DashboardSectionHeader, + DashboardTable, + DashboardChartTooltip, TableSkeleton, ChartSkeleton, TOOLTIP_STYLES, @@ -104,10 +98,7 @@ export const UserBehaviorDashboard = () => { throw new Error("Invalid response structure"); } - // Handle both data structures const rawData = result.data?.data || result.data; - - // Try to access the data differently based on the structure const pageResponse = rawData?.pageResponse || rawData?.reports?.[0]; const deviceResponse = rawData?.deviceResponse || rawData?.reports?.[1]; const sourceResponse = rawData?.sourceResponse || rawData?.reports?.[2]; @@ -122,7 +113,7 @@ export const UserBehaviorDashboard = () => { sourceData: processSourceData(sourceResponse), }, }; - + setData(processed); } catch (error) { console.error("Failed to fetch behavior data:", error); @@ -133,6 +124,74 @@ export const UserBehaviorDashboard = () => { fetchData(); }, [timeRange]); + const formatDuration = (seconds) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; + }; + + // Column definitions for pages table + const pagesColumns = [ + { + key: "path", + header: "Page Path", + render: (value) => {value}, + }, + { + key: "pageViews", + header: "Views", + align: "right", + render: (value) => {value.toLocaleString()}, + }, + { + key: "bounceRate", + header: "Bounce Rate", + align: "right", + render: (value) => {value.toFixed(1)}%, + }, + { + key: "avgSessionDuration", + header: "Avg. Duration", + align: "right", + render: (value) => {formatDuration(value)}, + }, + ]; + + // Column definitions for sources table + const sourcesColumns = [ + { + key: "source", + header: "Source", + width: "w-[35%] min-w-[120px]", + render: (value) => {value}, + }, + { + key: "sessions", + header: "Sessions", + align: "right", + width: "w-[20%] min-w-[80px]", + render: (value) => {value.toLocaleString()}, + }, + { + key: "conversions", + header: "Conv.", + align: "right", + width: "w-[20%] min-w-[80px]", + render: (value) => {value.toLocaleString()}, + }, + { + key: "conversionRate", + header: "Conv. Rate", + align: "right", + width: "w-[25%] min-w-[80px]", + render: (_, row) => ( + + {((row.conversions / row.sessions) * 100).toFixed(1)}% + + ), + }, + ]; + if (loading) { return ( @@ -180,41 +239,33 @@ export const UserBehaviorDashboard = () => { 0 ); - const CustomTooltip = ({ active, payload }) => { - if (active && payload && payload.length) { - const data = payload[0].payload; - const percentage = ((data.pageViews / totalViews) * 100).toFixed(1); - const sessionPercentage = ((data.sessions / totalSessions) * 100).toFixed(1); - const color = COLORS[data.device.toLowerCase()]; - return ( -
-

{data.device}

-
-
-
- - Views -
- {data.pageViews.toLocaleString()} ({percentage}%) -
-
-
- - Sessions -
- {data.sessions.toLocaleString()} ({sessionPercentage}%) -
-
-
- ); - } - return null; - }; + // Custom item renderer for the device tooltip - renders both Views and Sessions rows + const deviceTooltipRenderer = (item, index) => { + if (index > 0) return null; // Only render for the first item (pie chart sends single slice) - const formatDuration = (seconds) => { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}m ${remainingSeconds}s`; + const deviceData = item.payload; + const color = COLORS[deviceData.device.toLowerCase()]; + const viewsPercentage = ((deviceData.pageViews / totalViews) * 100).toFixed(1); + const sessionsPercentage = ((deviceData.sessions / totalSessions) * 100).toFixed(1); + + return ( + <> +
+
+ + Views +
+ {deviceData.pageViews.toLocaleString()} ({viewsPercentage}%) +
+
+
+ + Sessions +
+ {deviceData.sessions.toLocaleString()} ({sessionsPercentage}%) +
+ + ); }; return ( @@ -249,78 +300,27 @@ export const UserBehaviorDashboard = () => { Device Usage - - - - - Page Path - Views - Bounce Rate - Avg. Duration - - - - {data?.data?.pageData?.pageData.map((page, index) => ( - - - {page.path} - - - {page.pageViews.toLocaleString()} - - - {page.bounceRate.toFixed(1)}% - - - {formatDuration(page.avgSessionDuration)} - - - ))} - -
+ + `${page.path}-${index}`} + maxHeight="xl" + compact + /> - - - - - Source - Sessions - Conv. - Conv. Rate - - - - {data?.data?.sourceData?.map((source, index) => ( - - - {source.source} - - - {source.sessions.toLocaleString()} - - - {source.conversions.toLocaleString()} - - - {((source.conversions / source.sessions) * 100).toFixed(1)}% - - - ))} - -
+ + `${source.source}-${index}`} + maxHeight="xl" + compact + /> - +
@@ -343,7 +343,14 @@ export const UserBehaviorDashboard = () => { /> ))} - } /> + payload?.[0]?.payload?.device || ""} + itemRenderer={deviceTooltipRenderer} + /> + } + />
@@ -354,4 +361,4 @@ export const UserBehaviorDashboard = () => { ); }; -export default UserBehaviorDashboard; \ No newline at end of file +export default UserBehaviorDashboard; diff --git a/inventory/src/components/dashboard/shared/DashboardSkeleton.tsx b/inventory/src/components/dashboard/shared/DashboardSkeleton.tsx index 9ec362b..5b527fa 100644 --- a/inventory/src/components/dashboard/shared/DashboardSkeleton.tsx +++ b/inventory/src/components/dashboard/shared/DashboardSkeleton.tsx @@ -210,6 +210,8 @@ export const ChartSkeleton: React.FC = ({ // TABLE SKELETON // ============================================================================= +export type TableSkeletonVariant = "simple" | "detailed"; + export interface TableSkeletonProps { /** Number of rows to show */ rows?: number; @@ -227,6 +229,14 @@ export interface TableSkeletonProps { scrollable?: boolean; /** Max height for scrollable (uses SCROLL_STYLES keys) */ maxHeight?: "sm" | "md" | "lg" | "xl"; + /** + * Cell layout variant: + * - "simple": single-line cells (default) + * - "detailed": multi-line cells with icon in first column and value+sublabel in others + */ + variant?: TableSkeletonVariant; + /** Show icon placeholder in first column (only for detailed variant) */ + hasIcon?: boolean; } export const TableSkeleton: React.FC = ({ @@ -238,6 +248,8 @@ export const TableSkeleton: React.FC = ({ className, scrollable = false, maxHeight = "md", + variant = "simple", + hasIcon = true, }) => { const columnCount = Array.isArray(columns) ? columns.length : columns; const columnWidths = Array.isArray(columns) @@ -245,13 +257,50 @@ export const TableSkeleton: React.FC = ({ : Array(columnCount).fill("w-24"); const colors = COLOR_VARIANT_CLASSES[colorVariant]; + // Simple variant - single line cells + const renderSimpleCell = (colIndex: number) => ( + + ); + + // Detailed variant - first column has icon + stacked text, others have value + sublabel + const renderDetailedFirstCell = () => ( +
+ {hasIcon && } +
+ + + +
+
+ ); + + const renderDetailedMetricCell = () => ( +
+ + +
+ ); + const tableContent = ( {Array.from({ length: columnCount }).map((_, i) => ( - - + + ))} @@ -260,14 +309,12 @@ export const TableSkeleton: React.FC = ({ {Array.from({ length: rows }).map((_, rowIndex) => ( {Array.from({ length: columnCount }).map((_, colIndex) => ( - - + + {variant === "detailed" ? ( + colIndex === 0 ? renderDetailedFirstCell() : renderDetailedMetricCell() + ) : ( + renderSimpleCell(colIndex) + )} ))} diff --git a/inventory/src/components/dashboard/shared/DashboardTable.tsx b/inventory/src/components/dashboard/shared/DashboardTable.tsx index f7021e7..0f30491 100644 --- a/inventory/src/components/dashboard/shared/DashboardTable.tsx +++ b/inventory/src/components/dashboard/shared/DashboardTable.tsx @@ -55,6 +55,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { @@ -86,6 +87,17 @@ export interface TableColumn> { className?: string; /** Whether to use tabular-nums for numeric values */ numeric?: boolean; + /** Whether this column is sortable */ + sortable?: boolean; + /** Custom sort key if different from column key */ + sortKey?: string; +} + +export type SortDirection = "asc" | "desc"; + +export interface SortConfig { + key: string; + direction: SortDirection; } export interface DashboardTableProps> { @@ -119,6 +131,10 @@ export interface DashboardTableProps> { className?: string; /** Additional className for the table element */ tableClassName?: string; + /** Current sort configuration (for controlled sorting) */ + sortConfig?: SortConfig; + /** Callback when a sortable column header is clicked */ + onSort?: (key: string, direction: SortDirection) => void; } // ============================================================================= @@ -159,10 +175,50 @@ export function DashboardTable>({ bordered = false, className, tableClassName, + sortConfig, + onSort, }: DashboardTableProps): React.ReactElement { const paddingClass = compact ? "px-3 py-2" : "px-4 py-3"; const scrollClass = maxHeight !== "none" ? MAX_HEIGHT_CLASSES[maxHeight] : ""; + // Handle sort click - toggles direction or sets new sort key + const handleSortClick = (col: TableColumn) => { + if (!onSort || !col.sortable) return; + + const sortKey = col.sortKey || col.key; + const currentDirection = sortConfig?.key === sortKey ? sortConfig.direction : null; + const newDirection: SortDirection = currentDirection === "desc" ? "asc" : "desc"; + + onSort(sortKey, newDirection); + }; + + // Render header cell content - either plain text or sortable button + const renderHeaderContent = (col: TableColumn) => { + if (!col.sortable || !onSort) { + return col.header; + } + + const sortKey = col.sortKey || col.key; + const isActive = sortConfig?.key === sortKey; + + return ( + + ); + }; + // Loading skeleton if (loading) { return ( @@ -241,13 +297,14 @@ export function DashboardTable>({ key={col.key} className={cn( TABLE_STYLES.headerCell, - paddingClass, + // Reduce padding when sortable since button has its own padding + col.sortable && onSort ? "p-1" : paddingClass, ALIGNMENT_CLASSES[col.align || "left"], col.width, col.hideOnMobile && "hidden sm:table-cell" )} > - {col.header} + {renderHeaderContent(col)} ))} diff --git a/inventory/src/components/dashboard/shared/index.ts b/inventory/src/components/dashboard/shared/index.ts index 9d3d97d..615c6e5 100644 --- a/inventory/src/components/dashboard/shared/index.ts +++ b/inventory/src/components/dashboard/shared/index.ts @@ -72,6 +72,8 @@ export { type CellAlignment, type SimpleTableProps, type SimpleTableRow, + type SortDirection, + type SortConfig, } from "./DashboardTable"; // ============================================================================= @@ -133,6 +135,7 @@ export { // Types type ChartSkeletonProps, type TableSkeletonProps, + type TableSkeletonVariant, type StatCardSkeletonProps, type GridSkeletonProps, type ListSkeletonProps, diff --git a/inventory/tsconfig.tsbuildinfo b/inventory/tsconfig.tsbuildinfo index 1b9639d..339c2a4 100644 --- a/inventory/tsconfig.tsbuildinfo +++ b/inventory/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/quickorderbuilder.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/overview.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/overview/vendorperformance.tsx","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepnew/utils/countryutils.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/steps/validationstepnew/utils/priceutils.ts","./src/components/product-import/steps/validationstepnew/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/products.tsx","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/lib/utils.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/overview.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/vendors.tsx","./src/services/apiv2.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/config.ts","./src/components/analytics/categoryperformance.tsx","./src/components/analytics/priceanalysis.tsx","./src/components/analytics/profitanalysis.tsx","./src/components/analytics/stockanalysis.tsx","./src/components/analytics/vendorperformance.tsx","./src/components/auth/firstaccessiblepage.tsx","./src/components/auth/protected.tsx","./src/components/auth/requireauth.tsx","./src/components/chat/chatroom.tsx","./src/components/chat/chattest.tsx","./src/components/chat/roomlist.tsx","./src/components/chat/searchresults.tsx","./src/components/dashboard/financialoverview.tsx","./src/components/dashboard/periodselectionpopover.tsx","./src/components/dashboard/shared/dashboardbadge.tsx","./src/components/dashboard/shared/dashboardcharttooltip.tsx","./src/components/dashboard/shared/dashboardsectionheader.tsx","./src/components/dashboard/shared/dashboardskeleton.tsx","./src/components/dashboard/shared/dashboardstatcard.tsx","./src/components/dashboard/shared/dashboardstatcardmini.tsx","./src/components/dashboard/shared/dashboardstates.tsx","./src/components/dashboard/shared/dashboardtable.tsx","./src/components/dashboard/shared/index.ts","./src/components/discount-simulator/configpanel.tsx","./src/components/discount-simulator/resultschart.tsx","./src/components/discount-simulator/resultstable.tsx","./src/components/discount-simulator/summarycard.tsx","./src/components/forecasting/daterangepickerquick.tsx","./src/components/forecasting/quickorderbuilder.tsx","./src/components/forecasting/columns.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/mainlayout.tsx","./src/components/layout/navuser.tsx","./src/components/overview/bestsellers.tsx","./src/components/overview/forecastmetrics.tsx","./src/components/overview/overstockmetrics.tsx","./src/components/overview/overview.tsx","./src/components/overview/purchasemetrics.tsx","./src/components/overview/replenishmentmetrics.tsx","./src/components/overview/salesmetrics.tsx","./src/components/overview/stockmetrics.tsx","./src/components/overview/topoverstockedproducts.tsx","./src/components/overview/topreplenishproducts.tsx","./src/components/overview/vendorperformance.tsx","./src/components/product-import/createproductcategorydialog.tsx","./src/components/product-import/reactspreadsheetimport.tsx","./src/components/product-import/config.ts","./src/components/product-import/index.ts","./src/components/product-import/translationsrsiprops.ts","./src/components/product-import/types.ts","./src/components/product-import/components/modalwrapper.tsx","./src/components/product-import/components/providers.tsx","./src/components/product-import/components/table.tsx","./src/components/product-import/hooks/usersi.ts","./src/components/product-import/steps/steps.tsx","./src/components/product-import/steps/uploadflow.tsx","./src/components/product-import/steps/imageuploadstep/imageuploadstep.tsx","./src/components/product-import/steps/imageuploadstep/types.ts","./src/components/product-import/steps/imageuploadstep/components/droppablecontainer.tsx","./src/components/product-import/steps/imageuploadstep/components/genericdropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/copybutton.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/imagedropzone.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/productcard.tsx","./src/components/product-import/steps/imageuploadstep/components/productcard/sortableimage.tsx","./src/components/product-import/steps/imageuploadstep/components/unassignedimagessection/unassignedimageitem.tsx","./src/components/product-import/steps/imageuploadstep/hooks/usebulkimageupload.ts","./src/components/product-import/steps/imageuploadstep/hooks/usedraganddrop.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimageoperations.ts","./src/components/product-import/steps/imageuploadstep/hooks/useproductimagesinit.ts","./src/components/product-import/steps/imageuploadstep/hooks/useurlimageupload.ts","./src/components/product-import/steps/matchcolumnsstep/matchcolumnsstep.tsx","./src/components/product-import/steps/matchcolumnsstep/types.ts","./src/components/product-import/steps/matchcolumnsstep/components/matchicon.tsx","./src/components/product-import/steps/matchcolumnsstep/components/templatecolumn.tsx","./src/components/product-import/steps/matchcolumnsstep/utils/findmatch.ts","./src/components/product-import/steps/matchcolumnsstep/utils/findunmatchedrequiredfields.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getfieldoptions.ts","./src/components/product-import/steps/matchcolumnsstep/utils/getmatchedcolumns.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizecheckboxvalue.ts","./src/components/product-import/steps/matchcolumnsstep/utils/normalizetabledata.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setignorecolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/setsubcolumn.ts","./src/components/product-import/steps/matchcolumnsstep/utils/uniqueentries.ts","./src/components/product-import/steps/selectheaderstep/selectheaderstep.tsx","./src/components/product-import/steps/selectheaderstep/components/selectheadertable.tsx","./src/components/product-import/steps/selectheaderstep/components/columns.tsx","./src/components/product-import/steps/selectsheetstep/selectsheetstep.tsx","./src/components/product-import/steps/uploadstep/uploadstep.tsx","./src/components/product-import/steps/uploadstep/components/dropzone.tsx","./src/components/product-import/steps/uploadstep/components/columns.tsx","./src/components/product-import/steps/uploadstep/utils/readfilesasync.ts","./src/components/product-import/steps/validationstep/index.tsx","./src/components/product-import/steps/validationstep/components/copydownbanner.tsx","./src/components/product-import/steps/validationstep/components/floatingselectionbar.tsx","./src/components/product-import/steps/validationstep/components/initializingoverlay.tsx","./src/components/product-import/steps/validationstep/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstep/components/validationcontainer.tsx","./src/components/product-import/steps/validationstep/components/validationfooter.tsx","./src/components/product-import/steps/validationstep/components/validationtable.tsx","./src/components/product-import/steps/validationstep/components/validationtoolbar.tsx","./src/components/product-import/steps/validationstep/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/comboboxcell.tsx","./src/components/product-import/steps/validationstep/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstep/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstep/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstep/dialogs/aidebugdialog.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationprogress.tsx","./src/components/product-import/steps/validationstep/dialogs/aivalidationresults.tsx","./src/components/product-import/steps/validationstep/hooks/usefieldoptions.ts","./src/components/product-import/steps/validationstep/hooks/useproductlines.ts","./src/components/product-import/steps/validationstep/hooks/usetemplatemanagement.ts","./src/components/product-import/steps/validationstep/hooks/useupcvalidation.ts","./src/components/product-import/steps/validationstep/hooks/usevalidationactions.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/index.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiapi.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaiprogress.ts","./src/components/product-import/steps/validationstep/hooks/useaivalidation/useaitransform.ts","./src/components/product-import/steps/validationstep/store/selectors.ts","./src/components/product-import/steps/validationstep/store/types.ts","./src/components/product-import/steps/validationstep/store/validationstore.ts","./src/components/product-import/steps/validationstep/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstep/utils/countryutils.ts","./src/components/product-import/steps/validationstep/utils/datamutations.ts","./src/components/product-import/steps/validationstep/utils/priceutils.ts","./src/components/product-import/steps/validationstep/utils/upcutils.ts","./src/components/product-import/steps/validationstepnew/index.tsx","./src/components/product-import/steps/validationstepnew/types.ts","./src/components/product-import/steps/validationstepnew/components/aivalidationdialogs.tsx","./src/components/product-import/steps/validationstepnew/components/basecellcontent.tsx","./src/components/product-import/steps/validationstepnew/components/initializingvalidation.tsx","./src/components/product-import/steps/validationstepnew/components/searchabletemplateselect.tsx","./src/components/product-import/steps/validationstepnew/components/upcvalidationtableadapter.tsx","./src/components/product-import/steps/validationstepnew/components/validationcell.tsx","./src/components/product-import/steps/validationstepnew/components/validationcontainer.tsx","./src/components/product-import/steps/validationstepnew/components/validationtable.tsx","./src/components/product-import/steps/validationstepnew/components/cells/checkboxcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/inputcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multiselectcell.tsx","./src/components/product-import/steps/validationstepnew/components/cells/multilineinput.tsx","./src/components/product-import/steps/validationstepnew/components/cells/selectcell.tsx","./src/components/product-import/steps/validationstepnew/hooks/useaivalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefieldvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usefiltermanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useinitialvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useproductlinesfetching.tsx","./src/components/product-import/steps/validationstepnew/hooks/userowoperations.tsx","./src/components/product-import/steps/validationstepnew/hooks/usetemplatemanagement.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniqueitemnumbersvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useuniquevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/useupcvalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidation.tsx","./src/components/product-import/steps/validationstepnew/hooks/usevalidationstate.tsx","./src/components/product-import/steps/validationstepnew/hooks/validationtypes.ts","./src/components/product-import/steps/validationstepnew/types/index.ts","./src/components/product-import/steps/validationstepnew/utils/aivalidationutils.ts","./src/components/product-import/steps/validationstepnew/utils/countryutils.ts","./src/components/product-import/steps/validationstepnew/utils/datamutations.ts","./src/components/product-import/steps/validationstepnew/utils/priceutils.ts","./src/components/product-import/steps/validationstepnew/utils/upcutils.ts","./src/components/product-import/utils/exceedsmaxrecords.ts","./src/components/product-import/utils/mapdata.ts","./src/components/product-import/utils/mapworkbook.ts","./src/components/product-import/utils/steps.ts","./src/components/products/productdetail.tsx","./src/components/products/productfilters.tsx","./src/components/products/producttable.tsx","./src/components/products/producttableskeleton.tsx","./src/components/products/productviews.tsx","./src/components/products/products.tsx","./src/components/purchase-orders/categorymetricscard.tsx","./src/components/purchase-orders/filtercontrols.tsx","./src/components/purchase-orders/ordermetricscard.tsx","./src/components/purchase-orders/paginationcontrols.tsx","./src/components/purchase-orders/purchaseorderaccordion.tsx","./src/components/purchase-orders/purchaseorderstable.tsx","./src/components/purchase-orders/vendormetricscard.tsx","./src/components/settings/datamanagement.tsx","./src/components/settings/globalsettings.tsx","./src/components/settings/permissionselector.tsx","./src/components/settings/productsettings.tsx","./src/components/settings/promptmanagement.tsx","./src/components/settings/reusableimagemanagement.tsx","./src/components/settings/templatemanagement.tsx","./src/components/settings/userform.tsx","./src/components/settings/userlist.tsx","./src/components/settings/usermanagement.tsx","./src/components/settings/vendorsettings.tsx","./src/components/templates/searchproducttemplatedialog.tsx","./src/components/templates/templateform.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/code.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/date-range-picker-narrow.tsx","./src/components/ui/date-range-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/page-loading.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/config/dashboard.ts","./src/contexts/authcontext.tsx","./src/contexts/dashboardscrollcontext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/usedebounce.ts","./src/lib/utils.ts","./src/lib/dashboard/chartconfig.ts","./src/lib/dashboard/designtokens.ts","./src/pages/analytics.tsx","./src/pages/blackfridaydashboard.tsx","./src/pages/brands.tsx","./src/pages/categories.tsx","./src/pages/chat.tsx","./src/pages/dashboard.tsx","./src/pages/discountsimulator.tsx","./src/pages/forecasting.tsx","./src/pages/htslookup.tsx","./src/pages/import.tsx","./src/pages/login.tsx","./src/pages/overview.tsx","./src/pages/products.tsx","./src/pages/purchaseorders.tsx","./src/pages/settings.tsx","./src/pages/smalldashboard.tsx","./src/pages/vendors.tsx","./src/services/apiv2.ts","./src/types/dashboard-shims.d.ts","./src/types/dashboard.d.ts","./src/types/discount-simulator.ts","./src/types/globals.d.ts","./src/types/products.ts","./src/types/react-data-grid.d.ts","./src/types/status-codes.ts","./src/utils/emojiutils.ts","./src/utils/naturallanguageperiod.ts","./src/utils/productutils.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file