Merge branch 'master' into Fix-up-statcards
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,6 +164,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 pb-4">
|
<CardHeader className="p-6 pb-4">
|
||||||
|
<div className="flex flex-col gap-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,17 +172,19 @@ const ProductGrid = ({
|
|||||||
<CardDescription className="mt-1">{description}</CardDescription>
|
<CardDescription className="mt-1">{description}</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
{!error && (
|
{!error && (
|
||||||
<div className="relative hidden sm:block">
|
<Button
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
variant="outline"
|
||||||
<Input
|
size="icon"
|
||||||
placeholder="Search products..."
|
onClick={() => setIsSearchVisible(!isSearchVisible)}
|
||||||
value={searchQuery}
|
className={cn(
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
"h-9 w-9",
|
||||||
className="pl-8 h-9 w-[200px]"
|
isSearchVisible && "bg-muted"
|
||||||
/>
|
)}
|
||||||
</div>
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Select
|
<Select
|
||||||
value={selectedTimeRange}
|
value={selectedTimeRange}
|
||||||
@@ -199,6 +203,29 @@ const ProductGrid = ({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,7 +1116,8 @@ const StatCard = ({
|
|||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
<div className={`text-2xl font-bold ${colorClass}`}>
|
<div className={`text-2xl font-bold ${colorClass}`}>
|
||||||
{valuePrefix}{value}{valueSuffix}
|
{valuePrefix}{value}{valueSuffix}
|
||||||
</div>
|
</div>
|
||||||
@@ -1129,7 +1132,9 @@ const StatCard = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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"
|
||||||
|
|||||||
Reference in New Issue
Block a user