diff --git a/inventory-server/scripts/import/products.js b/inventory-server/scripts/import/products.js
index cc0a098..f1c6a2f 100644
--- a/inventory-server/scripts/import/products.js
+++ b/inventory-server/scripts/import/products.js
@@ -406,12 +406,7 @@ async function materializeCalculations(prodConnection, localConnection, incremen
WHERE oi.prod_pid = p.pid AND o.order_status >= 20) AS total_sold,
pls.date_sold as date_last_sold,
(SELECT iid FROM product_images WHERE pid = p.pid AND \`order\` = 255 LIMIT 1) AS primary_iid,
- GROUP_CONCAT(DISTINCT CASE
- WHEN pc.cat_id IS NOT NULL
- AND pc.type IN (10, 20, 11, 21, 12, 13)
- AND pci.cat_id NOT IN (16, 17)
- THEN pci.cat_id
- END) as category_ids
+ NULL as category_ids
FROM products p
LEFT JOIN shop_inventory si ON p.pid = si.pid AND si.store = 0
LEFT JOIN current_inventory ci ON p.pid = ci.pid
diff --git a/inventory-server/scripts/import/purchase-orders.js b/inventory-server/scripts/import/purchase-orders.js
index 1fe7563..e0a58f4 100644
--- a/inventory-server/scripts/import/purchase-orders.js
+++ b/inventory-server/scripts/import/purchase-orders.js
@@ -70,6 +70,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
DROP TABLE IF EXISTS temp_receivings;
DROP TABLE IF EXISTS temp_receiving_allocations;
DROP TABLE IF EXISTS employee_names;
+ DROP TABLE IF EXISTS temp_supplier_names;
-- Temporary table for purchase orders
CREATE TEMP TABLE temp_purchase_orders (
@@ -103,6 +104,8 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
receiving_created_date TIMESTAMP WITH TIME ZONE,
supplier_id INTEGER,
status TEXT,
+ sku TEXT,
+ name TEXT,
PRIMARY KEY (receiving_id, pid)
);
@@ -421,9 +424,12 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
rp.cost_each,
rp.received_by,
rp.received_date,
- r.date_created as receiving_created_date
+ r.date_created as receiving_created_date,
+ COALESCE(p.itemnumber, 'NO-SKU') AS sku,
+ COALESCE(p.description, 'Unknown Product') AS name
FROM receivings_products rp
JOIN receivings r ON rp.receiving_id = r.receiving_id
+ LEFT JOIN products p ON rp.pid = p.pid
WHERE rp.receiving_id IN (?)
`, [receivingIds]);
@@ -443,7 +449,9 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
received_date: validateDate(product.received_date) || validateDate(product.receiving_created_date),
receiving_created_date: validateDate(product.receiving_created_date),
supplier_id: receiving.supplier_id,
- status: receivingStatusMap[receiving.status] || 'created'
+ status: receivingStatusMap[receiving.status] || 'created',
+ sku: product.sku,
+ name: product.name
});
}
@@ -452,8 +460,8 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
const batch = completeReceivings.slice(i, i + INSERT_BATCH_SIZE);
const placeholders = batch.map((_, idx) => {
- const base = idx * 10;
- return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10})`;
+ const base = idx * 12;
+ return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12})`;
}).join(',');
const values = batch.flatMap(r => [
@@ -466,13 +474,16 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
r.received_date,
r.receiving_created_date,
r.supplier_id,
- r.status
+ r.status,
+ r.sku,
+ r.name
]);
await localConnection.query(`
INSERT INTO temp_receivings (
receiving_id, po_id, pid, qty_each, cost_each, received_by,
- received_date, receiving_created_date, supplier_id, status
+ received_date, receiving_created_date, supplier_id, status,
+ sku, name
)
VALUES ${placeholders}
ON CONFLICT (receiving_id, pid) DO UPDATE SET
@@ -483,7 +494,9 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
received_date = EXCLUDED.received_date,
receiving_created_date = EXCLUDED.receiving_created_date,
supplier_id = EXCLUDED.supplier_id,
- status = EXCLUDED.status
+ status = EXCLUDED.status,
+ sku = EXCLUDED.sku,
+ name = EXCLUDED.name
`, values);
}
@@ -506,6 +519,55 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
}
}
+ // Add this section before the FIFO steps to create a supplier names mapping
+ outputProgress({
+ status: "running",
+ operation: "Purchase orders import",
+ message: "Fetching supplier data for vendor mapping"
+ });
+
+ // Fetch supplier data from production and store in a temp table
+ const [suppliers] = await prodConnection.query(`
+ SELECT
+ supplierid,
+ companyname
+ FROM suppliers
+ WHERE companyname IS NOT NULL AND companyname != ''
+ `);
+
+ if (suppliers.length > 0) {
+ // Create temp table for supplier names
+ await localConnection.query(`
+ DROP TABLE IF EXISTS temp_supplier_names;
+ CREATE TEMP TABLE temp_supplier_names (
+ supplier_id INTEGER PRIMARY KEY,
+ company_name TEXT NOT NULL
+ );
+ `);
+
+ // Insert supplier data in batches
+ for (let i = 0; i < suppliers.length; i += INSERT_BATCH_SIZE) {
+ const batch = suppliers.slice(i, i + INSERT_BATCH_SIZE);
+
+ const placeholders = batch.map((_, idx) => {
+ const base = idx * 2;
+ return `($${base + 1}, $${base + 2})`;
+ }).join(',');
+
+ const values = batch.flatMap(s => [
+ s.supplierid,
+ s.companyname || 'Unnamed Supplier'
+ ]);
+
+ await localConnection.query(`
+ INSERT INTO temp_supplier_names (supplier_id, company_name)
+ VALUES ${placeholders}
+ ON CONFLICT (supplier_id) DO UPDATE SET
+ company_name = EXCLUDED.company_name
+ `, values);
+ }
+ }
+
// 3. Implement FIFO allocation of receivings to purchase orders
outputProgress({
status: "running",
@@ -583,12 +645,20 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
SELECT
r.receiving_id::text as po_id,
r.pid,
- COALESCE(p.sku, 'NO-SKU') as sku,
- COALESCE(p.name, 'Unknown Product') as name,
+ r.sku,
+ r.name,
COALESCE(
+ -- First, check if we already have a vendor name from the temp_purchase_orders table
(SELECT vendor FROM temp_purchase_orders
WHERE supplier_id = r.supplier_id LIMIT 1),
- 'Unknown Vendor'
+ -- Next, check the supplier_names mapping table we created
+ (SELECT company_name FROM temp_supplier_names
+ WHERE supplier_id = r.supplier_id),
+ -- If both fail, use a generic name with the supplier ID
+ CASE
+ WHEN r.supplier_id IS NOT NULL THEN 'Supplier #' || r.supplier_id::text
+ ELSE 'Unknown Supplier'
+ END
) as vendor,
COALESCE(r.received_date, r.receiving_created_date) as date,
'created' as status,
@@ -598,9 +668,6 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
COALESCE(r.receiving_created_date, r.received_date) as date_created,
NULL as date_ordered
FROM temp_receivings r
- LEFT JOIN (
- SELECT DISTINCT pid, sku, name FROM temp_purchase_orders
- ) p ON r.pid = p.pid
WHERE r.po_id IS NULL
OR NOT EXISTS (
SELECT 1 FROM temp_purchase_orders po
@@ -923,6 +990,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
DROP TABLE IF EXISTS temp_receivings;
DROP TABLE IF EXISTS temp_receiving_allocations;
DROP TABLE IF EXISTS employee_names;
+ DROP TABLE IF EXISTS temp_supplier_names;
`);
// Commit transaction
diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js
index 1c1cec5..de6317a 100644
--- a/inventory-server/src/routes/dashboard.js
+++ b/inventory-server/src/routes/dashboard.js
@@ -111,25 +111,35 @@ router.get('/purchase/metrics', async (req, res) => {
SELECT
COALESCE(COUNT(DISTINCT CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
THEN po.po_id
END), 0)::integer as active_pos,
COALESCE(COUNT(DISTINCT CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
AND po.expected_date < CURRENT_DATE
THEN po.po_id
END), 0)::integer as overdue_pos,
COALESCE(SUM(CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
THEN po.ordered
ELSE 0
END), 0)::integer as total_units,
ROUND(COALESCE(SUM(CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
THEN po.ordered * po.cost_price
ELSE 0
END), 0)::numeric, 3) as total_cost,
ROUND(COALESCE(SUM(CASE
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
THEN po.ordered * pm.current_price
ELSE 0
END), 0)::numeric, 3) as total_retail
@@ -147,6 +157,8 @@ router.get('/purchase/metrics', async (req, res) => {
FROM purchase_orders po
JOIN product_metrics pm ON po.pid = pm.pid
WHERE po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
+ AND po.date >= CURRENT_DATE - INTERVAL '6 months'
+ AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
GROUP BY po.vendor
HAVING ROUND(COALESCE(SUM(po.ordered * po.cost_price), 0)::numeric, 3) > 0
ORDER BY cost DESC
diff --git a/inventory-server/src/routes/purchase-orders.js b/inventory-server/src/routes/purchase-orders.js
index 740efad..50cdb0c 100644
--- a/inventory-server/src/routes/purchase-orders.js
+++ b/inventory-server/src/routes/purchase-orders.js
@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
// Status code constants
+// Frontend uses these numeric codes but database uses strings
const STATUS = {
CANCELED: 0,
CREATED: 1,
@@ -13,6 +14,18 @@ const STATUS = {
DONE: 50
};
+// Status mapping from database string values to frontend numeric codes
+const STATUS_MAPPING = {
+ 'canceled': STATUS.CANCELED,
+ 'created': STATUS.CREATED,
+ 'electronically_ready_send': STATUS.ELECTRONICALLY_READY_SEND,
+ 'ordered': STATUS.ORDERED,
+ 'preordered': STATUS.PREORDERED,
+ 'electronically_sent': STATUS.ELECTRONICALLY_SENT,
+ 'receiving_started': STATUS.RECEIVING_STARTED,
+ 'done': STATUS.DONE
+};
+
const RECEIVING_STATUS = {
CANCELED: 0,
CREATED: 1,
@@ -21,6 +34,26 @@ const RECEIVING_STATUS = {
PAID: 50
};
+// Receiving status mapping from database string values to frontend numeric codes
+const RECEIVING_STATUS_MAPPING = {
+ 'canceled': RECEIVING_STATUS.CANCELED,
+ 'created': RECEIVING_STATUS.CREATED,
+ 'partial_received': RECEIVING_STATUS.PARTIAL_RECEIVED,
+ 'full_received': RECEIVING_STATUS.FULL_RECEIVED,
+ 'paid': RECEIVING_STATUS.PAID
+};
+
+// Helper for SQL status value comparison with string values in DB
+function getStatusWhereClause(statusNum) {
+ const dbStatuses = Object.keys(STATUS_MAPPING).filter(key =>
+ STATUS_MAPPING[key] === parseInt(statusNum));
+
+ if (dbStatuses.length > 0) {
+ return `po.status = '${dbStatuses[0]}'`;
+ }
+ return `1=0`; // No match found, return false condition
+}
+
// Get all purchase orders with summary metrics
router.get('/', async (req, res) => {
try {
@@ -38,9 +71,7 @@ router.get('/', async (req, res) => {
}
if (status && status !== 'all') {
- whereClause += ` AND po.status = $${paramCounter}`;
- params.push(Number(status));
- paramCounter++;
+ whereClause += ` AND ${getStatusWhereClause(status)}`;
}
if (vendor && vendor !== 'all') {
@@ -139,17 +170,17 @@ router.get('/', async (req, res) => {
GROUP BY po_id, vendor, date, status, receiving_status
)
SELECT
- po_id as id,
- vendor as vendor_name,
- to_char(date, 'YYYY-MM-DD') as order_date,
- status,
- receiving_status,
- total_items,
- total_quantity,
- total_cost,
- total_received,
- fulfillment_rate
- FROM po_totals
+ pt.po_id as id,
+ pt.vendor as vendor_name,
+ to_char(pt.date, 'YYYY-MM-DD') as order_date,
+ pt.status,
+ pt.receiving_status,
+ pt.total_items,
+ pt.total_quantity,
+ pt.total_cost,
+ pt.total_received,
+ pt.fulfillment_rate
+ FROM po_totals pt
ORDER BY ${orderByClause}
LIMIT $${paramCounter} OFFSET $${paramCounter + 1}
`, [...params, Number(limit), offset]);
@@ -170,13 +201,36 @@ router.get('/', async (req, res) => {
ORDER BY status
`);
- // Parse numeric values
+ // Get product vendors for orders with Unknown Vendor
+ const poIds = orders.filter(o => o.vendor_name === 'Unknown Vendor').map(o => o.id);
+ let vendorMappings = {};
+
+ if (poIds.length > 0) {
+ const { rows: productVendors } = await pool.query(`
+ SELECT DISTINCT po.po_id, p.vendor
+ FROM purchase_orders po
+ JOIN products p ON po.pid = p.pid
+ WHERE po.po_id = ANY($1)
+ AND p.vendor IS NOT NULL AND p.vendor != ''
+ GROUP BY po.po_id, p.vendor
+ `, [poIds]);
+
+ // Create mapping of PO ID to actual vendor from products table
+ vendorMappings = productVendors.reduce((acc, pv) => {
+ if (!acc[pv.po_id]) {
+ acc[pv.po_id] = pv.vendor;
+ }
+ return acc;
+ }, {});
+ }
+
+ // Parse numeric values and map status strings to numeric codes
const parsedOrders = orders.map(order => ({
id: order.id,
- vendor_name: order.vendor_name,
+ vendor_name: vendorMappings[order.id] || order.vendor_name,
order_date: order.order_date,
- status: Number(order.status),
- receiving_status: Number(order.receiving_status),
+ status: STATUS_MAPPING[order.status] || 0, // Map string status to numeric code
+ receiving_status: RECEIVING_STATUS_MAPPING[order.receiving_status] || 0, // Map string receiving status to numeric code
total_items: Number(order.total_items) || 0,
total_quantity: Number(order.total_quantity) || 0,
total_cost: Number(order.total_cost) || 0,
@@ -205,7 +259,7 @@ router.get('/', async (req, res) => {
},
filters: {
vendors: vendors.map(v => v.vendor),
- statuses: statuses.map(s => Number(s.status))
+ statuses: statuses.map(s => STATUS_MAPPING[s.status] || 0) // Map string statuses to numeric codes for the frontend
}
});
} catch (error) {
@@ -228,14 +282,15 @@ router.get('/vendor-metrics', async (req, res) => {
received,
cost_price,
CASE
- WHEN status >= ${STATUS.RECEIVING_STARTED} AND receiving_status >= ${RECEIVING_STATUS.PARTIAL_RECEIVED}
+ WHEN status IN ('receiving_started', 'done')
+ AND receiving_status IN ('partial_received', 'full_received', 'paid')
AND received_date IS NOT NULL AND date IS NOT NULL
THEN (received_date - date)::integer
ELSE NULL
END as delivery_days
FROM purchase_orders
WHERE vendor IS NOT NULL AND vendor != ''
- AND status != ${STATUS.CANCELED} -- Exclude canceled orders
+ AND status != 'canceled' -- Exclude canceled orders
)
SELECT
vendor as vendor_name,
@@ -296,7 +351,7 @@ router.get('/cost-analysis', async (req, res) => {
FROM purchase_orders po
JOIN product_categories pc ON po.pid = pc.pid
JOIN categories c ON pc.cat_id = c.cat_id
- WHERE po.status != ${STATUS.CANCELED} -- Exclude canceled orders
+ WHERE po.status != 'canceled' -- Exclude canceled orders
)
SELECT
category,
@@ -311,7 +366,7 @@ router.get('/cost-analysis', async (req, res) => {
ORDER BY total_spend DESC
`);
- // Parse numeric values
+ // Parse numeric values and include ALL data for each category
const parsedAnalysis = {
unique_products: 0,
avg_cost: 0,
@@ -320,6 +375,11 @@ router.get('/cost-analysis', async (req, res) => {
cost_variance: 0,
total_spend_by_category: analysis.map(cat => ({
category: cat.category,
+ unique_products: Number(cat.unique_products) || 0,
+ avg_cost: Number(cat.avg_cost) || 0,
+ min_cost: Number(cat.min_cost) || 0,
+ max_cost: Number(cat.max_cost) || 0,
+ cost_variance: Number(cat.cost_variance) || 0,
total_spend: Number(cat.total_spend) || 0
}))
};
@@ -366,7 +426,7 @@ router.get('/receiving-status', async (req, res) => {
SUM(received) as total_received,
ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost
FROM purchase_orders
- WHERE status != ${STATUS.CANCELED}
+ WHERE status != 'canceled'
GROUP BY po_id, status, receiving_status
)
SELECT
@@ -379,16 +439,16 @@ router.get('/receiving-status', async (req, res) => {
ROUND(SUM(total_cost)::numeric, 3) as total_value,
ROUND(AVG(total_cost)::numeric, 3) as avg_cost,
COUNT(DISTINCT CASE
- WHEN receiving_status = ${RECEIVING_STATUS.CREATED} THEN po_id
+ WHEN receiving_status = 'created' THEN po_id
END) as pending_count,
COUNT(DISTINCT CASE
- WHEN receiving_status = ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po_id
+ WHEN receiving_status = 'partial_received' THEN po_id
END) as partial_count,
COUNT(DISTINCT CASE
- WHEN receiving_status >= ${RECEIVING_STATUS.FULL_RECEIVED} THEN po_id
+ WHEN receiving_status IN ('full_received', 'paid') THEN po_id
END) as completed_count,
COUNT(DISTINCT CASE
- WHEN receiving_status = ${RECEIVING_STATUS.CANCELED} THEN po_id
+ WHEN receiving_status = 'canceled' THEN po_id
END) as canceled_count
FROM po_totals
`);
@@ -423,7 +483,7 @@ router.get('/order-vs-received', async (req, res) => {
const { rows: quantities } = await pool.query(`
SELECT
- p.product_id,
+ p.pid as product_id,
p.title as product,
p.SKU as sku,
SUM(po.ordered) as ordered_quantity,
@@ -433,9 +493,9 @@ router.get('/order-vs-received', async (req, res) => {
) as fulfillment_rate,
COUNT(DISTINCT po.po_id) as order_count
FROM products p
- JOIN purchase_orders po ON p.product_id = po.product_id
+ JOIN purchase_orders po ON p.pid = po.pid
WHERE po.date >= (CURRENT_DATE - INTERVAL '90 days')
- GROUP BY p.product_id, p.title, p.SKU
+ GROUP BY p.pid, p.title, p.SKU
HAVING COUNT(DISTINCT po.po_id) > 0
ORDER BY SUM(po.ordered) DESC
LIMIT 20
@@ -445,10 +505,10 @@ router.get('/order-vs-received', async (req, res) => {
const parsedQuantities = quantities.map(q => ({
id: q.product_id,
...q,
- ordered_quantity: Number(q.ordered_quantity),
- received_quantity: Number(q.received_quantity),
- fulfillment_rate: Number(q.fulfillment_rate),
- order_count: Number(q.order_count)
+ ordered_quantity: Number(q.ordered_quantity) || 0,
+ received_quantity: Number(q.received_quantity) || 0,
+ fulfillment_rate: Number(q.fulfillment_rate) || 0,
+ order_count: Number(q.order_count) || 0
}));
res.json(parsedQuantities);
diff --git a/inventory/src/pages/PurchaseOrders.tsx b/inventory/src/pages/PurchaseOrders.tsx
index 35b077d..bbdf5be 100644
--- a/inventory/src/pages/PurchaseOrders.tsx
+++ b/inventory/src/pages/PurchaseOrders.tsx
@@ -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(
+