Files
inventory/inventory/src/components/purchase-orders/OrderMetricsCard.tsx

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>
);
}