Change filter sort to client side for vendors/cats

This commit is contained in:
2025-01-17 00:59:11 -05:00
parent 2235121761
commit 88d1189da0
4 changed files with 41 additions and 180 deletions

View File

@@ -1,66 +1,10 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
// Get categories with pagination, filtering, and sorting // Get all categories
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
const pool = req.app.locals.pool; const pool = req.app.locals.pool;
try { try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 50;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const parent = req.query.parent || 'all';
const performance = req.query.performance || 'all';
const sortColumn = req.query.sortColumn || 'name';
const sortDirection = req.query.sortDirection || 'asc';
// Build the WHERE clause based on filters
const whereConditions = [];
const params = [];
if (search) {
whereConditions.push('(LOWER(c.name) LIKE LOWER(?) OR LOWER(c.description) LIKE LOWER(?))');
params.push(`%${search}%`, `%${search}%`);
}
if (parent !== 'all') {
if (parent === 'none') {
whereConditions.push('c.parent_category IS NULL');
} else {
whereConditions.push('c.parent_category = ?');
params.push(parent);
}
}
if (performance !== 'all') {
switch (performance) {
case 'high_growth':
whereConditions.push('cm.growth_rate >= 20');
break;
case 'growing':
whereConditions.push('cm.growth_rate >= 5 AND cm.growth_rate < 20');
break;
case 'stable':
whereConditions.push('cm.growth_rate >= -5 AND cm.growth_rate < 5');
break;
case 'declining':
whereConditions.push('cm.growth_rate < -5');
break;
}
}
const whereClause = whereConditions.length > 0
? 'WHERE ' + whereConditions.join(' AND ')
: '';
// Get total count for pagination
const [countResult] = await pool.query(`
SELECT COUNT(DISTINCT c.id) as total
FROM categories c
LEFT JOIN category_metrics cm ON c.id = cm.category_id
${whereClause}
`, params);
// Get parent categories for filter dropdown // Get parent categories for filter dropdown
const [parentCategories] = await pool.query(` const [parentCategories] = await pool.query(`
SELECT DISTINCT parent_category SELECT DISTINCT parent_category
@@ -69,7 +13,7 @@ router.get('/', async (req, res) => {
ORDER BY parent_category ORDER BY parent_category
`); `);
// Get categories with metrics // Get all categories with metrics
const [categories] = await pool.query(` const [categories] = await pool.query(`
SELECT SELECT
c.id as category_id, c.id as category_id,
@@ -84,10 +28,8 @@ router.get('/', async (req, res) => {
cm.status cm.status
FROM categories c FROM categories c
LEFT JOIN category_metrics cm ON c.id = cm.category_id LEFT JOIN category_metrics cm ON c.id = cm.category_id
${whereClause} ORDER BY c.name ASC
ORDER BY ${sortColumn} ${sortDirection} `);
LIMIT ? OFFSET ?
`, [...params, limit, offset]);
// Get overall stats // Get overall stats
const [stats] = await pool.query(` const [stats] = await pool.query(`
@@ -116,11 +58,6 @@ router.get('/', async (req, res) => {
totalValue: parseFloat(stats[0].totalValue || 0), totalValue: parseFloat(stats[0].totalValue || 0),
avgMargin: parseFloat(stats[0].avgMargin || 0), avgMargin: parseFloat(stats[0].avgMargin || 0),
avgGrowth: parseFloat(stats[0].avgGrowth || 0) avgGrowth: parseFloat(stats[0].avgGrowth || 0)
},
pagination: {
total: countResult[0].total,
pages: Math.ceil(countResult[0].total / limit),
current: page,
} }
}); });
} catch (error) { } catch (error) {

View File

@@ -5,63 +5,7 @@ const router = express.Router();
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
const pool = req.app.locals.pool; const pool = req.app.locals.pool;
try { try {
const page = parseInt(req.query.page) || 1; // Get all vendors with metrics
const limit = parseInt(req.query.limit) || 50;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const status = req.query.status || 'all';
const performance = req.query.performance || 'all';
const sortColumn = req.query.sortColumn || 'name';
const sortDirection = req.query.sortDirection || 'asc';
// Build the WHERE clause based on filters
const whereConditions = ['p.vendor IS NOT NULL AND p.vendor != \'\''];
const params = [];
if (search) {
whereConditions.push('LOWER(p.vendor) LIKE LOWER(?)');
params.push(`%${search}%`);
}
if (status !== 'all') {
whereConditions.push(`
CASE
WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75 THEN 'active'
WHEN COALESCE(vm.total_orders, 0) > 0 THEN 'inactive'
ELSE 'pending'
END = ?
`);
params.push(status);
}
if (performance !== 'all') {
switch (performance) {
case 'excellent':
whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 95');
break;
case 'good':
whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 85 AND COALESCE(vm.order_fill_rate, 0) < 95');
break;
case 'fair':
whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 75 AND COALESCE(vm.order_fill_rate, 0) < 85');
break;
case 'poor':
whereConditions.push('COALESCE(vm.order_fill_rate, 0) < 75');
break;
}
}
const whereClause = 'WHERE ' + whereConditions.join(' AND ');
// Get total count for pagination
const [countResult] = await pool.query(`
SELECT COUNT(DISTINCT p.vendor) as total
FROM products p
LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor
${whereClause}
`, params);
// Get vendors with metrics
const [vendors] = await pool.query(` const [vendors] = await pool.query(`
SELECT DISTINCT SELECT DISTINCT
p.vendor as name, p.vendor as name,
@@ -77,13 +21,10 @@ router.get('/', async (req, res) => {
END as status END as status
FROM products p FROM products p
LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor
${whereClause} WHERE p.vendor IS NOT NULL AND p.vendor != ''
AND p.vendor IS NOT NULL AND p.vendor != '' `);
ORDER BY ${sortColumn} ${sortDirection}
LIMIT ? OFFSET ?
`, [...params, limit, offset]);
// Get cost metrics for these vendors // Get cost metrics for all vendors
const vendorNames = vendors.map(v => v.name); const vendorNames = vendors.map(v => v.name);
const [costMetrics] = await pool.query(` const [costMetrics] = await pool.query(`
SELECT SELECT
@@ -156,12 +97,6 @@ router.get('/', async (req, res) => {
avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery || 0), avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery || 0),
avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost || 0), avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost || 0),
totalSpend: parseFloat(overallCostMetrics[0].total_spend || 0) totalSpend: parseFloat(overallCostMetrics[0].total_spend || 0)
},
pagination: {
total: parseInt(countResult[0].total || 0),
currentPage: page,
pages: Math.ceil(parseInt(countResult[0].total || 0) / limit),
limit
} }
}); });
} catch (error) { } catch (error) {

View File

@@ -45,7 +45,9 @@ export function Categories() {
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["categories"], queryKey: ["categories"],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/categories`); const response = await fetch(`${config.apiUrl}/categories`, {
credentials: 'include'
});
if (!response.ok) throw new Error("Failed to fetch categories"); if (!response.ok) throw new Error("Failed to fetch categories");
return response.json(); return response.json();
}, },
@@ -109,9 +111,11 @@ export function Categories() {
}, [data?.categories, filters, sortColumn, sortDirection]); }, [data?.categories, filters, sortColumn, sortDirection]);
// Calculate pagination // Calculate pagination
const totalPages = Math.ceil(filteredData.length / 50);
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
const startIndex = (page - 1) * 50; const start = (page - 1) * 50;
return filteredData.slice(startIndex, startIndex + 50); const end = start + 50;
return filteredData.slice(start, end);
}, [filteredData, page]); }, [filteredData, page]);
// Calculate stats from filtered data // Calculate stats from filtered data
@@ -327,7 +331,7 @@ export function Categories() {
</Table> </Table>
</div> </div>
{filteredData.length > 0 && ( {totalPages > 1 && (
<motion.div <motion.div
layout="position" layout="position"
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
@@ -345,7 +349,7 @@ export function Categories() {
aria-disabled={page === 1} aria-disabled={page === 1}
/> />
</PaginationItem> </PaginationItem>
{Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => ( {Array.from({ length: totalPages }, (_, i) => (
<PaginationItem key={i + 1}> <PaginationItem key={i + 1}>
<PaginationLink <PaginationLink
href="#" href="#"
@@ -364,9 +368,9 @@ export function Categories() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1); if (page < totalPages) setPage(p => p + 1);
}} }}
aria-disabled={page >= Math.ceil(filteredData.length / 50)} aria-disabled={page >= totalPages}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>

View File

@@ -6,7 +6,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
import { motion } from "motion/react"; import { motion } from "framer-motion";
import config from "../config"; import config from "../config";
interface Vendor { interface Vendor {
@@ -28,6 +28,8 @@ interface VendorFilters {
performance: string; performance: string;
} }
const ITEMS_PER_PAGE = 50;
export function Vendors() { export function Vendors() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [sortColumn, setSortColumn] = useState<keyof Vendor>("name"); const [sortColumn, setSortColumn] = useState<keyof Vendor>("name");
@@ -37,24 +39,11 @@ export function Vendors() {
status: "all", status: "all",
performance: "all", performance: "all",
}); });
const [] = useState({
column: 'name',
direction: 'asc'
});
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["vendors", page, filters, sortColumn, sortDirection], queryKey: ["vendors"],
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams({ const response = await fetch(`${config.apiUrl}/vendors`, {
page: page.toString(),
limit: '50',
search: filters.search,
status: filters.status,
performance: filters.performance,
sortColumn,
sortDirection
});
const response = await fetch(`${config.apiUrl}/vendors?${params}`, {
credentials: 'include' credentials: 'include'
}); });
if (!response.ok) throw new Error("Failed to fetch vendors"); if (!response.ok) throw new Error("Failed to fetch vendors");
@@ -115,16 +104,12 @@ export function Vendors() {
}, [data?.vendors, filters, sortColumn, sortDirection]); }, [data?.vendors, filters, sortColumn, sortDirection]);
// Calculate pagination // Calculate pagination
const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE);
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
if (!data?.vendors) return []; const start = (page - 1) * ITEMS_PER_PAGE;
return data.vendors; const end = start + ITEMS_PER_PAGE;
}, [data?.vendors]); return filteredData.slice(start, end);
}, [filteredData, page]);
// Calculate stats from filtered data
const stats = useMemo(() => {
if (!data?.stats) return null;
return data.stats;
}, [data?.stats]);
const handleSort = (column: keyof Vendor) => { const handleSort = (column: keyof Vendor) => {
setSortDirection(prev => { setSortDirection(prev => {
@@ -147,7 +132,7 @@ export function Vendors() {
transition={{ transition={{
layout: { layout: {
duration: 0.15, duration: 0.15,
ease: [0.4, 0, 0.2, 1] // Material Design easing ease: [0.4, 0, 0.2, 1]
} }
}} }}
className="container mx-auto py-6 space-y-4" className="container mx-auto py-6 space-y-4"
@@ -173,9 +158,9 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle> <CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{stats?.totalVendors ?? "..."}</div> <div className="text-2xl font-bold">{data?.stats?.totalVendors ?? "..."}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{stats?.activeVendors ?? "..."} active {data?.stats?.activeVendors ?? "..."} active
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -186,10 +171,10 @@ export function Vendors() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
${typeof stats?.totalSpend === 'number' ? stats.totalSpend.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }) : "..."} ${typeof data?.stats?.totalSpend === 'number' ? data.stats.totalSpend.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }) : "..."}
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Avg unit cost: ${typeof stats?.avgUnitCost === 'number' ? stats.avgUnitCost.toFixed(2) : "..."} Avg unit cost: ${typeof data?.stats?.avgUnitCost === 'number' ? data.stats.avgUnitCost.toFixed(2) : "..."}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -199,9 +184,9 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Performance</CardTitle> <CardTitle className="text-sm font-medium">Performance</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof stats?.avgFillRate === 'number' ? stats.avgFillRate.toFixed(1) : "..."}%</div> <div className="text-2xl font-bold">{typeof data?.stats?.avgFillRate === 'number' ? data.stats.avgFillRate.toFixed(1) : "..."}%</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Fill rate / {typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}% on-time Fill rate / {typeof data?.stats?.avgOnTimeDelivery === 'number' ? data.stats.avgOnTimeDelivery.toFixed(1) : "..."}% on-time
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -211,7 +196,7 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Lead Time</CardTitle> <CardTitle className="text-sm font-medium">Lead Time</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof stats?.avgLeadTime === 'number' ? stats.avgLeadTime.toFixed(1) : "..."} days</div> <div className="text-2xl font-bold">{typeof data?.stats?.avgLeadTime === 'number' ? data.stats.avgLeadTime.toFixed(1) : "..."} days</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Average delivery time Average delivery time
</p> </p>
@@ -312,7 +297,7 @@ export function Vendors() {
</Table> </Table>
</div> </div>
{data?.pagination && data.pagination.total > 0 && ( {totalPages > 1 && (
<motion.div <motion.div
layout="position" layout="position"
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
@@ -330,7 +315,7 @@ export function Vendors() {
aria-disabled={page === 1} aria-disabled={page === 1}
/> />
</PaginationItem> </PaginationItem>
{Array.from({ length: data.pagination.pages }, (_, i) => ( {Array.from({ length: totalPages }, (_, i) => (
<PaginationItem key={i + 1}> <PaginationItem key={i + 1}>
<PaginationLink <PaginationLink
href="#" href="#"
@@ -349,9 +334,9 @@ export function Vendors() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (page < data.pagination.pages) setPage(p => p + 1); if (page < totalPages) setPage(p => p + 1);
}} }}
aria-disabled={page >= data.pagination.pages} aria-disabled={page >= totalPages}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>