Remove scrollbars and last updated

This commit is contained in:
2025-01-01 14:08:50 -05:00
parent 6c7aed68cc
commit 42f5033173
2 changed files with 185 additions and 174 deletions

View File

@@ -63,7 +63,7 @@ const PinProtectedLayout = ({ children }) => {
// Small Layout // Small Layout
const SmallLayout = () => { const SmallLayout = () => {
const DATETIME_SCALE = 2; const DATETIME_SCALE = 2;
const STATS_SCALE = 1.6; const STATS_SCALE = 1.65;
return ( return (
<div className="min-h-screen w-screen"> <div className="min-h-screen w-screen">
@@ -80,7 +80,7 @@ const SmallLayout = () => {
width: `${100/DATETIME_SCALE}%`, width: `${100/DATETIME_SCALE}%`,
}} }}
> >
<DateTimeWeatherDisplay /> <DateTimeWeatherDisplay scaleFactor={DATETIME_SCALE} />
</div> </div>
</div> </div>

View File

@@ -32,7 +32,12 @@ import {
Loader2, Loader2,
} from "lucide-react"; } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"; import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import { import {
Table, Table,
TableBody, TableBody,
@@ -147,7 +152,7 @@ const MiniStatCards = ({
endDate, endDate,
title = "Quick Stats", title = "Quick Stats",
description = "", description = "",
compact = false compact = false,
}) => { }) => {
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -176,8 +181,10 @@ const MiniStatCards = ({
}, []); }, []);
const calculateRevenueTrend = useCallback(() => { const calculateRevenueTrend = useCallback(() => {
if (!stats?.prevPeriodRevenue && stats?.prevPeriodRevenue !== 0) return null; if (!stats?.prevPeriodRevenue && stats?.prevPeriodRevenue !== 0)
const currentRevenue = stats.periodProgress < 100 ? stats.projectedRevenue : stats.revenue; return null;
const currentRevenue =
stats.periodProgress < 100 ? stats.projectedRevenue : stats.revenue;
const prevRevenue = stats.prevPeriodRevenue; const prevRevenue = stats.prevPeriodRevenue;
if (!currentRevenue || !prevRevenue) return null; if (!currentRevenue || !prevRevenue) return null;
@@ -213,8 +220,11 @@ const MiniStatCards = ({
setLoading(true); setLoading(true);
setStats(null); setStats(null);
const params = timeRange === "custom" ? { startDate, endDate } : { timeRange }; const params =
const response = await axios.get("/api/klaviyo/events/stats", { params }); timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/stats", {
params,
});
if (!isMounted) return; if (!isMounted) return;
@@ -248,8 +258,11 @@ const MiniStatCards = ({
try { try {
setProjectionLoading(true); setProjectionLoading(true);
const params = timeRange === "custom" ? { startDate, endDate } : { timeRange }; const params =
const response = await axios.get("/api/klaviyo/events/projection", { params }); timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/projection", {
params,
});
if (!isMounted) return; if (!isMounted) return;
setProjection(response.data); setProjection(response.data);
@@ -275,8 +288,12 @@ const MiniStatCards = ({
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const [statsResponse, projectionResponse] = await Promise.all([ const [statsResponse, projectionResponse] = await Promise.all([
axios.get("/api/klaviyo/events/stats", { params: { timeRange: "today" } }), axios.get("/api/klaviyo/events/stats", {
axios.get("/api/klaviyo/events/projection", { params: { timeRange: "today" } }), params: { timeRange: "today" },
}),
axios.get("/api/klaviyo/events/projection", {
params: { timeRange: "today" },
}),
]); ]);
setStats(statsResponse.data.stats); setStats(statsResponse.data.stats);
@@ -291,26 +308,29 @@ const MiniStatCards = ({
}, [timeRange]); }, [timeRange]);
// Add function to fetch detail data // Add function to fetch detail data
const fetchDetailData = useCallback(async (metric) => { const fetchDetailData = useCallback(
if (detailData[metric]) return; async (metric) => {
if (detailData[metric]) return;
setDetailDataLoading((prev) => ({ ...prev, [metric]: true })); setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
try { try {
const response = await axios.get("/api/klaviyo/events/stats/details", { const response = await axios.get("/api/klaviyo/events/stats/details", {
params: { params: {
timeRange: "last30days", timeRange: "last30days",
metric, metric,
daily: true, daily: true,
}, },
}); });
setDetailData((prev) => ({ ...prev, [metric]: response.data.stats })); setDetailData((prev) => ({ ...prev, [metric]: response.data.stats }));
} catch (error) { } catch (error) {
console.error(`Error fetching detail data for ${metric}:`, error); console.error(`Error fetching detail data for ${metric}:`, error);
} finally { } finally {
setDetailDataLoading((prev) => ({ ...prev, [metric]: false })); setDetailDataLoading((prev) => ({ ...prev, [metric]: false }));
} }
}, [detailData]); },
[detailData]
);
// Add effect to load detail data when metric is selected // Add effect to load detail data when metric is selected
useEffect(() => { useEffect(() => {
@@ -323,7 +343,7 @@ const MiniStatCards = ({
useEffect(() => { useEffect(() => {
// Preload all detail data when component mounts // Preload all detail data when component mounts
const metrics = ["revenue", "orders", "average_order", "shipping"]; const metrics = ["revenue", "orders", "average_order", "shipping"];
metrics.forEach(metric => { metrics.forEach((metric) => {
fetchDetailData(metric); fetchDetailData(metric);
}); });
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
@@ -331,7 +351,6 @@ const MiniStatCards = ({
if (loading && !stats) { if (loading && !stats) {
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">
<CardContent className="p-4 pt-0"> <CardContent className="p-4 pt-0">
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
@@ -360,156 +379,148 @@ const MiniStatCards = ({
const aovTrend = calculateAOVTrend(); const aovTrend = calculateAOVTrend();
return ( return (
<> <>
<div className="flex justify-between items-center"> <div className="grid grid-cols-4 gap-2">
<div> <StatCard
title="Total Revenue"
{lastUpdate && !loading && ( value={formatCurrency(stats?.revenue || 0)}
<CardDescription className="text-xs"> description={
Last updated {lastUpdate.toFormat("h:mm a")} stats?.periodProgress < 100 ? (
{projection?.confidence > 0 && !projectionLoading && ( <div className="flex items-center gap-1 text-sm">
<TooltipProvider> <span>Proj: </span>
<Tooltip delayDuration={300}> {projectionLoading ? (
<TooltipTrigger asChild> <Skeleton className="h-4 w-15" />
<span className="ml-1 text-muted-foreground"> ) : (
({Math.round(projection.confidence * 100)}%) formatCurrency(
</span> projection?.projectedRevenue || stats.projectedRevenue
</TooltipTrigger> )
<TooltipContent className="max-w-[250px]">
<p>Confidence level of revenue projection</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
</CardDescription> </div>
)} ) : null
</div> }
progress={
stats?.periodProgress < 100 ? stats.periodProgress : undefined
</div> }
trend={
<div className="grid grid-cols-4 gap-2"> projectionLoading && stats?.periodProgress < 100
<StatCard ? undefined
title="Total Revenue" : revenueTrend?.trend
value={formatCurrency(stats?.revenue || 0)} }
description={ trendValue={
stats?.periodProgress < 100 ? ( revenueTrend?.value ? formatPercentage(revenueTrend.value) : null
<div className="flex items-center gap-1 text-sm"> }
<span>Proj: </span> colorClass="text-green-600 dark:text-green-400"
{projectionLoading ? ( icon={DollarSign}
<Skeleton className="h-4 w-15" /> iconColor="text-green-500"
) : ( onDetailsClick={() => setSelectedMetric("revenue")}
formatCurrency(projection?.projectedRevenue || stats.projectedRevenue) isLoading={loading || !stats}
)} />
</div>
) : null
}
progress={stats?.periodProgress < 100 ? stats.periodProgress : undefined}
trend={projectionLoading && stats?.periodProgress < 100 ? undefined : revenueTrend?.trend}
trendValue={revenueTrend?.value ? formatPercentage(revenueTrend.value) : null}
colorClass="text-green-600 dark:text-green-400"
icon={DollarSign}
iconColor="text-green-500"
onDetailsClick={() => setSelectedMetric("revenue")}
isLoading={loading || !stats}
/>
<StatCard <StatCard
title="Orders" title="Orders"
value={stats?.orderCount} value={stats?.orderCount}
description={`${stats?.itemCount} items`} description={`${stats?.itemCount} items`}
trend={orderTrend?.trend} trend={orderTrend?.trend}
trendValue={orderTrend?.value ? formatPercentage(orderTrend.value) : null} trendValue={
colorClass="text-blue-600 dark:text-blue-400" orderTrend?.value ? formatPercentage(orderTrend.value) : null
icon={ShoppingCart} }
iconColor="text-blue-500" colorClass="text-blue-600 dark:text-blue-400"
onDetailsClick={() => setSelectedMetric("orders")} icon={ShoppingCart}
isLoading={loading || !stats} iconColor="text-blue-500"
/> onDetailsClick={() => setSelectedMetric("orders")}
isLoading={loading || !stats}
/>
<StatCard <StatCard
title="AOV" title="AOV"
value={stats?.averageOrderValue?.toFixed(2)} value={stats?.averageOrderValue?.toFixed(2)}
valuePrefix="$" valuePrefix="$"
description={`${stats?.averageItemsPerOrder?.toFixed(1)}/order`} description={`${stats?.averageItemsPerOrder?.toFixed(1)}/order`}
trend={aovTrend?.trend} trend={aovTrend?.trend}
trendValue={aovTrend?.value ? formatPercentage(aovTrend.value) : null} trendValue={aovTrend?.value ? formatPercentage(aovTrend.value) : null}
colorClass="text-purple-600 dark:text-purple-400" colorClass="text-purple-600 dark:text-purple-400"
icon={CircleDollarSign} icon={CircleDollarSign}
iconColor="text-purple-500" iconColor="text-purple-500"
onDetailsClick={() => setSelectedMetric("average_order")} onDetailsClick={() => setSelectedMetric("average_order")}
isLoading={loading || !stats} isLoading={loading || !stats}
/> />
<StatCard <StatCard
title="Shipped" title="Shipped"
value={stats?.shipping?.shippedCount || 0} value={stats?.shipping?.shippedCount || 0}
description={`${stats?.shipping?.locations?.total || 0} locations`} description={`${stats?.shipping?.locations?.total || 0} locations`}
colorClass="text-teal-600 dark:text-teal-400" colorClass="text-teal-600 dark:text-teal-400"
icon={Package} icon={Package}
iconColor="text-teal-500" iconColor="text-teal-500"
onDetailsClick={() => setSelectedMetric("shipping")} onDetailsClick={() => setSelectedMetric("shipping")}
isLoading={loading || !stats} isLoading={loading || !stats}
/> />
</div> </div>
<Dialog open={!!selectedMetric} onOpenChange={() => setSelectedMetric(null)}> <Dialog
<DialogContent className="w-[80vw] h-[80vh] max-w-none p-0"> open={!!selectedMetric}
<div className="transform scale-[2] origin-top-left h-[40vh] w-[40vw]"> onOpenChange={() => setSelectedMetric(null)}
<div className="h-full w-full p-6"> >
<DialogHeader> <DialogContent className="w-[80vw] h-[80vh] max-w-none p-0">
<DialogTitle> <div className="transform scale-[2] origin-top-left h-[40vh] w-[40vw]">
{selectedMetric <div className="h-full w-full p-6">
? `${selectedMetric <DialogHeader>
.split("_") <DialogTitle>
.map((w) => w.charAt(0).toUpperCase() + w.slice(1)) {selectedMetric
.join(" ")} Details` ? `${selectedMetric
: ""} .split("_")
</DialogTitle> .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
</DialogHeader> .join(" ")} Details`
<div className="mt-4 h-[calc(40vh-4rem)] overflow-auto"> : ""}
{detailDataLoading[selectedMetric] ? ( </DialogTitle>
<div className="space-y-4 h-full"> </DialogHeader>
{selectedMetric === "shipping" ? ( <div className="mt-4 h-[calc(40vh-4rem)] overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
<MiniSkeletonTable rows={8} /> {detailDataLoading[selectedMetric] ? (
) : ( <div className="space-y-4 h-full">
<> {selectedMetric === "shipping" ? (
<MiniSkeletonChart type={selectedMetric === "orders" ? "bar" : "line"} /> <MiniSkeletonTable rows={8} />
{selectedMetric === "orders" && ( ) : (
<div className="mt-8"> <>
<h3 className="text-lg font-medium mb-4">Hourly Distribution</h3> <MiniSkeletonChart
<MiniSkeletonChart type="bar" /> type={selectedMetric === "orders" ? "bar" : "line"}
</div>
)}
</>
)}
</div>
) : (
<div className="h-full">
{selectedMetric === "revenue" && (
<RevenueDetails data={detailData.revenue || []} />
)}
{selectedMetric === "orders" && (
<OrdersDetails data={detailData.orders || []} />
)}
{selectedMetric === "average_order" && (
<AverageOrderDetails
data={detailData.average_order || []}
orderCount={stats.orderCount}
/> />
)} {selectedMetric === "orders" && (
{selectedMetric === "shipping" && ( <div className="mt-8">
<ShippingDetails data={[stats]} timeRange={timeRange} /> <h3 className="text-lg font-medium mb-4">
)} Hourly Distribution
</div> </h3>
)} <MiniSkeletonChart type="bar" />
</div> </div>
)}
</>
)}
</div>
) : (
<div className="h-full">
{selectedMetric === "revenue" && (
<RevenueDetails data={detailData.revenue || []} />
)}
{selectedMetric === "orders" && (
<OrdersDetails data={detailData.orders || []} />
)}
{selectedMetric === "average_order" && (
<AverageOrderDetails
data={detailData.average_order || []}
orderCount={stats.orderCount}
/>
)}
{selectedMetric === "shipping" && (
<ShippingDetails data={[stats]} timeRange={timeRange} />
)}
</div>
)}
</div> </div>
</div> </div>
</DialogContent> </div>
</Dialog> </DialogContent>
</> </Dialog>
</>
); );
}; };
export default MiniStatCards; export default MiniStatCards;