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
*.sw?
.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 = () => {
return (
<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">
<Header />
</div>
<Navigation />
<div className="p-4 space-y-4">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-8">
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="xl:col-span-4 col-span-6">
<div className="space-y-4 h-full w-full">
<StatCards />
</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"><EventFeed /></div>
</div>
</div>
</div>
<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 />
</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"/>
</div>
</div>

View File

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

View File

@@ -133,7 +133,7 @@ const Header = () => {
)}
>
<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 space-x-2">
<div
@@ -154,7 +154,7 @@ const Header = () => {
</h1>
</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 && (
<>
<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 { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
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 {
Table,
TableBody,
@@ -46,6 +46,7 @@ const ProductGrid = ({
direction: "desc",
});
const [searchQuery, setSearchQuery] = useState("");
const [isSearchVisible, setIsSearchVisible] = useState(false);
useEffect(() => {
fetchProducts();
@@ -163,6 +164,7 @@ const ProductGrid = ({
return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-6 pb-4">
<div className="flex flex-col gap-4">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
@@ -170,17 +172,19 @@ const ProductGrid = ({
<CardDescription className="mt-1">{description}</CardDescription>
)}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
{!error && (
<div className="relative hidden sm:block">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search products..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 h-9 w-[200px]"
/>
</div>
<Button
variant="outline"
size="icon"
onClick={() => setIsSearchVisible(!isSearchVisible)}
className={cn(
"h-9 w-9",
isSearchVisible && "bg-muted"
)}
>
<Search className="h-4 w-4" />
</Button>
)}
<Select
value={selectedTimeRange}
@@ -199,6 +203,29 @@ const ProductGrid = ({
</Select>
</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>
</CardHeader>
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
@@ -225,39 +252,39 @@ const ProductGrid = ({
<table className="w-full">
<thead>
<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.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 w-[50px] min-w-[35px] border-b" />
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 border-b">
<Button
variant={sorting.column === "name" ? "default" : "ghost"}
onClick={() => handleSort("name")}
className="w-full justify-start h-8"
className="w-full p-2 justify-start h-8"
>
Product
</Button>
</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
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
onClick={() => handleSort("totalQuantity")}
className="w-full justify-center h-8"
className="w-full p-2 justify-center h-8"
>
Sold
</Button>
</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
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
onClick={() => handleSort("totalRevenue")}
className="w-full justify-center h-8"
className="w-full p-2 justify-center h-8"
>
Rev
</Button>
</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
variant={sorting.column === "orderCount" ? "default" : "ghost"}
onClick={() => handleSort("orderCount")}
className="w-full justify-center h-8"
className="w-full p-2 justify-center h-8"
>
Orders
</Button>

View File

@@ -449,7 +449,7 @@ const SummaryStats = memo(({ stats = {} }) => {
} = stats;
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
title="Total Revenue"
value={formatCurrency(totalRevenue, false)}
@@ -573,7 +573,6 @@ const SkeletonTable = () => (
const SalesChart = ({
timeRange = "last30days",
title = "Sales Overview",
description = "Track your sales performance over time",
}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
@@ -684,12 +683,12 @@ const SalesChart = ({
{title}
</CardTitle>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
{!error && (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="h-9">
View Details
Details
</Button>
</DialogTrigger>
<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>
</div>
</DialogHeader>
@@ -875,7 +874,7 @@ const SalesChart = ({
{/* Show metric toggles only if not in error state */}
{!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">
<Button
variant={metrics.revenue ? "default" : "outline"}
@@ -927,7 +926,8 @@ const SalesChart = ({
</Button>
</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
variant={metrics.showPrevious ? "default" : "outline"}
@@ -975,11 +975,11 @@ const SalesChart = ({
</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%">
<LineChart
data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
margin={{ top: 5, right: -30, left: -5, bottom: 5 }}
>
<CartesianGrid
strokeDasharray="3 3"

View File

@@ -69,6 +69,7 @@ import {
import { Skeleton } from "@/components/ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
const formatCurrency = (value, minimumFractionDigits = 0) => {
if (!value || isNaN(value)) return "$0";
@@ -1087,7 +1088,8 @@ const StatCard = ({
onClick,
info,
onDetailsClick,
isLoading = false
isLoading = false,
progress
}) => (
<Card
className={`${className} ${onClick || onDetailsClick ? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" : ""}`}
@@ -1114,7 +1116,8 @@ const StatCard = ({
<Skeleton className="h-4 w-24" />
</>
) : (
<>
<div className="space-y-2">
<div>
<div className={`text-2xl font-bold ${colorClass}`}>
{valuePrefix}{value}{valueSuffix}
</div>
@@ -1129,7 +1132,9 @@ const StatCard = ({
)}
</div>
)}
</>
</div>
</div>
)}
</CardContent>
</Card>
@@ -1376,7 +1381,8 @@ const StatCards = ({
} finally {
setDetailDataLoading(prev => ({ ...prev, [metric]: false }));
}
}, [timeRange, startDate, endDate]);
}, [timeRange, startDate, endDate, shouldUseLast30Days, setCacheData, getCacheData]);
// Corrected preloadDetailData function
const preloadDetailData = useCallback(() => {
const metrics = [
@@ -1743,15 +1749,16 @@ const StatCards = ({
</div>
</CardHeader>
<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
title="Total Revenue"
value={formatCurrency(stats?.revenue || 0)}
description={
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)}`
}
progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined}
trend={revenueTrend?.trend}
trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null}
colorClass="text-green-600 dark:text-green-400"