diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 33b619f..65a215b 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -2,6 +2,9 @@ const express = require('express'); const router = express.Router(); const db = require('../utils/db'); +// Import status codes +const { RECEIVING_STATUS } = require('../types/status-codes'); + // Helper function to execute queries using the connection pool async function executeQuery(sql, params = []) { const pool = db.getPool(); @@ -100,19 +103,27 @@ router.get('/purchase/metrics', async (req, res) => { try { const [rows] = await executeQuery(` SELECT - COALESCE(COUNT(DISTINCT CASE WHEN po.receiving_status < 30 THEN po.po_id END), 0) as active_pos, COALESCE(COUNT(DISTINCT CASE - WHEN po.receiving_status < 30 AND po.expected_date < CURDATE() + WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} + THEN po.po_id + END), 0) as active_pos, + COALESCE(COUNT(DISTINCT CASE + WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} + AND po.expected_date < CURDATE() THEN po.po_id END), 0) as overdue_pos, - COALESCE(SUM(CASE WHEN po.receiving_status < 30 THEN po.ordered ELSE 0 END), 0) as total_units, + COALESCE(SUM(CASE + WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} + THEN po.ordered + ELSE 0 + END), 0) as total_units, CAST(COALESCE(SUM(CASE - WHEN po.receiving_status < 30 + WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po.ordered * po.cost_price ELSE 0 END), 0) AS DECIMAL(15,3)) as total_cost, CAST(COALESCE(SUM(CASE - WHEN po.receiving_status < 30 + WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po.ordered * p.price ELSE 0 END), 0) AS DECIMAL(15,3)) as total_retail @@ -137,7 +148,7 @@ router.get('/purchase/metrics', async (req, res) => { CAST(COALESCE(SUM(po.ordered * p.price), 0) AS DECIMAL(15,3)) as order_retail FROM purchase_orders po JOIN products p ON po.pid = p.pid - WHERE po.receiving_status < 30 + WHERE po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED} GROUP BY po.vendor HAVING order_cost > 0 ORDER BY order_cost DESC diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index d8cc84f..f439bf8 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const multer = require('multer'); const { importProductsFromCSV } = require('../utils/csvImporter'); +const { PurchaseOrderStatus, ReceivingStatus } = require('../types/status-codes'); // Configure multer for file uploads const upload = multer({ dest: 'uploads/' }); @@ -642,70 +643,46 @@ router.get('/:id/metrics', async (req, res) => { // Get product time series data router.get('/:id/time-series', async (req, res) => { - const pool = req.app.locals.pool; + const { id } = req.params; try { - const { id } = req.params; - const months = parseInt(req.query.months) || 12; - - // Get monthly sales data with running totals and growth rates - const [monthlySales] = await pool.query(` - WITH monthly_data AS ( - SELECT - CONCAT(year, '-', LPAD(month, 2, '0')) as month, - total_quantity_sold as quantity, - total_revenue as revenue, - total_cost as cost, - avg_price, - profit_margin, - inventory_value - FROM product_time_aggregates - WHERE pid = ? - ORDER BY year DESC, month DESC - LIMIT ? - ) - SELECT - month, - quantity, - revenue, - cost, - avg_price, - profit_margin, - inventory_value, - LAG(quantity) OVER (ORDER BY month) as prev_month_quantity, - LAG(revenue) OVER (ORDER BY month) as prev_month_revenue - FROM monthly_data - ORDER BY month ASC - `, [id, months]); + const pool = req.app.locals.pool; - // Calculate growth rates and format data - const formattedMonthlySales = monthlySales.map(row => ({ - month: row.month, - quantity: parseInt(row.quantity) || 0, - revenue: parseFloat(row.revenue) || 0, - cost: parseFloat(row.cost) || 0, - avg_price: parseFloat(row.avg_price) || 0, - profit_margin: parseFloat(row.profit_margin) || 0, - inventory_value: parseFloat(row.inventory_value) || 0, - quantity_growth: row.prev_month_quantity ? - ((row.quantity - row.prev_month_quantity) / row.prev_month_quantity) * 100 : 0, - revenue_growth: row.prev_month_revenue ? - ((row.revenue - row.prev_month_revenue) / row.prev_month_revenue) * 100 : 0 + // Get monthly sales data + const [monthlySales] = await pool.query(` + SELECT + DATE_FORMAT(date, '%Y-%m') as month, + COUNT(DISTINCT order_id) as order_count, + SUM(quantity) as units_sold, + CAST(SUM(price * quantity) AS DECIMAL(15,3)) as revenue, + CAST(SUM((price - cost_price) * quantity) AS DECIMAL(15,3)) as profit + FROM order_items + WHERE pid = ? + AND canceled = false + GROUP BY DATE_FORMAT(date, '%Y-%m') + ORDER BY month DESC + LIMIT 12 + `, [id]); + + // Format monthly sales data + const formattedMonthlySales = monthlySales.map(month => ({ + month: month.month, + order_count: parseInt(month.order_count), + units_sold: parseInt(month.units_sold), + revenue: parseFloat(month.revenue), + profit: parseFloat(month.profit) })); - // Get recent orders with customer info and status + // Get recent orders const [recentOrders] = await pool.query(` SELECT DATE_FORMAT(date, '%Y-%m-%d') as date, - order_number, + order_id, quantity, price, discount, tax, - shipping, - customer, - status, - payment_method - FROM orders + shipping + FROM order_items WHERE pid = ? AND canceled = false ORDER BY date DESC @@ -722,17 +699,19 @@ router.get('/:id/time-series', async (req, res) => { ordered, received, status, + receiving_status, cost_price, notes, CASE WHEN received_date IS NOT NULL THEN DATEDIFF(received_date, date) - WHEN expected_date < CURDATE() AND status != 'received' THEN + WHEN expected_date < CURDATE() AND status < ${PurchaseOrderStatus.ReceivingStarted} THEN DATEDIFF(CURDATE(), expected_date) ELSE NULL END as lead_time_days FROM purchase_orders WHERE pid = ? + AND status != ${PurchaseOrderStatus.CANCELED} ORDER BY date DESC LIMIT 10 `, [id]); @@ -751,6 +730,8 @@ router.get('/:id/time-series', async (req, res) => { ...po, ordered: parseInt(po.ordered), received: parseInt(po.received), + status: parseInt(po.status), + receiving_status: parseInt(po.receiving_status), cost_price: parseFloat(po.cost_price), lead_time_days: po.lead_time_days ? parseInt(po.lead_time_days) : null })) diff --git a/inventory-server/src/routes/purchase-orders.js b/inventory-server/src/routes/purchase-orders.js index 2fac225..78d1bc8 100644 --- a/inventory-server/src/routes/purchase-orders.js +++ b/inventory-server/src/routes/purchase-orders.js @@ -1,6 +1,26 @@ const express = require('express'); const router = express.Router(); +// Status code constants +const STATUS = { + CANCELED: 0, + CREATED: 1, + ELECTRONICALLY_READY_SEND: 10, + ORDERED: 11, + PREORDERED: 12, + ELECTRONICALLY_SENT: 13, + RECEIVING_STARTED: 15, + DONE: 50 +}; + +const RECEIVING_STATUS = { + CANCELED: 0, + CREATED: 1, + PARTIAL_RECEIVED: 30, + FULL_RECEIVED: 40, + PAID: 50 +}; + // Get all purchase orders with summary metrics router.get('/', async (req, res) => { try { @@ -11,13 +31,13 @@ router.get('/', async (req, res) => { const params = []; if (search) { - whereClause += ' AND (po.po_id LIKE ? OR po.vendor LIKE ? OR po.status LIKE ?)'; - params.push(`%${search}%`, `%${search}%`, `%${search}%`); + whereClause += ' AND (po.po_id LIKE ? OR po.vendor LIKE ?)'; + params.push(`%${search}%`, `%${search}%`); } if (status && status !== 'all') { whereClause += ' AND po.status = ?'; - params.push(status); + params.push(Number(status)); } if (vendor && vendor !== 'all') { @@ -78,6 +98,7 @@ router.get('/', async (req, res) => { vendor, date, status, + receiving_status, COUNT(DISTINCT pid) as total_items, SUM(ordered) as total_quantity, CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost, @@ -87,13 +108,14 @@ router.get('/', async (req, res) => { ) as fulfillment_rate FROM purchase_orders po WHERE ${whereClause} - GROUP BY po_id, vendor, date, status + GROUP BY po_id, vendor, date, status, receiving_status ) SELECT po_id as id, vendor as vendor_name, DATE_FORMAT(date, '%Y-%m-%d') as order_date, status, + receiving_status, total_items, total_quantity, total_cost, @@ -127,7 +149,7 @@ router.get('/', async (req, res) => { const [statuses] = await pool.query(` SELECT DISTINCT status FROM purchase_orders - WHERE status IS NOT NULL AND status != '' + WHERE status IS NOT NULL ORDER BY status `); @@ -136,7 +158,8 @@ router.get('/', async (req, res) => { id: order.id, vendor_name: order.vendor_name, order_date: order.order_date, - status: order.status, + 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, @@ -165,7 +188,7 @@ router.get('/', async (req, res) => { }, filters: { vendors: vendors.map(v => v.vendor), - statuses: statuses.map(s => s.status) + statuses: statuses.map(s => Number(s.status)) } }); } catch (error) { @@ -188,12 +211,14 @@ router.get('/vendor-metrics', async (req, res) => { received, cost_price, CASE - WHEN status = 'received' AND received_date IS NOT NULL AND date IS NOT NULL + WHEN status >= ${STATUS.RECEIVING_STARTED} AND receiving_status >= ${RECEIVING_STATUS.PARTIAL_RECEIVED} + AND received_date IS NOT NULL AND date IS NOT NULL THEN DATEDIFF(received_date, date) ELSE NULL END as delivery_days FROM purchase_orders WHERE vendor IS NOT NULL AND vendor != '' + AND status != ${STATUS.CANCELED} -- Exclude canceled orders ) SELECT vendor as vendor_name, @@ -242,44 +267,47 @@ router.get('/cost-analysis', async (req, res) => { const pool = req.app.locals.pool; const [analysis] = await pool.query(` + WITH category_costs AS ( + SELECT + c.name as category, + po.pid, + po.cost_price, + po.ordered, + po.received, + po.status, + po.receiving_status + 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 + ) SELECT - c.name as categories, - COUNT(DISTINCT po.pid) as unique_products, - CAST(AVG(po.cost_price) AS DECIMAL(15,3)) as avg_cost, - CAST(MIN(po.cost_price) AS DECIMAL(15,3)) as min_cost, - CAST(MAX(po.cost_price) AS DECIMAL(15,3)) as max_cost, - CAST(STDDEV(po.cost_price) AS DECIMAL(15,3)) as cost_std_dev, - CAST(SUM(po.ordered * po.cost_price) AS DECIMAL(15,3)) as total_spend - FROM purchase_orders po - JOIN product_categories pc ON po.pid = pc.pid - JOIN categories c ON pc.cat_id = c.cat_id - GROUP BY c.name + category, + COUNT(DISTINCT pid) as unique_products, + CAST(AVG(cost_price) AS DECIMAL(15,3)) as avg_cost, + CAST(MIN(cost_price) AS DECIMAL(15,3)) as min_cost, + CAST(MAX(cost_price) AS DECIMAL(15,3)) as max_cost, + CAST(STDDEV(cost_price) AS DECIMAL(15,3)) as cost_variance, + CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend + FROM category_costs + GROUP BY category ORDER BY total_spend DESC `); - // Parse numeric values and add ids for React keys - const parsedAnalysis = analysis.map(item => ({ - id: item.categories || 'Uncategorized', - categories: item.categories || 'Uncategorized', - unique_products: Number(item.unique_products) || 0, - avg_cost: Number(item.avg_cost) || 0, - min_cost: Number(item.min_cost) || 0, - max_cost: Number(item.max_cost) || 0, - cost_variance: Number(item.cost_variance) || 0, - total_spend: Number(item.total_spend) || 0 - })); - - // Transform the data with parsed values - const transformedAnalysis = { - ...parsedAnalysis[0], - total_spend_by_category: parsedAnalysis.map(item => ({ - id: item.categories, - category: item.categories, - total_spend: Number(item.total_spend) + // Parse numeric values + const parsedAnalysis = { + categories: 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 })) }; - res.json(transformedAnalysis); + res.json(parsedAnalysis); } catch (error) { console.error('Error fetching cost analysis:', error); res.status(500).json({ error: 'Failed to fetch cost analysis' }); @@ -295,11 +323,14 @@ router.get('/receiving-status', async (req, res) => { WITH po_totals AS ( SELECT po_id, + status, + receiving_status, SUM(ordered) as total_ordered, SUM(received) as total_received, - SUM(ordered * cost_price) as total_cost + CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost FROM purchase_orders - GROUP BY po_id + WHERE status != ${STATUS.CANCELED} + GROUP BY po_id, status, receiving_status ) SELECT COUNT(DISTINCT po_id) as order_count, @@ -308,8 +339,20 @@ router.get('/receiving-status', async (req, res) => { ROUND( SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3 ) as fulfillment_rate, - SUM(total_cost) as total_value, - ROUND(AVG(total_cost), 2) as avg_cost + CAST(SUM(total_cost) AS DECIMAL(15,3)) as total_value, + CAST(AVG(total_cost) AS DECIMAL(15,3)) as avg_cost, + COUNT(DISTINCT CASE + WHEN receiving_status = ${RECEIVING_STATUS.CREATED} THEN po_id + END) as pending_count, + COUNT(DISTINCT CASE + WHEN receiving_status = ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po_id + END) as partial_count, + COUNT(DISTINCT CASE + WHEN receiving_status >= ${RECEIVING_STATUS.FULL_RECEIVED} THEN po_id + END) as completed_count, + COUNT(DISTINCT CASE + WHEN receiving_status = ${RECEIVING_STATUS.CANCELED} THEN po_id + END) as canceled_count FROM po_totals `); @@ -320,7 +363,13 @@ router.get('/receiving-status', async (req, res) => { total_received: Number(status[0].total_received) || 0, fulfillment_rate: Number(status[0].fulfillment_rate) || 0, total_value: Number(status[0].total_value) || 0, - avg_cost: Number(status[0].avg_cost) || 0 + avg_cost: Number(status[0].avg_cost) || 0, + status_breakdown: { + pending: Number(status[0].pending_count) || 0, + partial: Number(status[0].partial_count) || 0, + completed: Number(status[0].completed_count) || 0, + canceled: Number(status[0].canceled_count) || 0 + } }; res.json(parsedStatus); diff --git a/inventory/src/components/dashboard/PurchaseMetrics.tsx b/inventory/src/components/dashboard/PurchaseMetrics.tsx index 71ebe4d..b4852c4 100644 --- a/inventory/src/components/dashboard/PurchaseMetrics.tsx +++ b/inventory/src/components/dashboard/PurchaseMetrics.tsx @@ -5,13 +5,14 @@ import config from "@/config" import { formatCurrency } from "@/lib/utils" import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons import { useState } from "react" +import { PurchaseOrderStatus, ReceivingStatus } from "@/types/status-codes" interface PurchaseMetricsData { - activePurchaseOrders: number - overduePurchaseOrders: number - onOrderUnits: number - onOrderCost: number - onOrderRetail: number + activePurchaseOrders: number // Orders that are not canceled, done, or fully received + overduePurchaseOrders: number // Orders past their expected delivery date + onOrderUnits: number // Total units across all active orders + onOrderCost: number // Total cost across all active orders + onOrderRetail: number // Total retail value across all active orders vendorOrders: { vendor: string orders: number diff --git a/inventory/src/components/dashboard/TrendingProducts.tsx b/inventory/src/components/dashboard/TrendingProducts.tsx index 5ba568a..d9f2a74 100644 --- a/inventory/src/components/dashboard/TrendingProducts.tsx +++ b/inventory/src/components/dashboard/TrendingProducts.tsx @@ -15,10 +15,10 @@ interface Product { pid: number; sku: string; title: string; - daily_sales_avg: number; - weekly_sales_avg: number; - growth_rate: number; - total_revenue: number; + daily_sales_avg: string; + weekly_sales_avg: string; + growth_rate: string; + total_revenue: string; } export function TrendingProducts() { @@ -75,20 +75,20 @@ export function TrendingProducts() { - {parseFloat(product.daily_sales_avg).toFixed(1)} + {Number(product.daily_sales_avg).toFixed(1)}
- {parseFloat(product.growth_rate) > 0 ? ( + {Number(product.growth_rate) > 0 ? ( ) : ( )} 0 ? "text-success" : "text-destructive" + Number(product.growth_rate) > 0 ? "text-success" : "text-destructive" } > - {formatPercent(parseFloat(product.growth_rate))} + {formatPercent(Number(product.growth_rate))}
diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx index 43fd4ea..cae0646 100644 --- a/inventory/src/components/products/ProductFilters.tsx +++ b/inventory/src/components/products/ProductFilters.tsx @@ -24,7 +24,7 @@ type FilterValue = string | number | boolean; type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between"; interface FilterValueWithOperator { - value: FilterValue | [number, number]; + value: FilterValue | [string, string]; operator: ComparisonOperator; } @@ -317,18 +317,32 @@ export function ProductFilters({ }); }, []); - const handleApplyFilter = (value: FilterValue | [number, number]) => { + const handleApplyFilter = (value: FilterValue | [string, string]) => { if (!selectedFilter) return; - const newFilters = { - ...activeFilters, - [selectedFilter.id]: { - value, - operator: selectedOperator, - }, - }; + let filterValue: ActiveFilterValue; + + if (selectedFilter.type === "number") { + if (selectedOperator === "between" && Array.isArray(value)) { + filterValue = { + value: [value[0].toString(), value[1].toString()], + operator: selectedOperator, + }; + } else { + filterValue = { + value: value.toString(), + operator: selectedOperator, + }; + } + } else { + filterValue = value; + } + + onFilterChange({ + ...activeFilters, + [selectedFilter.id]: filterValue, + }); - onFilterChange(newFilters as Record); handlePopoverClose(); }; @@ -394,38 +408,14 @@ export function ProductFilters({ const getFilterDisplayValue = (filter: ActiveFilter) => { - const filterValue = activeFilters[filter.id]; - const filterOption = filterOptions.find((opt) => opt.id === filter.id); - - // For between ranges - if (Array.isArray(filterValue)) { - return `${filter.label} between ${filterValue[0]} and ${filterValue[1]}`; + if (typeof filter.value === "object" && "operator" in filter.value) { + const { operator, value } = filter.value; + if (Array.isArray(value)) { + return `${operator} ${value[0]} and ${value[1]}`; + } + return `${operator} ${value}`; } - - // For direct selections (select type) or text search - if ( - filterOption?.type === "select" || - filterOption?.type === "text" || - typeof filterValue !== "object" - ) { - const value = - typeof filterValue === "object" ? filterValue.value : filterValue; - return `${filter.label}: ${value}`; - } - - // For numeric filters with operators - const operator = filterValue.operator; - const value = filterValue.value; - const operatorDisplay = { - "=": "=", - ">": ">", - ">=": "≥", - "<": "<", - "<=": "≤", - between: "between", - }[operator]; - - return `${filter.label} ${operatorDisplay} ${value}`; + return filter.value.toString(); }; return ( diff --git a/inventory/src/components/products/ProductTable.tsx b/inventory/src/components/products/ProductTable.tsx index a7b9e65..dcea38e 100644 --- a/inventory/src/components/products/ProductTable.tsx +++ b/inventory/src/components/products/ProductTable.tsx @@ -261,6 +261,11 @@ export function ProductTable({ return columnDef.format(num); } } + // If the value is already a number, format it directly + if (typeof value === 'number') { + return columnDef.format(value); + } + // For other formats (e.g., date formatting), pass the value as is return columnDef.format(value); } return value ?? '-'; diff --git a/inventory/src/pages/PurchaseOrders.tsx b/inventory/src/pages/PurchaseOrders.tsx index 88400a8..4dec90a 100644 --- a/inventory/src/pages/PurchaseOrders.tsx +++ b/inventory/src/pages/PurchaseOrders.tsx @@ -20,12 +20,21 @@ import { PaginationPrevious, } from '../components/ui/pagination'; import { motion } from 'motion/react'; +import { + PurchaseOrderStatus, + ReceivingStatus as ReceivingStatusCode, + getPurchaseOrderStatusLabel, + getReceivingStatusLabel, + getPurchaseOrderStatusVariant, + getReceivingStatusVariant +} from '../types/status-codes'; interface PurchaseOrder { id: number; vendor_name: string; order_date: string; - status: string; + status: number; + receiving_status: number; total_items: number; total_quantity: number; total_cost: number; @@ -113,6 +122,16 @@ export default function PurchaseOrders() { limit: 100, }); + const STATUS_FILTER_OPTIONS = [ + { value: 'all', label: 'All Statuses' }, + { value: String(PurchaseOrderStatus.Created), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Created) }, + { value: String(PurchaseOrderStatus.ElectronicallyReadySend), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.ElectronicallyReadySend) }, + { value: String(PurchaseOrderStatus.Ordered), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Ordered) }, + { value: String(PurchaseOrderStatus.ReceivingStarted), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.ReceivingStarted) }, + { value: String(PurchaseOrderStatus.Done), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Done) }, + { value: String(PurchaseOrderStatus.Canceled), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Canceled) }, + ]; + const fetchData = async () => { try { const searchParams = new URLSearchParams({ @@ -171,16 +190,25 @@ export default function PurchaseOrders() { } }; - const getStatusBadge = (status: string) => { - const variants: Record = { - pending: { variant: "outline", label: "Pending" }, - received: { variant: "default", label: "Received" }, - partial: { variant: "secondary", label: "Partial" }, - cancelled: { variant: "destructive", label: "Cancelled" }, - }; - - const statusConfig = variants[status.toLowerCase()] || variants.pending; - return {statusConfig.label}; + const getStatusBadge = (status: number, receivingStatus: number) => { + // If the PO is canceled, show that status + if (status === PurchaseOrderStatus.Canceled) { + return + {getPurchaseOrderStatusLabel(status)} + ; + } + + // If receiving has started, show receiving status + if (status >= PurchaseOrderStatus.ReceivingStarted) { + return + {getReceivingStatusLabel(receivingStatus)} + ; + } + + // Otherwise show PO status + return + {getPurchaseOrderStatusLabel(status)} + ; }; const formatNumber = (value: number) => { @@ -252,45 +280,44 @@ export default function PurchaseOrders() { {/* Filters */} -
-
- setFilters(prev => ({ ...prev, search: e.target.value }))} - className="h-8 w-[300px]" - /> -
-
- - -
+
+ setFilters(prev => ({ ...prev, search: e.target.value }))} + className="max-w-xs" + /> + +
{/* Purchase Orders Table */} @@ -343,7 +370,7 @@ export default function PurchaseOrders() { {po.id} {po.vendor_name} {new Date(po.order_date).toLocaleDateString()} - {getStatusBadge(po.status)} + {getStatusBadge(po.status, po.receiving_status)} {po.total_items.toLocaleString()} {po.total_quantity.toLocaleString()} ${formatNumber(po.total_cost)} diff --git a/inventory/src/types/products.ts b/inventory/src/types/products.ts index 697d48e..bdeba2c 100644 --- a/inventory/src/types/products.ts +++ b/inventory/src/types/products.ts @@ -3,10 +3,10 @@ export interface Product { title: string; SKU: string; stock_quantity: number; - price: number; - regular_price: number; - cost_price: number; - landing_cost_price: number | null; + price: string; // DECIMAL(15,3) + regular_price: string; // DECIMAL(15,3) + cost_price: string; // DECIMAL(15,3) + landing_cost_price: string | null; // DECIMAL(15,3) barcode: string; vendor: string; vendor_reference: string; @@ -24,32 +24,32 @@ export interface Product { updated_at: string; // Metrics - daily_sales_avg?: number; - weekly_sales_avg?: number; - monthly_sales_avg?: number; - avg_quantity_per_order?: number; + daily_sales_avg?: string; // DECIMAL(15,3) + weekly_sales_avg?: string; // DECIMAL(15,3) + monthly_sales_avg?: string; // DECIMAL(15,3) + avg_quantity_per_order?: string; // DECIMAL(15,3) number_of_orders?: number; first_sale_date?: string; last_sale_date?: string; last_purchase_date?: string; - days_of_inventory?: number; - weeks_of_inventory?: number; - reorder_point?: number; - safety_stock?: number; - avg_margin_percent?: number; - total_revenue?: number; - inventory_value?: number; - cost_of_goods_sold?: number; - gross_profit?: number; - gmroi?: number; - avg_lead_time_days?: number; + days_of_inventory?: string; // DECIMAL(15,3) + weeks_of_inventory?: string; // DECIMAL(15,3) + reorder_point?: string; // DECIMAL(15,3) + safety_stock?: string; // DECIMAL(15,3) + avg_margin_percent?: string; // DECIMAL(15,3) + total_revenue?: string; // DECIMAL(15,3) + inventory_value?: string; // DECIMAL(15,3) + cost_of_goods_sold?: string; // DECIMAL(15,3) + gross_profit?: string; // DECIMAL(15,3) + gmroi?: string; // DECIMAL(15,3) + avg_lead_time_days?: string; // DECIMAL(15,3) last_received_date?: string; abc_class?: string; stock_status?: string; - turnover_rate?: number; - current_lead_time?: number; - target_lead_time?: number; + turnover_rate?: string; // DECIMAL(15,3) + current_lead_time?: string; // DECIMAL(15,3) + target_lead_time?: string; // DECIMAL(15,3) lead_time_status?: string; reorder_qty?: number; - overstocked_amt?: number; + overstocked_amt?: string; // DECIMAL(15,3) } diff --git a/inventory/src/types/status-codes.ts b/inventory/src/types/status-codes.ts new file mode 100644 index 0000000..1adfe06 --- /dev/null +++ b/inventory/src/types/status-codes.ts @@ -0,0 +1,81 @@ +// Purchase Order Status Codes +export enum PurchaseOrderStatus { + Canceled = 0, + Created = 1, + ElectronicallyReadySend = 10, + Ordered = 11, + Preordered = 12, + ElectronicallySent = 13, + ReceivingStarted = 15, + Done = 50 +} + +// Receiving Status Codes +export enum ReceivingStatus { + Canceled = 0, + Created = 1, + PartialReceived = 30, + FullReceived = 40, + Paid = 50 +} + +// Status Code Display Names +export const PurchaseOrderStatusLabels: Record = { + [PurchaseOrderStatus.Canceled]: 'Canceled', + [PurchaseOrderStatus.Created]: 'Created', + [PurchaseOrderStatus.ElectronicallyReadySend]: 'Ready to Send', + [PurchaseOrderStatus.Ordered]: 'Ordered', + [PurchaseOrderStatus.Preordered]: 'Preordered', + [PurchaseOrderStatus.ElectronicallySent]: 'Sent', + [PurchaseOrderStatus.ReceivingStarted]: 'Receiving Started', + [PurchaseOrderStatus.Done]: 'Done' +}; + +export const ReceivingStatusLabels: Record = { + [ReceivingStatus.Canceled]: 'Canceled', + [ReceivingStatus.Created]: 'Created', + [ReceivingStatus.PartialReceived]: 'Partially Received', + [ReceivingStatus.FullReceived]: 'Fully Received', + [ReceivingStatus.Paid]: 'Paid' +}; + +// Helper functions +export function getPurchaseOrderStatusLabel(status: number): string { + return PurchaseOrderStatusLabels[status as PurchaseOrderStatus] || 'Unknown'; +} + +export function getReceivingStatusLabel(status: number): string { + return ReceivingStatusLabels[status as ReceivingStatus] || 'Unknown'; +} + +// Status checks +export function isReceivingComplete(status: number): boolean { + return status >= ReceivingStatus.PartialReceived; +} + +export function isPurchaseOrderComplete(status: number): boolean { + return status === PurchaseOrderStatus.Done; +} + +export function isPurchaseOrderCanceled(status: number): boolean { + return status === PurchaseOrderStatus.Canceled; +} + +export function isReceivingCanceled(status: number): boolean { + return status === ReceivingStatus.Canceled; +} + +// Badge variants for different statuses +export function getPurchaseOrderStatusVariant(status: number): 'default' | 'secondary' | 'destructive' | 'outline' { + if (isPurchaseOrderCanceled(status)) return 'destructive'; + if (isPurchaseOrderComplete(status)) return 'default'; + if (status >= PurchaseOrderStatus.ElectronicallyReadySend) return 'secondary'; + return 'outline'; +} + +export function getReceivingStatusVariant(status: number): 'default' | 'secondary' | 'destructive' | 'outline' { + if (isReceivingCanceled(status)) return 'destructive'; + if (status === ReceivingStatus.Paid) return 'default'; + if (status >= ReceivingStatus.PartialReceived) return 'secondary'; + return 'outline'; +} \ No newline at end of file