Merge branch 'master' into Fix-up-statcards
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,41 +164,67 @@ 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 justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
||||
{description && (
|
||||
<CardDescription className="mt-1">{description}</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{!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>
|
||||
)}
|
||||
<Select
|
||||
value={selectedTimeRange}
|
||||
onValueChange={handleTimeRangeChange}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] h-9">
|
||||
<SelectValue placeholder="Select time range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIME_RANGES.map((range) => (
|
||||
<SelectItem key={range.value} value={range.value}>
|
||||
{range.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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>
|
||||
{description && (
|
||||
<CardDescription className="mt-1">{description}</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!error && (
|
||||
<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}
|
||||
onValueChange={handleTimeRangeChange}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] h-9">
|
||||
<SelectValue placeholder="Select time range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIME_RANGES.map((range) => (
|
||||
<SelectItem key={range.value} value={range.value}>
|
||||
{range.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,22 +1116,25 @@ const StatCard = ({
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={`text-2xl font-bold ${colorClass}`}>
|
||||
{valuePrefix}{value}{valueSuffix}
|
||||
</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 className="space-y-2">
|
||||
<div>
|
||||
<div className={`text-2xl font-bold ${colorClass}`}>
|
||||
{valuePrefix}{value}{valueSuffix}
|
||||
</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>
|
||||
</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"
|
||||
|
||||
Reference in New Issue
Block a user