Align badges and fix search
This commit is contained in:
@@ -19,7 +19,7 @@ router.get('/', async (req, res) => {
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(c.name LIKE ? OR c.description LIKE ?)');
|
||||
whereConditions.push('(LOWER(c.name) LIKE LOWER(?) OR LOWER(c.description) LIKE LOWER(?))');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ router.get('/', async (req, res) => {
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
whereConditions.push('(p.vendor LIKE ? OR vd.contact_name LIKE ?)');
|
||||
whereConditions.push('(LOWER(p.vendor) LIKE LOWER(?) OR LOWER(vd.contact_name) LIKE LOWER(?))');
|
||||
params.push(`%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
@@ -37,27 +37,101 @@ export function Categories() {
|
||||
parent: "all",
|
||||
performance: "all",
|
||||
});
|
||||
const [sorting, setSorting] = useState({
|
||||
column: 'name',
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["categories", page, sortColumn, sortDirection, filters],
|
||||
queryKey: ["categories"],
|
||||
queryFn: async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: "50",
|
||||
sortColumn: sortColumn.toString(),
|
||||
sortDirection,
|
||||
...filters.search && { search: filters.search },
|
||||
...filters.parent !== "all" && { parent: filters.parent },
|
||||
...filters.performance !== "all" && { performance: filters.performance },
|
||||
});
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/categories?${searchParams}`);
|
||||
const response = await fetch(`${config.apiUrl}/categories`);
|
||||
if (!response.ok) throw new Error("Failed to fetch categories");
|
||||
return response.json();
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
// Filter and sort the data client-side
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data?.categories) return [];
|
||||
|
||||
let filtered = [...data.categories];
|
||||
|
||||
// Apply search filter
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
filtered = filtered.filter(category =>
|
||||
category.name.toLowerCase().includes(searchLower) ||
|
||||
(category.description?.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Apply parent filter
|
||||
if (filters.parent !== 'all') {
|
||||
if (filters.parent === 'none') {
|
||||
filtered = filtered.filter(category => !category.parent_category);
|
||||
} else {
|
||||
filtered = filtered.filter(category => category.parent_category === filters.parent);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply performance filter
|
||||
if (filters.performance !== 'all') {
|
||||
filtered = filtered.filter(category => {
|
||||
const growth = category.growth_rate ?? 0;
|
||||
switch (filters.performance) {
|
||||
case 'high_growth': return growth >= 20;
|
||||
case 'growing': return growth >= 5 && growth < 20;
|
||||
case 'stable': return growth >= -5 && growth < 5;
|
||||
case 'declining': return growth < -5;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
const aVal = a[sortColumn];
|
||||
const bVal = b[sortColumn];
|
||||
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
}
|
||||
|
||||
const aStr = String(aVal || '');
|
||||
const bStr = String(bVal || '');
|
||||
return sortDirection === 'asc' ?
|
||||
aStr.localeCompare(bStr) :
|
||||
bStr.localeCompare(aStr);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [data?.categories, filters, sortColumn, sortDirection]);
|
||||
|
||||
// Calculate pagination
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (page - 1) * 50;
|
||||
return filteredData.slice(startIndex, startIndex + 50);
|
||||
}, [filteredData, page]);
|
||||
|
||||
// Calculate stats from filtered data
|
||||
const stats = useMemo(() => {
|
||||
if (!filteredData.length) return data?.stats;
|
||||
|
||||
const activeCategories = filteredData.filter(c => c.status === 'active').length;
|
||||
const totalValue = filteredData.reduce((sum, c) => sum + (c.total_value || 0), 0);
|
||||
const margins = filteredData.map(c => c.avg_margin || 0).filter(m => m !== 0);
|
||||
const growthRates = filteredData.map(c => c.growth_rate || 0).filter(g => g !== 0);
|
||||
|
||||
return {
|
||||
totalCategories: filteredData.length,
|
||||
activeCategories,
|
||||
totalValue,
|
||||
avgMargin: margins.length ? margins.reduce((a, b) => a + b, 0) / margins.length : 0,
|
||||
avgGrowth: growthRates.length ? growthRates.reduce((a, b) => a + b, 0) / growthRates.length : 0
|
||||
};
|
||||
}, [filteredData, data?.stats]);
|
||||
|
||||
const handleSort = (column: keyof Category) => {
|
||||
setSortDirection(prev => {
|
||||
if (sortColumn !== column) return "asc";
|
||||
@@ -83,23 +157,40 @@ export function Categories() {
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div layout className="container mx-auto py-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<motion.div
|
||||
layout
|
||||
transition={{
|
||||
layout: {
|
||||
duration: 0.15,
|
||||
ease: [0.4, 0, 0.2, 1] // Material Design easing
|
||||
}
|
||||
}}
|
||||
className="container mx-auto py-6 space-y-4"
|
||||
>
|
||||
<motion.div
|
||||
layout="position"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Categories</h1>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data?.pagination.total.toLocaleString() ?? "..."} categories
|
||||
{filteredData.length.toLocaleString()} categories
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<motion.div
|
||||
layout="preserve-aspect"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="grid gap-4 md:grid-cols-4"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Categories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{data?.stats?.totalCategories ?? "..."}</div>
|
||||
<div className="text-2xl font-bold">{stats?.totalCategories ?? "..."}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{data?.stats?.activeCategories ?? "..."} active
|
||||
{stats?.activeCategories ?? "..."} active
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -139,7 +230,7 @@ export function Categories() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
@@ -203,7 +294,7 @@ export function Categories() {
|
||||
Loading categories...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.categories?.map((category: Category) => (
|
||||
) : paginatedData.map((category: Category) => (
|
||||
<TableRow key={category.category_id}>
|
||||
<TableCell>
|
||||
<div className="font-medium">{category.name}</div>
|
||||
@@ -215,15 +306,17 @@ export function Categories() {
|
||||
<TableCell>{typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}%</TableCell>
|
||||
<TableCell>{typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
|
||||
<div className="flex items-center gap-2" style={{ minWidth: '120px' }}>
|
||||
<div style={{ width: '50px', textAlign: 'right' }}>
|
||||
{typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
|
||||
</div>
|
||||
{getPerformanceBadge(category.growth_rate ?? 0)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{category.status}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!isLoading && !data?.categories.length && (
|
||||
{!isLoading && !paginatedData.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||
No categories found
|
||||
@@ -234,8 +327,12 @@ export function Categories() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{data?.pagination.pages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
{filteredData.length > 0 && (
|
||||
<motion.div
|
||||
layout="position"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
@@ -243,22 +340,22 @@ export function Categories() {
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.max(1, p - 1));
|
||||
if (page > 1) setPage(p => p - 1);
|
||||
}}
|
||||
aria-disabled={page === 1}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
|
||||
<PaginationItem key={p}>
|
||||
{Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
|
||||
<PaginationItem key={i + 1}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={page === p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p);
|
||||
setPage(i + 1);
|
||||
}}
|
||||
isActive={page === i + 1}
|
||||
>
|
||||
{p}
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
@@ -267,14 +364,14 @@ export function Categories() {
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.min(data.pagination.pages, p + 1));
|
||||
if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1);
|
||||
}}
|
||||
aria-disabled={page === data.pagination.pages}
|
||||
aria-disabled={page >= Math.ceil(filteredData.length / 50)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, keepPreviousData } from "@tanstack/react-query";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
@@ -38,27 +38,98 @@ export function Vendors() {
|
||||
status: "all",
|
||||
performance: "all",
|
||||
});
|
||||
const [sorting, setSorting] = useState({
|
||||
column: 'name',
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["vendors", page, sortColumn, sortDirection, filters],
|
||||
queryKey: ["vendors"],
|
||||
queryFn: async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: "50",
|
||||
sortColumn: sortColumn.toString(),
|
||||
sortDirection,
|
||||
...filters.search && { search: filters.search },
|
||||
...filters.status !== "all" && { status: filters.status },
|
||||
...filters.performance !== "all" && { performance: filters.performance },
|
||||
});
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/vendors?${searchParams}`);
|
||||
const response = await fetch(`${config.apiUrl}/vendors`);
|
||||
if (!response.ok) throw new Error("Failed to fetch vendors");
|
||||
return response.json();
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
// Filter and sort the data client-side
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data?.vendors) return [];
|
||||
|
||||
let filtered = [...data.vendors];
|
||||
|
||||
// Apply search filter
|
||||
if (filters.search) {
|
||||
const searchLower = filters.search.toLowerCase();
|
||||
filtered = filtered.filter(vendor =>
|
||||
vendor.name.toLowerCase().includes(searchLower) ||
|
||||
vendor.contact_name?.toLowerCase().includes(searchLower) ||
|
||||
vendor.email?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply status filter
|
||||
if (filters.status !== 'all') {
|
||||
filtered = filtered.filter(vendor => vendor.status === filters.status);
|
||||
}
|
||||
|
||||
// Apply performance filter
|
||||
if (filters.performance !== 'all') {
|
||||
filtered = filtered.filter(vendor => {
|
||||
const fillRate = vendor.order_fill_rate ?? 0;
|
||||
switch (filters.performance) {
|
||||
case 'excellent': return fillRate >= 95;
|
||||
case 'good': return fillRate >= 85 && fillRate < 95;
|
||||
case 'fair': return fillRate >= 75 && fillRate < 85;
|
||||
case 'poor': return fillRate < 75;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
const aVal = a[sortColumn];
|
||||
const bVal = b[sortColumn];
|
||||
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
}
|
||||
|
||||
const aStr = String(aVal || '');
|
||||
const bStr = String(bVal || '');
|
||||
return sortDirection === 'asc' ?
|
||||
aStr.localeCompare(bStr) :
|
||||
bStr.localeCompare(aStr);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [data?.vendors, filters, sortColumn, sortDirection]);
|
||||
|
||||
// Calculate pagination
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (page - 1) * 50;
|
||||
return filteredData.slice(startIndex, startIndex + 50);
|
||||
}, [filteredData, page]);
|
||||
|
||||
// Calculate stats from filtered data
|
||||
const stats = useMemo(() => {
|
||||
if (!filteredData.length) return data?.stats;
|
||||
|
||||
const activeVendors = filteredData.filter(v => v.status === 'active').length;
|
||||
const leadTimes = filteredData.map(v => v.avg_lead_time_days || 0).filter(lt => lt !== 0);
|
||||
const fillRates = filteredData.map(v => v.order_fill_rate || 0).filter(fr => fr !== 0);
|
||||
const onTimeRates = filteredData.map(v => v.on_time_delivery_rate || 0).filter(otr => otr !== 0);
|
||||
|
||||
return {
|
||||
totalVendors: filteredData.length,
|
||||
activeVendors,
|
||||
avgLeadTime: leadTimes.length ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0,
|
||||
avgFillRate: fillRates.length ? fillRates.reduce((a, b) => a + b, 0) / fillRates.length : 0,
|
||||
avgOnTimeDelivery: onTimeRates.length ? onTimeRates.reduce((a, b) => a + b, 0) / onTimeRates.length : 0
|
||||
};
|
||||
}, [filteredData, data?.stats]);
|
||||
|
||||
const handleSort = (column: keyof Vendor) => {
|
||||
setSortDirection(prev => {
|
||||
if (sortColumn !== column) return "asc";
|
||||
@@ -75,23 +146,40 @@ export function Vendors() {
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div layout className="container mx-auto py-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<motion.div
|
||||
layout
|
||||
transition={{
|
||||
layout: {
|
||||
duration: 0.15,
|
||||
ease: [0.4, 0, 0.2, 1] // Material Design easing
|
||||
}
|
||||
}}
|
||||
className="container mx-auto py-6 space-y-4"
|
||||
>
|
||||
<motion.div
|
||||
layout="position"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Vendors</h1>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data?.pagination.total.toLocaleString() ?? "..."} vendors
|
||||
{filteredData.length.toLocaleString()} vendors
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<motion.div
|
||||
layout="preserve-aspect"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="grid gap-4 md:grid-cols-4"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{data?.stats?.totalVendors ?? "..."}</div>
|
||||
<div className="text-2xl font-bold">{stats?.totalVendors ?? "..."}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{data?.stats?.activeVendors ?? "..."} active
|
||||
{stats?.activeVendors ?? "..."} active
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -101,7 +189,7 @@ export function Vendors() {
|
||||
<CardTitle className="text-sm font-medium">Avg Lead Time</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{typeof data?.stats?.avgLeadTime === 'number' ? data.stats.avgLeadTime.toFixed(1) : "..."} days</div>
|
||||
<div className="text-2xl font-bold">{typeof stats?.avgLeadTime === 'number' ? stats.avgLeadTime.toFixed(1) : "..."} days</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Across all vendors
|
||||
</p>
|
||||
@@ -113,7 +201,7 @@ export function Vendors() {
|
||||
<CardTitle className="text-sm font-medium">Fill Rate</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{typeof data?.stats?.avgFillRate === 'number' ? data.stats.avgFillRate.toFixed(1) : "..."}%</div>
|
||||
<div className="text-2xl font-bold">{typeof stats?.avgFillRate === 'number' ? stats.avgFillRate.toFixed(1) : "..."}%</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Average order fill rate
|
||||
</p>
|
||||
@@ -125,13 +213,13 @@ export function Vendors() {
|
||||
<CardTitle className="text-sm font-medium">On-Time Delivery</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{typeof data?.stats?.avgOnTimeDelivery === 'number' ? data.stats.avgOnTimeDelivery.toFixed(1) : "..."}%</div>
|
||||
<div className="text-2xl font-bold">{typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}%</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Average on-time rate
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
@@ -194,7 +282,7 @@ export function Vendors() {
|
||||
Loading vendors...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.vendors?.map((vendor: Vendor) => (
|
||||
) : paginatedData.map((vendor: Vendor) => (
|
||||
<TableRow key={vendor.vendor_id}>
|
||||
<TableCell className="font-medium">{vendor.name}</TableCell>
|
||||
<TableCell>
|
||||
@@ -205,8 +293,10 @@ export function Vendors() {
|
||||
<TableCell>{typeof vendor.avg_lead_time_days === 'number' ? vendor.avg_lead_time_days.toFixed(1) : "0.0"} days</TableCell>
|
||||
<TableCell>{typeof vendor.on_time_delivery_rate === 'number' ? vendor.on_time_delivery_rate.toFixed(1) : "0.0"}%</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}%
|
||||
<div className="flex items-center gap-2" style={{ minWidth: '120px' }}>
|
||||
<div style={{ width: '50px', textAlign: 'right' }}>
|
||||
{typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}%
|
||||
</div>
|
||||
{getPerformanceBadge(vendor.order_fill_rate ?? 0)}
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -214,7 +304,7 @@ export function Vendors() {
|
||||
<TableCell>{vendor.active_products?.toLocaleString() ?? 0}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!isLoading && !data?.vendors.length && (
|
||||
{!isLoading && !paginatedData.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||
No vendors found
|
||||
@@ -225,8 +315,12 @@ export function Vendors() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{data?.pagination.pages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
{filteredData.length > 0 && (
|
||||
<motion.div
|
||||
layout="position"
|
||||
transition={{ duration: 0.15 }}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
@@ -234,22 +328,22 @@ export function Vendors() {
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.max(1, p - 1));
|
||||
if (page > 1) setPage(p => p - 1);
|
||||
}}
|
||||
aria-disabled={page === 1}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
|
||||
<PaginationItem key={p}>
|
||||
{Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
|
||||
<PaginationItem key={i + 1}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={page === p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p);
|
||||
setPage(i + 1);
|
||||
}}
|
||||
isActive={page === i + 1}
|
||||
>
|
||||
{p}
|
||||
{i + 1}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
@@ -258,14 +352,14 @@ export function Vendors() {
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPage(p => Math.min(data.pagination.pages, p + 1));
|
||||
if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1);
|
||||
}}
|
||||
aria-disabled={page === data.pagination.pages}
|
||||
aria-disabled={page >= Math.ceil(filteredData.length / 50)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user