PO-related fixes

This commit is contained in:
2025-04-12 10:54:42 -04:00
parent 00249f7c33
commit ac14179bd2
5 changed files with 433 additions and 112 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../components/ui/table';
import { Loader2, ArrowUpDown } from 'lucide-react';
import { Loader2, ArrowUpDown, Info, BarChart3 } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Badge } from '../components/ui/badge';
@@ -15,10 +15,26 @@ import {
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '../components/ui/pagination';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../components/ui/tooltip';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../components/ui/dialog";
import { motion } from 'motion/react';
import {
PurchaseOrderStatus,
@@ -29,7 +45,7 @@ import {
} from '../types/status-codes';
interface PurchaseOrder {
id: number;
id: number | string;
vendor_name: string;
order_date: string;
status: number;
@@ -59,6 +75,9 @@ interface CostAnalysis {
total_spend_by_category: {
category: string;
total_spend: number;
unique_products?: number;
avg_cost?: number;
cost_variance?: number;
}[];
}
@@ -89,7 +108,7 @@ interface PurchaseOrdersResponse {
};
filters: {
vendors: string[];
statuses: string[];
statuses: number[];
};
}
@@ -109,7 +128,7 @@ export default function PurchaseOrders() {
});
const [filterOptions, setFilterOptions] = useState<{
vendors: string[];
statuses: string[];
statuses: number[];
}>({
vendors: [],
statuses: []
@@ -120,6 +139,7 @@ export default function PurchaseOrders() {
page: 1,
limit: 100,
});
const [costAnalysisOpen, setCostAnalysisOpen] = useState(false);
const STATUS_FILTER_OPTIONS = [
{ value: 'all', label: 'All Statuses' },
@@ -133,14 +153,15 @@ export default function PurchaseOrders() {
const fetchData = async () => {
try {
setLoading(true);
const searchParams = new URLSearchParams({
page: page.toString(),
limit: '100',
sortColumn,
sortDirection,
...filters.search && { search: filters.search },
...filters.status && { status: filters.status },
...filters.vendor && { vendor: filters.vendor },
...filters.status !== 'all' && { status: filters.status },
...filters.vendor !== 'all' && { vendor: filters.vendor },
});
const [
@@ -205,7 +226,23 @@ export default function PurchaseOrders() {
console.error('Failed to fetch cost analysis:', await costAnalysisRes.text());
}
setPurchaseOrders(purchaseOrdersData.orders);
// Process orders data
const processedOrders = purchaseOrdersData.orders.map(order => {
let processedOrder = {
...order,
status: Number(order.status),
receiving_status: Number(order.receiving_status),
total_items: Number(order.total_items) || 0,
total_quantity: Number(order.total_quantity) || 0,
total_cost: Number(order.total_cost) || 0,
total_received: Number(order.total_received) || 0,
fulfillment_rate: Number(order.fulfillment_rate) || 0
};
return processedOrder;
});
setPurchaseOrders(processedOrders);
setPagination(purchaseOrdersData.pagination);
setFilterOptions(purchaseOrdersData.filters);
setSummary(purchaseOrdersData.summary);
@@ -280,6 +317,10 @@ export default function PurchaseOrders() {
});
};
const formatCurrency = (value: number) => {
return `$${formatNumber(value)}`;
};
const formatPercent = (value: number) => {
return (value * 100).toLocaleString('en-US', {
minimumFractionDigits: 1,
@@ -287,6 +328,141 @@ export default function PurchaseOrders() {
}) + '%';
};
// Generate pagination items
const getPaginationItems = () => {
const items = [];
const maxPagesToShow = 5;
const totalPages = pagination.pages;
// Always show first page
if (totalPages > 0) {
items.push(
<PaginationItem key="first">
<PaginationLink
isActive={page === 1}
onClick={() => page !== 1 && setPage(1)}
>
1
</PaginationLink>
</PaginationItem>
);
}
// Add ellipsis if needed
if (page > 3) {
items.push(
<PaginationItem key="ellipsis-1">
<PaginationEllipsis />
</PaginationItem>
);
}
// Add pages around current page
const startPage = Math.max(2, page - 1);
const endPage = Math.min(totalPages - 1, page + 1);
for (let i = startPage; i <= endPage; i++) {
if (i <= 1 || i >= totalPages) continue; // Skip first and last page as they're handled separately
items.push(
<PaginationItem key={i}>
<PaginationLink
isActive={page === i}
onClick={() => page !== i && setPage(i)}
>
{i}
</PaginationLink>
</PaginationItem>
);
}
// Add ellipsis if needed
if (page < totalPages - 2) {
items.push(
<PaginationItem key="ellipsis-2">
<PaginationEllipsis />
</PaginationItem>
);
}
// Always show last page if there are multiple pages
if (totalPages > 1) {
items.push(
<PaginationItem key="last">
<PaginationLink
isActive={page === totalPages}
onClick={() => page !== totalPages && setPage(totalPages)}
>
{totalPages}
</PaginationLink>
</PaginationItem>
);
}
return items;
};
// Cost Analysis table component
const CostAnalysisTable = () => {
if (!costAnalysis) return null;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Category</TableHead>
<TableHead>Products</TableHead>
<TableHead>Avg. Cost</TableHead>
<TableHead>Price Variance</TableHead>
<TableHead>Total Spend</TableHead>
<TableHead>% of Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{costAnalysis?.total_spend_by_category?.length ?
costAnalysis.total_spend_by_category.map((category) => {
// Calculate percentage of total spend
const totalSpendPercentage =
costAnalysis.total_spend_by_category.reduce((sum, cat) => sum + cat.total_spend, 0) > 0
? (category.total_spend /
costAnalysis.total_spend_by_category.reduce((sum, cat) => sum + cat.total_spend, 0))
: 0;
return (
<TableRow key={category.category}>
<TableCell className="font-medium">
{category.category || 'Uncategorized'}
</TableCell>
<TableCell>
{category.unique_products?.toLocaleString() || "N/A"}
</TableCell>
<TableCell>
{category.avg_cost !== undefined ? formatCurrency(category.avg_cost) : "N/A"}
</TableCell>
<TableCell>
{category.cost_variance !== undefined ?
parseFloat(category.cost_variance.toFixed(2)).toLocaleString() : "N/A"}
</TableCell>
<TableCell>
{formatCurrency(category.total_spend)}
</TableCell>
<TableCell>
{formatPercent(totalSpendPercentage)}
</TableCell>
</TableRow>
);
}) : (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No cost analysis data available
</TableCell>
</TableRow>
)
}
</TableBody>
</Table>
);
};
if (loading) {
return (
<div className="flex h-full items-center justify-center">
@@ -315,7 +491,7 @@ export default function PurchaseOrders() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
${formatNumber(summary?.total_value || 0)}
{formatCurrency(summary?.total_value || 0)}
</div>
</CardContent>
</Card>
@@ -331,12 +507,44 @@ export default function PurchaseOrders() {
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Avg Cost per PO</CardTitle>
<CardTitle className="text-sm font-medium">Spending Analysis</CardTitle>
<Dialog open={costAnalysisOpen} onOpenChange={setCostAnalysisOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<BarChart3 className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-[90%] w-fit">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
<span>Purchase Order Spending Analysis by Category</span>
</DialogTitle>
<DialogDescription>
This analysis shows spending distribution across product categories
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
<CostAnalysisTable />
</div>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
${formatNumber(summary?.avg_cost || 0)}
{formatCurrency(summary?.avg_cost || 0)}
<div className="text-sm font-normal text-muted-foreground">
Avg. Cost per PO
</div>
</div>
<Button
variant="outline"
className="w-full mt-2 text-sm"
onClick={() => setCostAnalysisOpen(true)}
>
<BarChart3 className="h-4 w-4 mr-2" />
View Category Analysis
</Button>
</CardContent>
</Card>
</div>
@@ -435,9 +643,11 @@ export default function PurchaseOrders() {
<TableCell>{getStatusBadge(po.status, po.receiving_status)}</TableCell>
<TableCell>{po.total_items.toLocaleString()}</TableCell>
<TableCell>{po.total_quantity.toLocaleString()}</TableCell>
<TableCell>${formatNumber(po.total_cost)}</TableCell>
<TableCell>{formatCurrency(po.total_cost)}</TableCell>
<TableCell>{po.total_received.toLocaleString()}</TableCell>
<TableCell>{formatPercent(po.fulfillment_rate)}</TableCell>
<TableCell>
{po.fulfillment_rate === null ? 'N/A' : formatPercent(po.fulfillment_rate)}
</TableCell>
</TableRow>
))}
{!purchaseOrders.length && (
@@ -454,62 +664,38 @@ export default function PurchaseOrders() {
{/* Pagination */}
{pagination.pages > 1 && (
<div className="flex justify-center">
<div className="flex justify-center mb-6">
<Pagination>
<PaginationContent>
<PaginationItem>
<Button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="h-9 px-4"
>
<PaginationPrevious className="h-4 w-4" />
</Button>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (page > 1) setPage(page - 1);
}}
aria-disabled={page === 1}
className={page === 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{getPaginationItems()}
<PaginationItem>
<Button
onClick={() => setPage(page + 1)}
disabled={page === pagination.pages}
className="h-9 px-4"
>
<PaginationNext className="h-4 w-4" />
</Button>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (page < pagination.pages) setPage(page + 1);
}}
aria-disabled={page === pagination.pages}
className={page === pagination.pages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
{/* Cost Analysis */}
<Card>
<CardHeader>
<CardTitle>Cost Analysis by Category</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Category</TableHead>
<TableHead>Total Spend</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{costAnalysis?.total_spend_by_category?.map((category) => (
<TableRow key={category.category}>
<TableCell>{category.category}</TableCell>
<TableCell>${formatNumber(category.total_spend)}</TableCell>
</TableRow>
)) || (
<TableRow>
<TableCell colSpan={2} className="text-center text-muted-foreground">
No cost analysis data available
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</motion.div>
);
}