Standardize alert messages and clean up headers

This commit is contained in:
2024-12-22 14:34:56 -05:00
parent 28623160ee
commit 4619d24e84
4 changed files with 406 additions and 409 deletions

View File

@@ -48,6 +48,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const METRIC_IDS = { const METRIC_IDS = {
PLACED_ORDER: "Y8cqcF", PLACED_ORDER: "Y8cqcF",
@@ -1387,34 +1388,35 @@ const EventFeed = ({
return ( return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6 pb-0"> <CardHeader className="p-6">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
{lastUpdate && ( {lastUpdate && (
<CardDescription> <CardDescription className="text-xs">
Last updated: {format(lastUpdate, "hh:mm a")} Last updated {format(lastUpdate, "h:mm a")}
</CardDescription> </CardDescription>
)} )}
</div> </div>
<div className="flex flex-wrap gap-1"> {!error && (
<TooltipProvider> <div className="flex flex-wrap gap-1">
<Tooltip> <TooltipProvider>
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"} <Button
size="sm" variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"}
onClick={() => handleEventTypeClick(METRIC_IDS.PLACED_ORDER)} size="sm"
className="h-8 w-8 p-0" onClick={() => handleEventTypeClick(METRIC_IDS.PLACED_ORDER)}
> className="h-8 w-8 p-0"
<Package className="h-4 w-4" /> >
</Button> <Package className="h-4 w-4" />
</TooltipTrigger> </Button>
<TooltipContent> </TooltipTrigger>
<EventTypeTooltipContent /> <TooltipContent>
</TooltipContent> <EventTypeTooltipContent />
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@@ -1488,155 +1490,150 @@ const EventFeed = ({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"} variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"}
size="sm" size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.NEW_BLOG_POST)} onClick={() => handleEventTypeClick(METRIC_IDS.NEW_BLOG_POST)}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<EventTypeTooltipContent /> <EventTypeTooltipContent />
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
)}
</div> </div>
{/* Order Property Filters */} {/* Order Property Filters - only show if not in error state */}
<div className="flex flex-wrap gap-2 justify-center mt-4"> {!error && (
{counts.orderProperties.hasPreorder > 0 && ( <div className="flex flex-wrap gap-2 justify-center mt-4 pt-1">
<Badge {counts.orderProperties.hasPreorder > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('hasPreorder')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('hasPreorder')}
orderFilters.hasPreorder className={`${
? 'bg-purple-800 text-purple-200 hover:bg-purple-700' orderFilters.hasPreorder
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/20' ? 'bg-purple-800 text-purple-200 hover:bg-purple-700'
} cursor-pointer`} : 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/20'
> } cursor-pointer`}
Pre-order ({counts.orderProperties.hasPreorder}) >
</Badge> Pre-order ({counts.orderProperties.hasPreorder})
)} </Badge>
{counts.orderProperties.localPickup > 0 && ( )}
<Badge {counts.orderProperties.localPickup > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('localPickup')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('localPickup')}
orderFilters.localPickup className={`${
? 'bg-green-800 text-green-200 hover:bg-green-700' orderFilters.localPickup
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20' ? 'bg-green-800 text-green-200 hover:bg-green-700'
} cursor-pointer`} : 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
> } cursor-pointer`}
Local ({counts.orderProperties.localPickup}) >
</Badge> Local ({counts.orderProperties.localPickup})
)} </Badge>
{counts.orderProperties.isOnHold > 0 && ( )}
<Badge {counts.orderProperties.isOnHold > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('isOnHold')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('isOnHold')}
orderFilters.isOnHold className={`${
? 'bg-blue-800 text-blue-200 hover:bg-blue-700' orderFilters.isOnHold
: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/20' ? 'bg-blue-800 text-blue-200 hover:bg-blue-700'
} cursor-pointer`} : 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/20'
> } cursor-pointer`}
On Hold ({counts.orderProperties.isOnHold}) >
</Badge> On Hold ({counts.orderProperties.isOnHold})
)} </Badge>
{counts.orderProperties.onHoldReleased > 0 && ( )}
<Badge {counts.orderProperties.onHoldReleased > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('onHoldReleased')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('onHoldReleased')}
orderFilters.onHoldReleased className={`${
? 'bg-green-800 text-green-200 hover:bg-green-700' orderFilters.onHoldReleased
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20' ? 'bg-green-800 text-green-200 hover:bg-green-700'
} cursor-pointer`} : 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
> } cursor-pointer`}
Hold Released ({counts.orderProperties.onHoldReleased}) >
</Badge> Hold Released ({counts.orderProperties.onHoldReleased})
)} </Badge>
{counts.orderProperties.hasDigiItem > 0 && ( )}
<Badge {counts.orderProperties.hasDigiItem > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('hasDigiItem')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('hasDigiItem')}
orderFilters.hasDigiItem className={`${
? 'bg-indigo-800 text-indigo-200 hover:bg-indigo-700' orderFilters.hasDigiItem
: 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/20' ? 'bg-indigo-800 text-indigo-200 hover:bg-indigo-700'
} cursor-pointer`} : 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/20'
> } cursor-pointer`}
Digital ({counts.orderProperties.hasDigiItem}) >
</Badge> Digital ({counts.orderProperties.hasDigiItem})
)} </Badge>
{counts.orderProperties.hasNotions > 0 && ( )}
<Badge {counts.orderProperties.hasNotions > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('hasNotions')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('hasNotions')}
orderFilters.hasNotions className={`${
? 'bg-yellow-800 text-yellow-200 hover:bg-yellow-700' orderFilters.hasNotions
: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900/20' ? 'bg-yellow-800 text-yellow-200 hover:bg-yellow-700'
} cursor-pointer`} : 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900/20'
> } cursor-pointer`}
Notions ({counts.orderProperties.hasNotions}) >
</Badge> Notions ({counts.orderProperties.hasNotions})
)} </Badge>
{counts.orderProperties.hasGiftCard > 0 && ( )}
<Badge {counts.orderProperties.hasGiftCard > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('hasGiftCard')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('hasGiftCard')}
orderFilters.hasGiftCard className={`${
? 'bg-pink-800 text-pink-200 hover:bg-pink-700' orderFilters.hasGiftCard
: 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 hover:bg-pink-100 dark:hover:bg-pink-900/20' ? 'bg-pink-800 text-pink-200 hover:bg-pink-700'
} cursor-pointer`} : 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 hover:bg-pink-100 dark:hover:bg-pink-900/20'
> } cursor-pointer`}
eGift Card ({counts.orderProperties.hasGiftCard}) >
</Badge> eGift Card ({counts.orderProperties.hasGiftCard})
)} </Badge>
{counts.orderProperties.stillOwes > 0 && ( )}
<Badge {counts.orderProperties.stillOwes > 0 && (
variant="secondary" <Badge
onClick={() => handleOrderPropertyClick('stillOwes')} variant="secondary"
className={`${ onClick={() => handleOrderPropertyClick('stillOwes')}
orderFilters.stillOwes className={`${
? 'bg-red-800 text-red-200 hover:bg-red-700' orderFilters.stillOwes
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/20' ? 'bg-red-800 text-red-200 hover:bg-red-700'
} cursor-pointer`} : 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/20'
> } cursor-pointer`}
Owes ({counts.orderProperties.stillOwes}) >
</Badge> Owes ({counts.orderProperties.stillOwes})
)} </Badge>
</div> )}
</div>
)}
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-4 flex-1 overflow-hidden"> <CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-2">
<ScrollArea className="h-full"> <ScrollArea className="h-full">
{loading && !events.length ? ( {loading && !events.length ? (
<LoadingState /> <LoadingState />
) : error ? ( ) : error ? (
<div className="flex flex-col items-center justify-center p-6 text-center"> <Alert variant="destructive" className="mt-1" >
<AlertCircle className="w-12 h-12 text-red-500 mb-4" /> <AlertCircle className="h-4 w-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> <AlertTitle>Error</AlertTitle>
Error Loading Feed <AlertDescription>
</h3> Failed to load event feed: {error}
<p className="text-sm text-muted-foreground mb-4"> </AlertDescription>
{error} </Alert>
</p>
<button
onClick={() => fetchEvents()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Try Again
</button>
</div>
) : !filteredEvents || filteredEvents.length === 0 ? ( ) : !filteredEvents || filteredEvents.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center py-16 px-4"> <div className="h-full flex flex-col items-center justify-center py-16 px-4">
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4"> <div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">

View File

@@ -29,6 +29,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const ProductGrid = ({ const ProductGrid = ({
timeRange = "today", timeRange = "today",
@@ -161,7 +162,7 @@ const ProductGrid = ({
return ( return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6"> <CardHeader className="p-6 pb-4">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
@@ -170,15 +171,17 @@ const ProductGrid = ({
)} )}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="relative hidden sm:block"> {!error && (
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <div className="relative hidden sm:block">
<Input <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
placeholder="Search products..." <Input
value={searchQuery} placeholder="Search products..."
onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery}
className="pl-8 h-9 w-[200px]" onChange={(e) => setSearchQuery(e.target.value)}
/> className="pl-8 h-9 w-[200px]"
</div> />
</div>
)}
<Select <Select
value={selectedTimeRange} value={selectedTimeRange}
onValueChange={handleTimeRangeChange} onValueChange={handleTimeRangeChange}
@@ -203,11 +206,13 @@ const ProductGrid = ({
{loading ? ( {loading ? (
<LoadingState /> <LoadingState />
) : error ? ( ) : error ? (
<div className="flex flex-col items-center justify-center py-8 text-center"> <Alert variant="destructive" >
<AlertCircle className="h-12 w-12 text-destructive mb-4" /> <AlertCircle className="h-4 w-4" />
<p className="font-medium mb-2">Error loading products</p> <AlertTitle>Error</AlertTitle>
<p className="text-sm text-muted-foreground">{error}</p> <AlertDescription>
</div> Failed to load products: {error}
</AlertDescription>
</Alert>
) : !products?.length ? ( ) : !products?.length ? (
<div className="flex flex-col items-center justify-center py-8 text-center"> <div className="flex flex-col items-center justify-center py-8 text-center">
<Package className="h-12 w-12 text-muted-foreground mb-4" /> <Package className="h-12 w-12 text-muted-foreground mb-4" />

View File

@@ -69,6 +69,7 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const METRIC_IDS = { const METRIC_IDS = {
PLACED_ORDER: "Y8cqcF", PLACED_ORDER: "Y8cqcF",
@@ -675,7 +676,7 @@ const SalesChart = ({
return ( return (
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6"> <CardHeader className="p-6 pb-4">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
@@ -684,171 +685,173 @@ const SalesChart = ({
</CardTitle> </CardTitle>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Dialog> {!error && (
<DialogTrigger asChild> <Dialog>
<Button variant="outline" className="h-9"> <DialogTrigger asChild>
View Details <Button variant="outline" className="h-9">
</Button> View Details
</DialogTrigger> </Button>
<DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col"> </DialogTrigger>
<DialogHeader className="flex-none"> <DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col">
<DialogTitle>Daily Details</DialogTitle> <DialogHeader className="flex-none">
<div className="flex items-center justify-center gap-2 pt-4"> <DialogTitle>Daily Details</DialogTitle>
<div className="flex flex-wrap gap-1"> <div className="flex items-center justify-center gap-2 pt-4">
<div className="flex flex-wrap gap-1">
<Button
variant={metrics.revenue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
revenue: !prev.revenue,
}))
}
>
Revenue
</Button>
<Button
variant={metrics.orders ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
orders: !prev.orders,
}))
}
>
Orders
</Button>
<Button
variant={metrics.movingAverage ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
movingAverage: !prev.movingAverage,
}))
}
>
7-Day Avg
</Button>
<Button
variant={metrics.avgOrderValue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
avgOrderValue: !prev.avgOrderValue,
}))
}
>
AOV
</Button>
</div>
<Separator orientation="vertical" className="h-6" />
<Button <Button
variant={metrics.revenue ? "default" : "outline"} variant={metrics.showPrevious ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onClick={() =>
setMetrics((prev) => ({ setMetrics((prev) => ({
...prev, ...prev,
revenue: !prev.revenue, showPrevious: !prev.showPrevious,
})) }))
} }
> >
Revenue Compare Prev Period
</Button>
<Button
variant={metrics.orders ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
orders: !prev.orders,
}))
}
>
Orders
</Button>
<Button
variant={metrics.movingAverage ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
movingAverage: !prev.movingAverage,
}))
}
>
7-Day Avg
</Button>
<Button
variant={metrics.avgOrderValue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
avgOrderValue: !prev.avgOrderValue,
}))
}
>
AOV
</Button> </Button>
</div> </div>
</DialogHeader>
<Separator orientation="vertical" className="h-6" /> <div className="flex-1 overflow-y-auto mt-6">
<div className="rounded-lg border bg-card w-full">
<Button <Table className="w-full">
variant={metrics.showPrevious ? "default" : "outline"} <TableHeader>
size="sm" <TableRow>
onClick={() => <TableHead className="text-center whitespace-nowrap px-6 w-[120px]">Date</TableHead>
setMetrics((prev) => ({
...prev,
showPrevious: !prev.showPrevious,
}))
}
>
Compare Prev Period
</Button>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto mt-6">
<div className="rounded-lg border bg-card w-full">
<Table className="w-full">
<TableHeader>
<TableRow>
<TableHead className="text-center whitespace-nowrap px-6 w-[120px]">Date</TableHead>
{metrics.orders && (
<>
<TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">Orders</TableHead>
{metrics.showPrevious && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">Prev Orders</TableHead>
)}
</>
)}
{metrics.revenue && (
<>
<TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">Revenue</TableHead>
{metrics.showPrevious && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">Prev Revenue</TableHead>
)}
</>
)}
{metrics.avgOrderValue && (
<>
<TableHead className="text-center whitespace-nowrap px-6 min-w-[120px]">AOV</TableHead>
{metrics.showPrevious && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[120px]">Prev AOV</TableHead>
)}
</>
)}
{metrics.movingAverage && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">7-Day Avg</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{data.map((day) => (
<TableRow key={day.timestamp}>
<TableCell className="text-center whitespace-nowrap px-6">{formatXAxis(day.timestamp)}</TableCell>
{metrics.orders && ( {metrics.orders && (
<> <>
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">Orders</TableHead>
{day.orders.toLocaleString()}
</TableCell>
{metrics.showPrevious && ( {metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">Prev Orders</TableHead>
{day.prevOrders.toLocaleString()}
</TableCell>
)} )}
</> </>
)} )}
{metrics.revenue && ( {metrics.revenue && (
<> <>
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">Revenue</TableHead>
{formatCurrency(day.revenue)}
</TableCell>
{metrics.showPrevious && ( {metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">Prev Revenue</TableHead>
{formatCurrency(day.prevRevenue)}
</TableCell>
)} )}
</> </>
)} )}
{metrics.avgOrderValue && ( {metrics.avgOrderValue && (
<> <>
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[120px]">AOV</TableHead>
{formatCurrency(day.avgOrderValue)}
</TableCell>
{metrics.showPrevious && ( {metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[120px]">Prev AOV</TableHead>
{formatCurrency(day.prevAvgOrderValue)}
</TableCell>
)} )}
</> </>
)} )}
{metrics.movingAverage && ( {metrics.movingAverage && (
<TableCell className="text-center whitespace-nowrap px-6"> <TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">7-Day Avg</TableHead>
{formatCurrency(day.movingAverage)}
</TableCell>
)} )}
</TableRow> </TableRow>
))} </TableHeader>
</TableBody> <TableBody>
</Table> {data.map((day) => (
<TableRow key={day.timestamp}>
<TableCell className="text-center whitespace-nowrap px-6">{formatXAxis(day.timestamp)}</TableCell>
{metrics.orders && (
<>
<TableCell className="text-center whitespace-nowrap px-6">
{day.orders.toLocaleString()}
</TableCell>
{metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6">
{day.prevOrders.toLocaleString()}
</TableCell>
)}
</>
)}
{metrics.revenue && (
<>
<TableCell className="text-center whitespace-nowrap px-6">
{formatCurrency(day.revenue)}
</TableCell>
{metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6">
{formatCurrency(day.prevRevenue)}
</TableCell>
)}
</>
)}
{metrics.avgOrderValue && (
<>
<TableCell className="text-center whitespace-nowrap px-6">
{formatCurrency(day.avgOrderValue)}
</TableCell>
{metrics.showPrevious && (
<TableCell className="text-center whitespace-nowrap px-6">
{formatCurrency(day.prevAvgOrderValue)}
</TableCell>
)}
</>
)}
{metrics.movingAverage && (
<TableCell className="text-center whitespace-nowrap px-6">
{formatCurrency(day.movingAverage)}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div> </div>
</div> </DialogContent>
</DialogContent> </Dialog>
</Dialog> )}
<Select <Select
value={selectedTimeRange} value={selectedTimeRange}
onValueChange={handleTimeRangeChange} onValueChange={handleTimeRangeChange}
@@ -867,77 +870,79 @@ const SalesChart = ({
</div> </div>
</div> </div>
{/* Show either skeletons or actual stats */} {/* Show stats only if not in error state */}
{loading ? <SkeletonStats /> : <SummaryStats stats={summaryStats} />} {!error && (loading ? <SkeletonStats /> : <SummaryStats stats={summaryStats} />)}
{/* Show metric toggles only if not in error state */}
{!error && (
<div className="flex items-center gap-2 pt-2">
<div className="flex flex-wrap gap-1">
<Button
variant={metrics.revenue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
revenue: !prev.revenue,
}))
}
>
Revenue
</Button>
<Button
variant={metrics.orders ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
orders: !prev.orders,
}))
}
>
Orders
</Button>
<Button
variant={metrics.movingAverage ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
movingAverage: !prev.movingAverage,
}))
}
>
7-Day Avg
</Button>
<Button
variant={metrics.avgOrderValue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
avgOrderValue: !prev.avgOrderValue,
}))
}
>
AOV
</Button>
</div>
<Separator orientation="vertical" className="h-6" />
{/* Metric Toggles */}
<div className="flex items-center gap-2 pt-2">
<div className="flex flex-wrap gap-1">
<Button <Button
variant={metrics.revenue ? "default" : "outline"} variant={metrics.showPrevious ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onClick={() =>
setMetrics((prev) => ({ setMetrics((prev) => ({
...prev, ...prev,
revenue: !prev.revenue, showPrevious: !prev.showPrevious,
})) }))
} }
> >
Revenue Compare Prev Period
</Button>
<Button
variant={metrics.orders ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
orders: !prev.orders,
}))
}
>
Orders
</Button>
<Button
variant={metrics.movingAverage ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
movingAverage: !prev.movingAverage,
}))
}
>
7-Day Avg
</Button>
<Button
variant={metrics.avgOrderValue ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
avgOrderValue: !prev.avgOrderValue,
}))
}
>
AOV
</Button> </Button>
</div> </div>
)}
<Separator orientation="vertical" className="h-6" />
<Button
variant={metrics.showPrevious ? "default" : "outline"}
size="sm"
onClick={() =>
setMetrics((prev) => ({
...prev,
showPrevious: !prev.showPrevious,
}))
}
>
Compare Prev Period
</Button>
</div>
</div> </div>
</CardHeader> </CardHeader>
@@ -951,13 +956,13 @@ const SalesChart = ({
{showDailyTable && <SkeletonTable />} {showDailyTable && <SkeletonTable />}
</div> </div>
) : error ? ( ) : error ? (
<div className="flex items-center justify-center h-[400px] text-destructive"> <Alert variant="destructive">
<div className="text-center"> <AlertCircle className="h-4 w-4" />
<AlertCircle className="h-12 w-12 mx-auto mb-4" /> <AlertTitle>Error</AlertTitle>
<div className="font-medium mb-2">Error loading sales data</div> <AlertDescription>
<div className="text-sm text-muted-foreground">{error}</div> Failed to load sales data: {error}
</div> </AlertDescription>
</div> </Alert>
) : !data.length ? ( ) : !data.length ? (
<div className="flex items-center justify-center h-[400px] text-muted-foreground"> <div className="flex items-center justify-center h-[400px] text-muted-foreground">
<div className="text-center"> <div className="text-center">

View File

@@ -67,6 +67,8 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
const formatCurrency = (value, minimumFractionDigits = 0) => { const formatCurrency = (value, minimumFractionDigits = 0) => {
if (!value || isNaN(value)) return "$0"; if (!value || isNaN(value)) return "$0";
@@ -1401,21 +1403,13 @@ const StatCards = ({
if (!cachedData && error) { if (!cachedData && error) {
return ( return (
<div className="flex flex-col items-center justify-center p-6 text-center"> <Alert variant="destructive">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" /> <AlertCircle className="h-4 w-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> <AlertTitle>Error</AlertTitle>
Error Loading Data <AlertDescription>
</h3> Failed to load stats: {error}
<p className="text-sm text-muted-foreground mb-4"> </AlertDescription>
{error} </Alert>
</p>
<button
onClick={() => fetchDetailData(metric)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Try Again
</button>
</div>
); );
} }
@@ -1519,8 +1513,8 @@ const StatCards = ({
if (error) { if (error) {
return ( return (
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
<CardHeader className="p-6"> <CardHeader className="p-6 pb-4">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
@@ -1549,14 +1543,16 @@ const StatCards = ({
</Select> </Select>
</div> </div>
</div> </div>
{/* Rest of the header content */}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-0"> <CardContent className="p-6 pt-0">
<p className="text-destructive text-center py-8"> <Alert variant="destructive">
Error loading stats: {error} <AlertCircle className="h-4 w-4" />
</p> <AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load stats: {error}
</AlertDescription>
</Alert>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -1576,16 +1572,12 @@ const StatCards = ({
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle> <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
{description && ( {lastUpdate && !loading && (
<CardDescription className="mt-1">{description}</CardDescription> <CardDescription className="text-xs"> Last updated {lastUpdate.toFormat("h:mm a")}</CardDescription>
)} )}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{lastUpdate && !loading && (
<span className="text-sm text-muted-foreground">
Last updated: {lastUpdate.toFormat("hh:mm a")}
</span>
)}
<Select value={timeRange} onValueChange={setTimeRange}> <Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[130px] h-9"> <SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="Select time range" /> <SelectValue placeholder="Select time range" />
@@ -1600,8 +1592,6 @@ const StatCards = ({
</Select> </Select>
</div> </div>
</div> </div>
{/* Rest of the header content */}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-0"> <CardContent className="p-6 pt-0">