Merge branch 'master' into Fix-up-statcards

This commit is contained in:
2024-12-23 00:32:36 -05:00
7 changed files with 129 additions and 84 deletions

9
.gitignore vendored
View File

@@ -23,3 +23,12 @@ dist-ssr
*.sln *.sln
*.sw? *.sw?
.env .env
dashboard/build/assets/index-Bv76DabZ.js
dashboard/build/assets/index-Bv76DabZ.js.map
dashboard/build/assets/index-DOOYmQEM.css
dashboard-server/frontend/build/assets/._index-Bv76DabZ.js
dashboard-server/frontend/build/assets/._index-Bv76DabZ.js.map
dashboard-server/frontend/build/assets/._index-DOOYmQEM.css
dashboard-server/frontend/build/assets/index-Bv76DabZ.js
dashboard-server/frontend/build/assets/index-Bv76DabZ.js.map
dashboard-server/frontend/build/assets/index-DOOYmQEM.css

View File

@@ -82,29 +82,32 @@ const SmallLayout = () => {
const DashboardLayout = () => { const DashboardLayout = () => {
return ( return (
<ScrollProvider> <ScrollProvider>
<div className="min-h-screen max-w-[1800px] mx-auto"> <div className="min-h-screen max-w-[1600px] mx-auto">
<div className="p-4"> <div className="p-4">
<Header /> <Header />
</div> </div>
<Navigation /> <Navigation />
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="col-span-8"> <div className="xl:col-span-4 col-span-6">
<div className="space-y-4 h-full w-full"> <div className="space-y-4 h-full w-full">
<StatCards /> <StatCards />
</div> </div>
</div> </div>
<div className="col-span-4 h-[530px]"> <div className="xl:col-span-2 col-span-6 h-[500px] xl:h-[643px] 2xl:h-[510px] lg:hidden xl:block">
<div className="h-full"> <div className="h-full">
<div className="h-full"><EventFeed /></div> <div className="h-full"><EventFeed /></div>
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div className="col-span-4 h-[740px]"> <div className="hidden lg:col-span-6 lg:block xl:hidden h-[740px]">
<EventFeed />
</div>
<div className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
<ProductGrid /> <ProductGrid />
</div> </div>
<div className="col-span-8 h-full w-full flex"> <div className="col-span-12 xl:col-span-8 h-full w-full flex">
<SalesChart className="w-full h-full"/> <SalesChart className="w-full h-full"/>
</div> </div>
</div> </div>

View File

@@ -1121,8 +1121,7 @@ const EventCard = ({ event }) => {
const DEFAULT_METRICS = Object.values(METRIC_IDS); const DEFAULT_METRICS = Object.values(METRIC_IDS);
const EventFeed = ({ const EventFeed = ({
title = "Live Event Feed", title = "Event Feed",
description = "Real-time event stream",
selectedMetrics = DEFAULT_METRICS, selectedMetrics = DEFAULT_METRICS,
}) => { }) => {
const metrics = useMemo(() => selectedMetrics, [selectedMetrics]); const metrics = useMemo(() => selectedMetrics, [selectedMetrics]);
@@ -1387,7 +1386,7 @@ 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 w-full">
<CardHeader className="p-6"> <CardHeader className="p-6">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
@@ -1622,7 +1621,7 @@ const EventFeed = ({
)} )}
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-2"> <CardContent className="p-0 pt-0 flex-1 overflow-hidden -mt-2">
<ScrollArea className="h-full"> <ScrollArea className="h-full">
{loading && !events.length ? ( {loading && !events.length ? (
<LoadingState /> <LoadingState />

View File

@@ -133,7 +133,7 @@ const Header = () => {
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex flex-col justify-between lg:flex-row items-left sm:items-center flex-wrap"> <div className="flex flex-col justify-between lg:flex-row items-center sm:items-center flex-wrap">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="flex space-x-2"> <div className="flex space-x-2">
<div <div
@@ -154,7 +154,7 @@ const Header = () => {
</h1> </h1>
</div> </div>
</div> </div>
<div className="flex items-left sm:items-center justify-start flex-wrap mt-2 sm:mt-0"> <div className="flex items-left sm:items-center justify-center flex-wrap mt-2 sm:mt-0">
{weather?.main && ( {weather?.main && (
<> <>
<div className="flex-col items-center text-center"> <div className="flex-col items-center text-center">

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import axios from "axios"; import axios from "axios";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Loader2, ArrowUpDown, AlertCircle, Package, Settings2, Search } from "lucide-react"; import { Loader2, ArrowUpDown, AlertCircle, Package, Settings2, Search, X } from "lucide-react";
import { import {
Table, Table,
TableBody, TableBody,
@@ -46,6 +46,7 @@ const ProductGrid = ({
direction: "desc", direction: "desc",
}); });
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [isSearchVisible, setIsSearchVisible] = useState(false);
useEffect(() => { useEffect(() => {
fetchProducts(); fetchProducts();
@@ -163,41 +164,67 @@ 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 pb-4"> <CardHeader className="p-6 pb-4">
<div className="flex justify-between items-start"> <div className="flex flex-col gap-4">
<div> <div className="flex justify-between items-start">
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle> <div>
{description && ( <CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
<CardDescription className="mt-1">{description}</CardDescription> {description && (
)} <CardDescription className="mt-1">{description}</CardDescription>
</div> )}
<div className="flex items-center gap-4"> </div>
{!error && ( <div className="flex items-center gap-2">
<div className="relative hidden sm:block"> {!error && (
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <Button
<Input variant="outline"
placeholder="Search products..." size="icon"
value={searchQuery} onClick={() => setIsSearchVisible(!isSearchVisible)}
onChange={(e) => setSearchQuery(e.target.value)} className={cn(
className="pl-8 h-9 w-[200px]" "h-9 w-9",
/> isSearchVisible && "bg-muted"
</div> )}
)} >
<Select <Search className="h-4 w-4" />
value={selectedTimeRange} </Button>
onValueChange={handleTimeRangeChange} )}
> <Select
<SelectTrigger className="w-[130px] h-9"> value={selectedTimeRange}
<SelectValue placeholder="Select time range" /> onValueChange={handleTimeRangeChange}
</SelectTrigger> >
<SelectContent> <SelectTrigger className="w-[130px] h-9">
{TIME_RANGES.map((range) => ( <SelectValue placeholder="Select time range" />
<SelectItem key={range.value} value={range.value}> </SelectTrigger>
{range.label} <SelectContent>
</SelectItem> {TIME_RANGES.map((range) => (
))} <SelectItem key={range.value} value={range.value}>
</SelectContent> {range.label}
</Select> </SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
{isSearchVisible && !error && (
<div className="relative w-full">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search products..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9 h-9 w-full"
autoFocus
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1 h-7 w-7"
onClick={() => setSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
)}
</div> </div>
</CardHeader> </CardHeader>
@@ -225,39 +252,39 @@ const ProductGrid = ({
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="hover:bg-transparent"> <tr className="hover:bg-transparent">
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 w-[50px] min-w-[50px] border-b" /> <th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 w-[50px] min-w-[35px] border-b" />
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 min-w-[200px] border-b"> <th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 border-b">
<Button <Button
variant={sorting.column === "name" ? "default" : "ghost"} variant={sorting.column === "name" ? "default" : "ghost"}
onClick={() => handleSort("name")} onClick={() => handleSort("name")}
className="w-full justify-start h-8" className="w-full p-2 justify-start h-8"
> >
Product Product
</Button> </Button>
</th> </th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b"> <th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<Button <Button
variant={sorting.column === "totalQuantity" ? "default" : "ghost"} variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
onClick={() => handleSort("totalQuantity")} onClick={() => handleSort("totalQuantity")}
className="w-full justify-center h-8" className="w-full p-2 justify-center h-8"
> >
Sold Sold
</Button> </Button>
</th> </th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b"> <th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<Button <Button
variant={sorting.column === "totalRevenue" ? "default" : "ghost"} variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
onClick={() => handleSort("totalRevenue")} onClick={() => handleSort("totalRevenue")}
className="w-full justify-center h-8" className="w-full p-2 justify-center h-8"
> >
Rev Rev
</Button> </Button>
</th> </th>
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b"> <th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b">
<Button <Button
variant={sorting.column === "orderCount" ? "default" : "ghost"} variant={sorting.column === "orderCount" ? "default" : "ghost"}
onClick={() => handleSort("orderCount")} onClick={() => handleSort("orderCount")}
className="w-full justify-center h-8" className="w-full p-2 justify-center h-8"
> >
Orders Orders
</Button> </Button>

View File

@@ -449,7 +449,7 @@ const SummaryStats = memo(({ stats = {} }) => {
} = stats; } = stats;
return ( return (
<div className="grid grid-cols-4 gap-4 py-4 max-w-4xl"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
<StatCard <StatCard
title="Total Revenue" title="Total Revenue"
value={formatCurrency(totalRevenue, false)} value={formatCurrency(totalRevenue, false)}
@@ -573,7 +573,6 @@ const SkeletonTable = () => (
const SalesChart = ({ const SalesChart = ({
timeRange = "last30days", timeRange = "last30days",
title = "Sales Overview", title = "Sales Overview",
description = "Track your sales performance over time",
}) => { }) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -684,12 +683,12 @@ const SalesChart = ({
{title} {title}
</CardTitle> </CardTitle>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
{!error && ( {!error && (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="h-9"> <Button variant="outline" className="h-9">
View Details Details
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col"> <DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col">
@@ -759,7 +758,7 @@ const SalesChart = ({
})) }))
} }
> >
Compare Prev Period Compare Prev
</Button> </Button>
</div> </div>
</DialogHeader> </DialogHeader>
@@ -875,7 +874,7 @@ const SalesChart = ({
{/* Show metric toggles only if not in error state */} {/* Show metric toggles only if not in error state */}
{!error && ( {!error && (
<div className="flex items-center gap-2 pt-2"> <div className="flex items-center flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
<Button <Button
variant={metrics.revenue ? "default" : "outline"} variant={metrics.revenue ? "default" : "outline"}
@@ -927,7 +926,8 @@ const SalesChart = ({
</Button> </Button>
</div> </div>
<Separator orientation="vertical" className="h-6" /> <Separator orientation="vertical" className="h-6 hidden sm:block" />
<Separator orientation="horizontal" className="sm:hidden w-20 my-2" />
<Button <Button
variant={metrics.showPrevious ? "default" : "outline"} variant={metrics.showPrevious ? "default" : "outline"}
@@ -975,11 +975,11 @@ const SalesChart = ({
</div> </div>
) : ( ) : (
<> <>
<div className="h-[400px] mt-4 bg-card rounded-lg p-4 relative"> <div className="h-[400px] mt-4 bg-card rounded-lg p-0 relative">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart <LineChart
data={data} data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: -30, left: -5, bottom: 5 }}
> >
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"

View File

@@ -69,6 +69,7 @@ import {
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
const formatCurrency = (value, minimumFractionDigits = 0) => { const formatCurrency = (value, minimumFractionDigits = 0) => {
if (!value || isNaN(value)) return "$0"; if (!value || isNaN(value)) return "$0";
@@ -1087,7 +1088,8 @@ const StatCard = ({
onClick, onClick,
info, info,
onDetailsClick, onDetailsClick,
isLoading = false isLoading = false,
progress
}) => ( }) => (
<Card <Card
className={`${className} ${onClick || onDetailsClick ? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" : ""}`} className={`${className} ${onClick || onDetailsClick ? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" : ""}`}
@@ -1114,22 +1116,25 @@ const StatCard = ({
<Skeleton className="h-4 w-24" /> <Skeleton className="h-4 w-24" />
</> </>
) : ( ) : (
<> <div className="space-y-2">
<div className={`text-2xl font-bold ${colorClass}`}> <div>
{valuePrefix}{value}{valueSuffix} <div className={`text-2xl font-bold ${colorClass}`}>
</div> {valuePrefix}{value}{valueSuffix}
{description && (
<div className="text-sm text-muted-foreground mt-2 items-center gap-2 flex">
{description}
{trend && (
<span className={`flex items-center gap-1 ${trend === 'up' ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'}`}>
{trend === 'up' ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
{trendPrefix}{trendValue}{trendSuffix}
</span>
)}
</div> </div>
)} {description && (
</> <div className="text-sm text-muted-foreground mt-2 items-center gap-2 flex">
{description}
{trend && (
<span className={`flex items-center gap-1 ${trend === 'up' ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'}`}>
{trend === 'up' ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
{trendPrefix}{trendValue}{trendSuffix}
</span>
)}
</div>
)}
</div>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -1376,7 +1381,8 @@ const StatCards = ({
} finally { } finally {
setDetailDataLoading(prev => ({ ...prev, [metric]: false })); setDetailDataLoading(prev => ({ ...prev, [metric]: false }));
} }
}, [timeRange, startDate, endDate]); }, [timeRange, startDate, endDate, shouldUseLast30Days, setCacheData, getCacheData]);
// Corrected preloadDetailData function // Corrected preloadDetailData function
const preloadDetailData = useCallback(() => { const preloadDetailData = useCallback(() => {
const metrics = [ const metrics = [
@@ -1743,15 +1749,16 @@ const StatCards = ({
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-0"> <CardContent className="p-6 pt-0">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-3 2xl:grid-cols-4 gap-2 md:gap-3">
<StatCard <StatCard
title="Total Revenue" title="Total Revenue"
value={formatCurrency(stats?.revenue || 0)} value={formatCurrency(stats?.revenue || 0)}
description={ description={
stats?.periodProgress < 100 stats?.periodProgress < 100
? `${stats.periodProgress.toFixed(1)}% complete • Projected: ${formatCurrency(stats.projectedRevenue)}` ? <span><span className="md:hidden">Proj: </span><span className="hidden md:inline">Projected: </span>{formatCurrency(stats.projectedRevenue)}</span>
: `Previous: ${formatCurrency(stats.prevPeriodRevenue)}` : `Previous: ${formatCurrency(stats.prevPeriodRevenue)}`
} }
progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined}
trend={revenueTrend?.trend} trend={revenueTrend?.trend}
trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null} trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null}
colorClass="text-green-600 dark:text-green-400" colorClass="text-green-600 dark:text-green-400"