122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "../../components/ui/card";
|
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
|
|
type ReceivingStatus = {
|
|
order_count: number;
|
|
total_ordered: number;
|
|
total_received: number;
|
|
fulfillment_rate: number;
|
|
total_value: number;
|
|
avg_cost: number;
|
|
avg_delivery_days?: number;
|
|
max_delivery_days?: number;
|
|
};
|
|
|
|
interface OrderMetricsCardProps {
|
|
summary: ReceivingStatus | null;
|
|
loading: boolean;
|
|
}
|
|
|
|
export default function OrderMetricsCard({
|
|
summary,
|
|
loading,
|
|
}: OrderMetricsCardProps) {
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
|
|
// Only show loading state on initial load, not during table refreshes
|
|
useEffect(() => {
|
|
if (summary) {
|
|
setInitialLoading(false);
|
|
}
|
|
}, [summary]);
|
|
|
|
const formatNumber = (value: number) => {
|
|
return value.toLocaleString("en-US", {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
};
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return `$${formatNumber(value)}`;
|
|
};
|
|
|
|
const formatPercent = (value: number) => {
|
|
return (
|
|
(value * 100).toLocaleString("en-US", {
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1,
|
|
}) + "%"
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Order Metrics</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{initialLoading || loading ? (
|
|
<div className="flex flex-col gap-2">
|
|
{/* 5 rows of skeleton metrics */}
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="flex items-baseline justify-between">
|
|
<Skeleton className="h-4 w-32" />
|
|
<Skeleton className="h-6 w-16" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Avg. Cost per PO
|
|
</p>
|
|
<p className="text-lg font-bold">
|
|
{formatCurrency(summary?.avg_cost || 0)}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Overall Fulfillment Rate
|
|
</p>
|
|
<p className="text-lg font-bold">
|
|
{formatPercent(summary?.fulfillment_rate || 0)}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Total Orders
|
|
</p>
|
|
<p className="text-lg font-bold">
|
|
{summary?.order_count.toLocaleString() || 0}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Avg. Delivery Days
|
|
</p>
|
|
<p className="text-lg font-bold">
|
|
{summary?.avg_delivery_days ? summary.avg_delivery_days.toFixed(1) : "N/A"}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Longest Delivery Days
|
|
</p>
|
|
<p className="text-lg font-bold">
|
|
{summary?.max_delivery_days ? summary.max_delivery_days.toFixed(0) : "N/A"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|