Consolidate old/new vendor and category routes, enhance new brands route, update frontend accordingly for all three pages, improve hierarchy on categories page, fix some calculations

This commit is contained in:
2025-04-02 14:28:18 -04:00
parent dbd0232285
commit 6051b849d6
16 changed files with 2273 additions and 880 deletions

View File

@@ -3,21 +3,23 @@ import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { motion } from "framer-motion";
import { Input } from "@/components/ui/input";
import config from "../config";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
// Matches backend COLUMN_MAP keys for sorting
type BrandSortableColumns =
| 'brandName' | 'productCount' | 'activeProductCount' | 'currentStockUnits'
| 'currentStockCost' | 'currentStockRetail' | 'revenue_7d' | 'revenue_30d'
| 'profit_30d' | 'sales_30d' | 'avg_margin_30d'; // Add more as needed
| 'profit_30d' | 'sales_30d' | 'avg_margin_30d' | 'stock_turn_30d' | 'status'; // Add more as needed
interface BrandMetric {
// Assuming brand_name is unique primary identifier in brand_metrics
brand_id: string | number;
brand_name: string;
last_calculated: string;
product_count: number;
@@ -37,7 +39,12 @@ interface BrandMetric {
lifetime_sales: number;
lifetime_revenue: string | number;
avg_margin_30d: string | number | null;
stock_turn_30d: string | number | null;
status: string;
brand_status: string;
description: string;
// Camel case versions
brandId: string | number;
brandName: string;
lastCalculated: string;
productCount: number;
@@ -49,6 +56,7 @@ interface BrandMetric {
lifetimeSales: number;
lifetimeRevenue: string | number;
avgMargin_30d: string | number | null;
stockTurn_30d: string | number | null;
}
// Define response type to avoid type errors
@@ -62,11 +70,13 @@ interface BrandResponse {
};
}
// Filter options are just a list of names, not useful for dropdowns here
// interface BrandFilterOptions { brands: string[]; }
interface BrandFilterOptions {
statuses: string[];
}
interface BrandStats {
totalBrands: number;
activeBrands: number;
totalActiveProducts: number; // SUM(active_product_count)
totalValue: number; // SUM(current_stock_cost)
avgMargin: number; // Weighted avg margin 30d
@@ -74,7 +84,8 @@ interface BrandStats {
interface BrandFilters {
search: string;
showInactive: boolean; // New filter for showing brands with 0 active products
status: string;
showInactive: boolean; // Show brands with 0 active products
}
const ITEMS_PER_PAGE = 50;
@@ -129,6 +140,19 @@ const formatPercentage = (value: number | string | null | undefined, digits = 1)
return `${value.toFixed(digits)}%`;
};
const getStatusVariant = (status: string): "default" | "secondary" | "outline" | "destructive" => {
switch (status) {
case 'active':
return 'default';
case 'inactive':
return 'secondary';
case 'discontinued':
return 'destructive';
default:
return 'outline';
}
};
export function Brands() {
const [page, setPage] = useState(1);
const [limit] = useState(ITEMS_PER_PAGE);
@@ -136,6 +160,7 @@ export function Brands() {
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const [filters, setFilters] = useState<BrandFilters>({
search: "",
status: "all",
showInactive: false, // Default to hiding brands with 0 active products
});
@@ -151,6 +176,9 @@ export function Brands() {
if (filters.search) {
params.set('brandName_ilike', filters.search); // Filter by name
}
if (filters.status !== 'all') {
params.set('status', filters.status); // Filter by status
}
if (!filters.showInactive) {
params.set('activeProductCount_gt', '0'); // Only show brands with active products
}
@@ -184,8 +212,17 @@ export function Brands() {
},
});
// Filter options query might not be needed if only search is used
// const { data: filterOptions, isLoading: isLoadingFilterOptions } = useQuery<BrandFilterOptions, Error>({ ... });
// Fetch filter options
const { data: filterOptions } = useQuery<BrandFilterOptions, Error>({
queryKey: ['brandsFilterOptions'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/brands-aggregate/filter-options`, {
credentials: 'include'
});
if (!response.ok) throw new Error("Failed to fetch filter options");
return response.json();
},
});
// --- Event Handlers ---
@@ -236,7 +273,8 @@ export function Brands() {
<CardContent>
{isLoadingStats ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{formatNumber(statsData?.totalBrands)}</div>}
<p className="text-xs text-muted-foreground">
All brands with metrics
{isLoadingStats ? <Skeleton className="h-4 w-28" /> :
`${formatNumber(statsData?.activeBrands)} active`}
</p>
</CardContent>
</Card>
@@ -276,22 +314,36 @@ export function Brands() {
</motion.div>
{/* Filter Controls */}
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center justify-between space-x-2">
<Input
placeholder="Search brands..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
className="w-[150px] lg:w-[250px]"
<div className="flex flex-wrap items-center space-y-2 sm:space-y-0 sm:space-x-2">
<Input
placeholder="Search brands..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
className="w-full sm:w-[250px]"
/>
<Select
value={filters.status}
onValueChange={(value) => handleFilterChange('status', value)}
>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
{filterOptions?.statuses?.map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center space-x-2 ml-auto">
<Switch
id="show-inactive-brands"
checked={filters.showInactive}
onCheckedChange={(checked) => handleFilterChange('showInactive', checked)}
/>
<div className="flex items-center space-x-2">
<Switch
id="show-inactive-brands"
checked={filters.showInactive}
onCheckedChange={(checked) => handleFilterChange('showInactive', checked)}
/>
<Label htmlFor="show-inactive-brands">Show brands with no active products</Label>
</div>
<Label htmlFor="show-inactive-brands">Show brands with no active products</Label>
</div>
</div>
@@ -308,6 +360,8 @@ export function Brands() {
<TableHead onClick={() => handleSort("revenue_30d")} className="cursor-pointer text-right">Revenue (30d)</TableHead>
<TableHead onClick={() => handleSort("profit_30d")} className="cursor-pointer text-right">Profit (30d)</TableHead>
<TableHead onClick={() => handleSort("avg_margin_30d")} className="cursor-pointer text-right">Margin (30d)</TableHead>
<TableHead onClick={() => handleSort("stock_turn_30d")} className="cursor-pointer text-right">Stock Turn (30d)</TableHead>
<TableHead onClick={() => handleSort("status")} className="cursor-pointer text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -322,23 +376,25 @@ export function Brands() {
<TableCell className="text-right"><Skeleton className="h-5 w-20 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-20 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
</TableRow>
))
) : listError ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-destructive">
<TableCell colSpan={9} className="text-center py-8 text-destructive">
Error loading brands: {listError.message}
</TableCell>
</TableRow>
) : brands.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
No brands found matching your criteria.
</TableCell>
</TableRow>
) : (
brands.map((brand: BrandMetric) => (
<TableRow key={brand.brand_name}> {/* Use brand_name as key */}
<TableRow key={brand.brand_id} className={brand.active_product_count === 0 ? "opacity-60" : ""}>
<TableCell className="font-medium">{brand.brand_name}</TableCell>
<TableCell className="text-right">{formatNumber(brand.active_product_count || brand.activeProductCount)}</TableCell>
<TableCell className="text-right">{formatNumber(brand.current_stock_units || brand.currentStockUnits)}</TableCell>
@@ -347,6 +403,12 @@ export function Brands() {
<TableCell className="text-right">{formatCurrency(brand.revenue_30d as number)}</TableCell>
<TableCell className="text-right">{formatCurrency(brand.profit_30d as number)}</TableCell>
<TableCell className="text-right">{formatPercentage(brand.avg_margin_30d as number)}</TableCell>
<TableCell className="text-right">{formatNumber(brand.stock_turn_30d, 2)}</TableCell>
<TableCell className="text-right">
<Badge variant={getStatusVariant(brand.status)}>
{brand.status || 'Unknown'}
</Badge>
</TableCell>
</TableRow>
))
)}

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@ import { useState, useMemo, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
// import { Badge } from "@/components/ui/badge"; // Badge removed as status/performance filters are gone
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; // Select removed as filters are gone
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { motion } from "framer-motion";
import config from "../config";
@@ -16,10 +16,10 @@ import { Label } from "@/components/ui/label";
type VendorSortableColumns =
| 'vendorName' | 'productCount' | 'activeProductCount' | 'currentStockUnits'
| 'currentStockCost' | 'onOrderUnits' | 'onOrderCost' | 'avgLeadTimeDays'
| 'revenue_30d' | 'profit_30d' | 'avg_margin_30d' | 'po_count_365d';
| 'revenue_30d' | 'profit_30d' | 'avg_margin_30d' | 'po_count_365d' | 'status';
interface VendorMetric {
// Assuming vendor_name is unique primary identifier
vendor_id: string | number;
vendor_name: string;
last_calculated: string;
product_count: number;
@@ -43,7 +43,16 @@ interface VendorMetric {
lifetime_sales: number;
lifetime_revenue: string | number;
avg_margin_30d: string | number | null;
// New fields added by vendorsAggregate
status: string;
vendor_status: string;
cost_metrics_30d: {
avg_unit_cost: number;
total_spend: number;
order_count: number;
};
// Camel case versions
vendorId: string | number;
vendorName: string;
lastCalculated: string;
productCount: number;
@@ -72,26 +81,27 @@ interface VendorResponse {
};
}
// Filter options are just a list of names, not useful for dropdowns here
// interface VendorFilterOptions { vendors: string[]; }
interface VendorFilterOptions {
statuses: string[];
}
interface VendorStats {
totalVendors: number;
totalActiveProducts: number; // This seems to be SUM(active_product_count) per vendor
totalValue: number; // SUM(current_stock_cost)
totalOnOrderValue: number; // SUM(on_order_cost)
avgLeadTime: number; // AVG(avg_lead_time_days)
activeVendors: number;
totalActiveProducts: number;
totalValue: number;
totalOnOrderValue: number;
avgLeadTime: number;
}
interface VendorFilters {
search: string;
showInactive: boolean; // New filter for showing vendors with 0 active products
// Status and Performance filters removed
status: string;
showInactive: boolean;
}
const ITEMS_PER_PAGE = 50;
// Re-use formatting helpers from Categories or define here
const formatCurrency = (value: number | string | null | undefined, digits = 0): string => {
if (value == null) return 'N/A';
if (typeof value === 'string') {
@@ -152,6 +162,19 @@ const formatDays = (value: number | string | null | undefined, digits = 1): stri
return `${value.toFixed(digits)} days`;
};
const getStatusVariant = (status: string): "default" | "secondary" | "outline" | "destructive" => {
switch (status) {
case 'active':
return 'default';
case 'inactive':
return 'secondary';
case 'discontinued':
return 'destructive';
default:
return 'outline';
}
};
export function Vendors() {
const [page, setPage] = useState(1);
const [limit] = useState(ITEMS_PER_PAGE);
@@ -159,6 +182,7 @@ export function Vendors() {
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const [filters, setFilters] = useState<VendorFilters>({
search: "",
status: "all",
showInactive: false, // Default to hiding vendors with 0 active products
});
@@ -174,10 +198,12 @@ export function Vendors() {
if (filters.search) {
params.set('vendorName_ilike', filters.search); // Filter by name
}
if (filters.status !== 'all') {
params.set('status', filters.status); // Filter by status
}
if (!filters.showInactive) {
params.set('activeProductCount_gt', '0'); // Only show vendors with active products
}
// Add more filters here if needed (e.g., avgLeadTimeDays_lte=10)
return params;
}, [page, limit, sortColumn, sortDirection, filters]);
@@ -189,9 +215,7 @@ export function Vendors() {
credentials: 'include'
});
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
const data = await response.json();
console.log('Vendors data:', JSON.stringify(data, null, 2));
return data;
return response.json();
},
placeholderData: (prev) => prev, // Modern replacement for keepPreviousData
});
@@ -207,8 +231,17 @@ export function Vendors() {
},
});
// Filter options query might not be needed if only search is used
// const { data: filterOptions, isLoading: isLoadingFilterOptions } = useQuery<VendorFilterOptions, Error>({ ... });
// Fetch filter options
const { data: filterOptions, isLoading: isLoadingFilterOptions } = useQuery<VendorFilterOptions, Error>({
queryKey: ['vendorsFilterOptions'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/vendors-aggregate/filter-options`, {
credentials: 'include'
});
if (!response.ok) throw new Error("Failed to fetch filter options");
return response.json();
},
});
// --- Event Handlers ---
@@ -257,10 +290,10 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
</CardHeader>
<CardContent>
{isLoadingStats ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{formatNumber(statsData?.totalVendors)}</div>}
{isLoadingStats ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{formatNumber(statsData?.totalVendors)}</div>}
<p className="text-xs text-muted-foreground">
{/* Active vendor count not directly available, showing total */}
All vendors with metrics
{isLoadingStats ? <Skeleton className="h-4 w-28" /> :
`${formatNumber(statsData?.activeVendors)} active`}
</p>
</CardContent>
</Card>
@@ -269,9 +302,9 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Total Stock Value</CardTitle>
</CardHeader>
<CardContent>
{isLoadingStats ? <Skeleton className="h-8 w-28" /> : <div className="text-2xl font-bold">{formatCurrency(statsData?.totalValue)}</div>}
{isLoadingStats ? <Skeleton className="h-8 w-28" /> : <div className="text-2xl font-bold">{formatCurrency(statsData?.totalValue)}</div>}
<p className="text-xs text-muted-foreground">
Current cost value
Current cost value
</p>
</CardContent>
</Card>
@@ -281,67 +314,79 @@ export function Vendors() {
</CardHeader>
<CardContent>
{isLoadingStats ? <Skeleton className="h-8 w-28" /> : <div className="text-2xl font-bold">{formatCurrency(statsData?.totalOnOrderValue)}</div>}
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground">
Total cost on open POs
</p>
</CardContent>
</Card>
<Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Avg Lead Time</CardTitle>
</CardHeader>
<CardContent>
{isLoadingStats ? <Skeleton className="h-8 w-20" /> : <div className="text-2xl font-bold">{formatDays(statsData?.avgLeadTime)}</div>}
{isLoadingStats ? <Skeleton className="h-8 w-20" /> : <div className="text-2xl font-bold">{formatDays(statsData?.avgLeadTime)}</div>}
<p className="text-xs text-muted-foreground">
Average across vendors
</p>
</CardContent>
</Card>
{/* Note: Total Spend and Performance cards removed */}
</motion.div>
{/* Filter Controls */}
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center justify-between space-x-2">
<Input
placeholder="Search vendors..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
className="w-[150px] lg:w-[250px]"
<div className="flex flex-wrap items-center space-y-2 sm:space-y-0 sm:space-x-2">
<Input
placeholder="Search vendors..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
className="w-full sm:w-[250px]"
/>
<Select
value={filters.status}
onValueChange={(value) => handleFilterChange('status', value)}
>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
{filterOptions?.statuses?.map((status) => (
<SelectItem key={status} value={status}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center space-x-2 ml-auto">
<Switch
id="show-inactive-vendors"
checked={filters.showInactive}
onCheckedChange={(checked) => handleFilterChange('showInactive', checked)}
/>
<div className="flex items-center space-x-2">
<Switch
id="show-inactive-vendors"
checked={filters.showInactive}
onCheckedChange={(checked) => handleFilterChange('showInactive', checked)}
/>
<Label htmlFor="show-inactive-vendors">Show vendors with no active products</Label>
</div>
{/* Note: Status and Performance Select dropdowns removed */}
<Label htmlFor="show-inactive-vendors">Show vendors with no active products</Label>
</div>
</div>
{/* Data Table */}
<div className="rounded-md border">
{/* Data Table */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead onClick={() => handleSort("vendorName")} className="cursor-pointer">Vendor</TableHead>
<TableHead onClick={() => handleSort("activeProductCount")} className="cursor-pointer text-right">Active Prod.</TableHead>
<TableHead onClick={() => handleSort("currentStockCost")} className="cursor-pointer text-right">Stock Value</TableHead>
<TableHead onClick={() => handleSort("onOrderUnits")} className="cursor-pointer text-right">On Order (Units)</TableHead>
<TableHead onClick={() => handleSort("onOrderCost")} className="cursor-pointer text-right">On Order (Cost)</TableHead>
<TableHead onClick={() => handleSort("avgLeadTimeDays")} className="cursor-pointer text-right">Avg Lead Time</TableHead>
<TableHead onClick={() => handleSort("revenue_30d")} className="cursor-pointer text-right">Revenue (30d)</TableHead>
<TableHead onClick={() => handleSort("profit_30d")} className="cursor-pointer text-right">Profit (30d)</TableHead>
<TableHead onClick={() => handleSort("avg_margin_30d")} className="cursor-pointer text-right">Margin (30d)</TableHead>
<TableHead onClick={() => handleSort("po_count_365d")} className="cursor-pointer text-right">POs (365d)</TableHead>
{/* Removed: Status, On-Time %, Fill Rate, Avg Unit Cost, Total Spend, Orders */}
<TableHead onClick={() => handleSort("vendorName")} className="cursor-pointer">Vendor</TableHead>
<TableHead onClick={() => handleSort("activeProductCount")} className="cursor-pointer text-right">Active Prod.</TableHead>
<TableHead onClick={() => handleSort("currentStockCost")} className="cursor-pointer text-right">Stock Value</TableHead>
<TableHead onClick={() => handleSort("onOrderUnits")} className="cursor-pointer text-right">On Order (Units)</TableHead>
<TableHead onClick={() => handleSort("onOrderCost")} className="cursor-pointer text-right">On Order (Cost)</TableHead>
<TableHead onClick={() => handleSort("avgLeadTimeDays")} className="cursor-pointer text-right">Avg Lead Time</TableHead>
<TableHead onClick={() => handleSort("revenue_30d")} className="cursor-pointer text-right">Revenue (30d)</TableHead>
<TableHead onClick={() => handleSort("profit_30d")} className="cursor-pointer text-right">Profit (30d)</TableHead>
<TableHead onClick={() => handleSort("avg_margin_30d")} className="cursor-pointer text-right">Margin (30d)</TableHead>
<TableHead onClick={() => handleSort("po_count_365d")} className="cursor-pointer text-right">POs (365d)</TableHead>
<TableHead onClick={() => handleSort("status")} className="cursor-pointer text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoadingList && !listData ? (
Array.from({ length: 5 }).map((_, i) => ( // Skeleton rows
Array.from({ length: 5 }).map((_, i) => ( // Skeleton rows
<TableRow key={`skel-${i}`}>
<TableCell><Skeleton className="h-5 w-40" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
@@ -353,23 +398,24 @@ export function Vendors() {
<TableCell className="text-right"><Skeleton className="h-5 w-20 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
<TableCell className="text-right"><Skeleton className="h-5 w-16 ml-auto" /></TableCell>
</TableRow>
))
))
) : listError ? (
<TableRow>
<TableCell colSpan={10} className="text-center py-8 text-destructive">
<TableCell colSpan={11} className="text-center py-8 text-destructive">
Error loading vendors: {listError.message}
</TableCell>
</TableRow>
) : vendors.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
<TableCell colSpan={11} className="text-center py-8 text-muted-foreground">
No vendors found matching your criteria.
</TableCell>
</TableRow>
) : (
vendors.map((vendor: VendorMetric) => (
<TableRow key={vendor.vendor_name}> {/* Use vendor_name as key assuming it's unique */}
<TableRow key={vendor.vendor_id} className={vendor.active_product_count === 0 ? "opacity-60" : ""}>
<TableCell className="font-medium">{vendor.vendor_name}</TableCell>
<TableCell className="text-right">{formatNumber(vendor.active_product_count || vendor.activeProductCount)}</TableCell>
<TableCell className="text-right">{formatCurrency(vendor.current_stock_cost as number)}</TableCell>
@@ -380,6 +426,11 @@ export function Vendors() {
<TableCell className="text-right">{formatCurrency(vendor.profit_30d as number)}</TableCell>
<TableCell className="text-right">{formatPercentage(vendor.avg_margin_30d as number)}</TableCell>
<TableCell className="text-right">{formatNumber(vendor.po_count_365d || vendor.poCount_365d)}</TableCell>
<TableCell className="text-right">
<Badge variant={getStatusVariant(vendor.status)}>
{vendor.status || 'Unknown'}
</Badge>
</TableCell>
</TableRow>
))
)}
@@ -387,10 +438,10 @@ export function Vendors() {
</Table>
</div>
{/* Pagination Controls */}
{/* Pagination Controls */}
{totalPages > 1 && pagination && (
<motion.div layout="position" transition={{ duration: 0.15 }} className="flex justify-center">
<Pagination>
<div className="flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
@@ -400,7 +451,7 @@ export function Vendors() {
className={pagination.currentPage === 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
{[...Array(totalPages)].map((_, i) => (
{[...Array(totalPages)].map((_, i) => (
<PaginationItem key={i + 1}>
<PaginationLink
href="#"
@@ -410,18 +461,18 @@ export function Vendors() {
{i + 1}
</PaginationLink>
</PaginationItem>
))}
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => { e.preventDefault(); handlePageChange(pagination.currentPage + 1); }}
aria-disabled={pagination.currentPage >= totalPages}
className={pagination.currentPage >= totalPages ? "pointer-events-none opacity-50" : ""}
className={pagination.currentPage >= totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</motion.div>
</div>
)}
</motion.div>
);