diff --git a/inventory-server/src/routes/categories.js b/inventory-server/src/routes/categories.js
index bfa3d0a..7e13de5 100644
--- a/inventory-server/src/routes/categories.js
+++ b/inventory-server/src/routes/categories.js
@@ -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}%`);
}
diff --git a/inventory-server/src/routes/vendors.js b/inventory-server/src/routes/vendors.js
index 0bacf66..2a40eae 100644
--- a/inventory-server/src/routes/vendors.js
+++ b/inventory-server/src/routes/vendors.js
@@ -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}%`);
}
diff --git a/inventory/src/pages/Categories.tsx b/inventory/src/pages/Categories.tsx
index 6ecb17b..1d85cbb 100644
--- a/inventory/src/pages/Categories.tsx
+++ b/inventory/src/pages/Categories.tsx
@@ -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 (
-
-
+
+
Categories
- {data?.pagination.total.toLocaleString() ?? "..."} categories
+ {filteredData.length.toLocaleString()} categories
-
+
-
+
Total Categories
- {data?.stats?.totalCategories ?? "..."}
+ {stats?.totalCategories ?? "..."}
- {data?.stats?.activeCategories ?? "..."} active
+ {stats?.activeCategories ?? "..."} active
@@ -139,7 +230,7 @@ export function Categories() {
-
+
@@ -203,7 +294,7 @@ export function Categories() {
Loading categories...
- ) : data?.categories?.map((category: Category) => (
+ ) : paginatedData.map((category: Category) => (
{category.name}
@@ -215,15 +306,17 @@ export function Categories() {
{typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}%
{typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x
-
- {typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
+
+
+ {typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
+
{getPerformanceBadge(category.growth_rate ?? 0)}
{category.status}
))}
- {!isLoading && !data?.categories.length && (
+ {!isLoading && !paginatedData.length && (
No categories found
@@ -234,8 +327,12 @@ export function Categories() {
- {data?.pagination.pages > 1 && (
-
+ {filteredData.length > 0 && (
+
@@ -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}
/>
- {Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
-
+ {Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
+
{
e.preventDefault();
- setPage(p);
+ setPage(i + 1);
}}
+ isActive={page === i + 1}
>
- {p}
+ {i + 1}
))}
@@ -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)}
/>
-
+
)}
);
diff --git a/inventory/src/pages/Vendors.tsx b/inventory/src/pages/Vendors.tsx
index 75e4694..c671526 100644
--- a/inventory/src/pages/Vendors.tsx
+++ b/inventory/src/pages/Vendors.tsx
@@ -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 (
-
-
+
+
Vendors
- {data?.pagination.total.toLocaleString() ?? "..."} vendors
+ {filteredData.length.toLocaleString()} vendors
-
+
-
+
Total Vendors
- {data?.stats?.totalVendors ?? "..."}
+ {stats?.totalVendors ?? "..."}
- {data?.stats?.activeVendors ?? "..."} active
+ {stats?.activeVendors ?? "..."} active
@@ -101,7 +189,7 @@ export function Vendors() {
Avg Lead Time
- {typeof data?.stats?.avgLeadTime === 'number' ? data.stats.avgLeadTime.toFixed(1) : "..."} days
+ {typeof stats?.avgLeadTime === 'number' ? stats.avgLeadTime.toFixed(1) : "..."} days
Across all vendors
@@ -113,7 +201,7 @@ export function Vendors() {
Fill Rate
- {typeof data?.stats?.avgFillRate === 'number' ? data.stats.avgFillRate.toFixed(1) : "..."}%
+ {typeof stats?.avgFillRate === 'number' ? stats.avgFillRate.toFixed(1) : "..."}%
Average order fill rate
@@ -125,13 +213,13 @@ export function Vendors() {
On-Time Delivery
- {typeof data?.stats?.avgOnTimeDelivery === 'number' ? data.stats.avgOnTimeDelivery.toFixed(1) : "..."}%
+ {typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}%
Average on-time rate
-
+
@@ -194,7 +282,7 @@ export function Vendors() {
Loading vendors...
- ) : data?.vendors?.map((vendor: Vendor) => (
+ ) : paginatedData.map((vendor: Vendor) => (
{vendor.name}
@@ -205,8 +293,10 @@ export function Vendors() {
{typeof vendor.avg_lead_time_days === 'number' ? vendor.avg_lead_time_days.toFixed(1) : "0.0"} days
{typeof vendor.on_time_delivery_rate === 'number' ? vendor.on_time_delivery_rate.toFixed(1) : "0.0"}%
-
- {typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}%
+
+
+ {typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}%
+
{getPerformanceBadge(vendor.order_fill_rate ?? 0)}
@@ -214,7 +304,7 @@ export function Vendors() {
{vendor.active_products?.toLocaleString() ?? 0}
))}
- {!isLoading && !data?.vendors.length && (
+ {!isLoading && !paginatedData.length && (
No vendors found
@@ -225,8 +315,12 @@ export function Vendors() {
- {data?.pagination.pages > 1 && (
-
+ {filteredData.length > 0 && (
+
@@ -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}
/>
- {Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => (
-
+ {Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
+
{
e.preventDefault();
- setPage(p);
+ setPage(i + 1);
}}
+ isActive={page === i + 1}
>
- {p}
+ {i + 1}
))}
@@ -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)}
/>
-
+
)}
);