Update frontend to match part 3

This commit is contained in:
2025-01-28 13:40:28 -05:00
parent 57b0e9a120
commit 25a0bc8d4c
10 changed files with 377 additions and 232 deletions

View File

@@ -2,6 +2,9 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../utils/db'); const db = require('../utils/db');
// Import status codes
const { RECEIVING_STATUS } = require('../types/status-codes');
// Helper function to execute queries using the connection pool // Helper function to execute queries using the connection pool
async function executeQuery(sql, params = []) { async function executeQuery(sql, params = []) {
const pool = db.getPool(); const pool = db.getPool();
@@ -100,19 +103,27 @@ router.get('/purchase/metrics', async (req, res) => {
try { try {
const [rows] = await executeQuery(` const [rows] = await executeQuery(`
SELECT SELECT
COALESCE(COUNT(DISTINCT CASE WHEN po.receiving_status < 30 THEN po.po_id END), 0) as active_pos,
COALESCE(COUNT(DISTINCT CASE 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 THEN po.po_id
END), 0) as overdue_pos, 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 CAST(COALESCE(SUM(CASE
WHEN po.receiving_status < 30 WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED}
THEN po.ordered * po.cost_price THEN po.ordered * po.cost_price
ELSE 0 ELSE 0
END), 0) AS DECIMAL(15,3)) as total_cost, END), 0) AS DECIMAL(15,3)) as total_cost,
CAST(COALESCE(SUM(CASE CAST(COALESCE(SUM(CASE
WHEN po.receiving_status < 30 WHEN po.receiving_status < ${RECEIVING_STATUS.PARTIAL_RECEIVED}
THEN po.ordered * p.price THEN po.ordered * p.price
ELSE 0 ELSE 0
END), 0) AS DECIMAL(15,3)) as total_retail 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 CAST(COALESCE(SUM(po.ordered * p.price), 0) AS DECIMAL(15,3)) as order_retail
FROM purchase_orders po FROM purchase_orders po
JOIN products p ON po.pid = p.pid 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 GROUP BY po.vendor
HAVING order_cost > 0 HAVING order_cost > 0
ORDER BY order_cost DESC ORDER BY order_cost DESC

View File

@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const multer = require('multer'); const multer = require('multer');
const { importProductsFromCSV } = require('../utils/csvImporter'); const { importProductsFromCSV } = require('../utils/csvImporter');
const { PurchaseOrderStatus, ReceivingStatus } = require('../types/status-codes');
// Configure multer for file uploads // Configure multer for file uploads
const upload = multer({ dest: 'uploads/' }); const upload = multer({ dest: 'uploads/' });
@@ -642,70 +643,46 @@ router.get('/:id/metrics', async (req, res) => {
// Get product time series data // Get product time series data
router.get('/:id/time-series', async (req, res) => { router.get('/:id/time-series', async (req, res) => {
const pool = req.app.locals.pool; const { id } = req.params;
try { try {
const { id } = req.params; const pool = req.app.locals.pool;
const months = parseInt(req.query.months) || 12;
// Get monthly sales data with running totals and growth rates // Get monthly sales data
const [monthlySales] = await pool.query(` 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 SELECT
month, DATE_FORMAT(date, '%Y-%m') as month,
quantity, COUNT(DISTINCT order_id) as order_count,
revenue, SUM(quantity) as units_sold,
cost, CAST(SUM(price * quantity) AS DECIMAL(15,3)) as revenue,
avg_price, CAST(SUM((price - cost_price) * quantity) AS DECIMAL(15,3)) as profit
profit_margin, FROM order_items
inventory_value, WHERE pid = ?
LAG(quantity) OVER (ORDER BY month) as prev_month_quantity, AND canceled = false
LAG(revenue) OVER (ORDER BY month) as prev_month_revenue GROUP BY DATE_FORMAT(date, '%Y-%m')
FROM monthly_data ORDER BY month DESC
ORDER BY month ASC LIMIT 12
`, [id, months]); `, [id]);
// Calculate growth rates and format data // Format monthly sales data
const formattedMonthlySales = monthlySales.map(row => ({ const formattedMonthlySales = monthlySales.map(month => ({
month: row.month, month: month.month,
quantity: parseInt(row.quantity) || 0, order_count: parseInt(month.order_count),
revenue: parseFloat(row.revenue) || 0, units_sold: parseInt(month.units_sold),
cost: parseFloat(row.cost) || 0, revenue: parseFloat(month.revenue),
avg_price: parseFloat(row.avg_price) || 0, profit: parseFloat(month.profit)
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 recent orders with customer info and status // Get recent orders
const [recentOrders] = await pool.query(` const [recentOrders] = await pool.query(`
SELECT SELECT
DATE_FORMAT(date, '%Y-%m-%d') as date, DATE_FORMAT(date, '%Y-%m-%d') as date,
order_number, order_id,
quantity, quantity,
price, price,
discount, discount,
tax, tax,
shipping, shipping
customer, FROM order_items
status,
payment_method
FROM orders
WHERE pid = ? WHERE pid = ?
AND canceled = false AND canceled = false
ORDER BY date DESC ORDER BY date DESC
@@ -722,17 +699,19 @@ router.get('/:id/time-series', async (req, res) => {
ordered, ordered,
received, received,
status, status,
receiving_status,
cost_price, cost_price,
notes, notes,
CASE CASE
WHEN received_date IS NOT NULL THEN WHEN received_date IS NOT NULL THEN
DATEDIFF(received_date, date) 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) DATEDIFF(CURDATE(), expected_date)
ELSE NULL ELSE NULL
END as lead_time_days END as lead_time_days
FROM purchase_orders FROM purchase_orders
WHERE pid = ? WHERE pid = ?
AND status != ${PurchaseOrderStatus.CANCELED}
ORDER BY date DESC ORDER BY date DESC
LIMIT 10 LIMIT 10
`, [id]); `, [id]);
@@ -751,6 +730,8 @@ router.get('/:id/time-series', async (req, res) => {
...po, ...po,
ordered: parseInt(po.ordered), ordered: parseInt(po.ordered),
received: parseInt(po.received), received: parseInt(po.received),
status: parseInt(po.status),
receiving_status: parseInt(po.receiving_status),
cost_price: parseFloat(po.cost_price), cost_price: parseFloat(po.cost_price),
lead_time_days: po.lead_time_days ? parseInt(po.lead_time_days) : null lead_time_days: po.lead_time_days ? parseInt(po.lead_time_days) : null
})) }))

View File

@@ -1,6 +1,26 @@
const express = require('express'); const express = require('express');
const router = express.Router(); 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 // Get all purchase orders with summary metrics
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
@@ -11,13 +31,13 @@ router.get('/', async (req, res) => {
const params = []; const params = [];
if (search) { if (search) {
whereClause += ' AND (po.po_id LIKE ? OR po.vendor LIKE ? OR po.status LIKE ?)'; whereClause += ' AND (po.po_id LIKE ? OR po.vendor LIKE ?)';
params.push(`%${search}%`, `%${search}%`, `%${search}%`); params.push(`%${search}%`, `%${search}%`);
} }
if (status && status !== 'all') { if (status && status !== 'all') {
whereClause += ' AND po.status = ?'; whereClause += ' AND po.status = ?';
params.push(status); params.push(Number(status));
} }
if (vendor && vendor !== 'all') { if (vendor && vendor !== 'all') {
@@ -78,6 +98,7 @@ router.get('/', async (req, res) => {
vendor, vendor,
date, date,
status, status,
receiving_status,
COUNT(DISTINCT pid) as total_items, COUNT(DISTINCT pid) as total_items,
SUM(ordered) as total_quantity, SUM(ordered) as total_quantity,
CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost, CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_cost,
@@ -87,13 +108,14 @@ router.get('/', async (req, res) => {
) as fulfillment_rate ) as fulfillment_rate
FROM purchase_orders po FROM purchase_orders po
WHERE ${whereClause} WHERE ${whereClause}
GROUP BY po_id, vendor, date, status GROUP BY po_id, vendor, date, status, receiving_status
) )
SELECT SELECT
po_id as id, po_id as id,
vendor as vendor_name, vendor as vendor_name,
DATE_FORMAT(date, '%Y-%m-%d') as order_date, DATE_FORMAT(date, '%Y-%m-%d') as order_date,
status, status,
receiving_status,
total_items, total_items,
total_quantity, total_quantity,
total_cost, total_cost,
@@ -127,7 +149,7 @@ router.get('/', async (req, res) => {
const [statuses] = await pool.query(` const [statuses] = await pool.query(`
SELECT DISTINCT status SELECT DISTINCT status
FROM purchase_orders FROM purchase_orders
WHERE status IS NOT NULL AND status != '' WHERE status IS NOT NULL
ORDER BY status ORDER BY status
`); `);
@@ -136,7 +158,8 @@ router.get('/', async (req, res) => {
id: order.id, id: order.id,
vendor_name: order.vendor_name, vendor_name: order.vendor_name,
order_date: order.order_date, 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_items: Number(order.total_items) || 0,
total_quantity: Number(order.total_quantity) || 0, total_quantity: Number(order.total_quantity) || 0,
total_cost: Number(order.total_cost) || 0, total_cost: Number(order.total_cost) || 0,
@@ -165,7 +188,7 @@ router.get('/', async (req, res) => {
}, },
filters: { filters: {
vendors: vendors.map(v => v.vendor), vendors: vendors.map(v => v.vendor),
statuses: statuses.map(s => s.status) statuses: statuses.map(s => Number(s.status))
} }
}); });
} catch (error) { } catch (error) {
@@ -188,12 +211,14 @@ router.get('/vendor-metrics', async (req, res) => {
received, received,
cost_price, cost_price,
CASE 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) THEN DATEDIFF(received_date, date)
ELSE NULL ELSE NULL
END as delivery_days END as delivery_days
FROM purchase_orders FROM purchase_orders
WHERE vendor IS NOT NULL AND vendor != '' WHERE vendor IS NOT NULL AND vendor != ''
AND status != ${STATUS.CANCELED} -- Exclude canceled orders
) )
SELECT SELECT
vendor as vendor_name, vendor as vendor_name,
@@ -242,44 +267,47 @@ router.get('/cost-analysis', async (req, res) => {
const pool = req.app.locals.pool; const pool = req.app.locals.pool;
const [analysis] = await pool.query(` 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 SELECT
c.name as categories, category,
COUNT(DISTINCT po.pid) as unique_products, COUNT(DISTINCT pid) as unique_products,
CAST(AVG(po.cost_price) AS DECIMAL(15,3)) as avg_cost, CAST(AVG(cost_price) AS DECIMAL(15,3)) as avg_cost,
CAST(MIN(po.cost_price) AS DECIMAL(15,3)) as min_cost, CAST(MIN(cost_price) AS DECIMAL(15,3)) as min_cost,
CAST(MAX(po.cost_price) AS DECIMAL(15,3)) as max_cost, CAST(MAX(cost_price) AS DECIMAL(15,3)) as max_cost,
CAST(STDDEV(po.cost_price) AS DECIMAL(15,3)) as cost_std_dev, CAST(STDDEV(cost_price) AS DECIMAL(15,3)) as cost_variance,
CAST(SUM(po.ordered * po.cost_price) AS DECIMAL(15,3)) as total_spend CAST(SUM(ordered * cost_price) AS DECIMAL(15,3)) as total_spend
FROM purchase_orders po FROM category_costs
JOIN product_categories pc ON po.pid = pc.pid GROUP BY category
JOIN categories c ON pc.cat_id = c.cat_id
GROUP BY c.name
ORDER BY total_spend DESC ORDER BY total_spend DESC
`); `);
// Parse numeric values and add ids for React keys // Parse numeric values
const parsedAnalysis = analysis.map(item => ({ const parsedAnalysis = {
id: item.categories || 'Uncategorized', categories: analysis.map(cat => ({
categories: item.categories || 'Uncategorized', category: cat.category,
unique_products: Number(item.unique_products) || 0, unique_products: Number(cat.unique_products) || 0,
avg_cost: Number(item.avg_cost) || 0, avg_cost: Number(cat.avg_cost) || 0,
min_cost: Number(item.min_cost) || 0, min_cost: Number(cat.min_cost) || 0,
max_cost: Number(item.max_cost) || 0, max_cost: Number(cat.max_cost) || 0,
cost_variance: Number(item.cost_variance) || 0, cost_variance: Number(cat.cost_variance) || 0,
total_spend: Number(item.total_spend) || 0 total_spend: Number(cat.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)
})) }))
}; };
res.json(transformedAnalysis); res.json(parsedAnalysis);
} catch (error) { } catch (error) {
console.error('Error fetching cost analysis:', error); console.error('Error fetching cost analysis:', error);
res.status(500).json({ error: 'Failed to fetch cost analysis' }); 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 ( WITH po_totals AS (
SELECT SELECT
po_id, po_id,
status,
receiving_status,
SUM(ordered) as total_ordered, SUM(ordered) as total_ordered,
SUM(received) as total_received, 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 FROM purchase_orders
GROUP BY po_id WHERE status != ${STATUS.CANCELED}
GROUP BY po_id, status, receiving_status
) )
SELECT SELECT
COUNT(DISTINCT po_id) as order_count, COUNT(DISTINCT po_id) as order_count,
@@ -308,8 +339,20 @@ router.get('/receiving-status', async (req, res) => {
ROUND( ROUND(
SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3 SUM(total_received) / NULLIF(SUM(total_ordered), 0), 3
) as fulfillment_rate, ) as fulfillment_rate,
SUM(total_cost) as total_value, CAST(SUM(total_cost) AS DECIMAL(15,3)) as total_value,
ROUND(AVG(total_cost), 2) as avg_cost 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 FROM po_totals
`); `);
@@ -320,7 +363,13 @@ router.get('/receiving-status', async (req, res) => {
total_received: Number(status[0].total_received) || 0, total_received: Number(status[0].total_received) || 0,
fulfillment_rate: Number(status[0].fulfillment_rate) || 0, fulfillment_rate: Number(status[0].fulfillment_rate) || 0,
total_value: Number(status[0].total_value) || 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); res.json(parsedStatus);

View File

@@ -5,13 +5,14 @@ import config from "@/config"
import { formatCurrency } from "@/lib/utils" import { formatCurrency } from "@/lib/utils"
import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons
import { useState } from "react" import { useState } from "react"
import { PurchaseOrderStatus, ReceivingStatus } from "@/types/status-codes"
interface PurchaseMetricsData { interface PurchaseMetricsData {
activePurchaseOrders: number activePurchaseOrders: number // Orders that are not canceled, done, or fully received
overduePurchaseOrders: number overduePurchaseOrders: number // Orders past their expected delivery date
onOrderUnits: number onOrderUnits: number // Total units across all active orders
onOrderCost: number onOrderCost: number // Total cost across all active orders
onOrderRetail: number onOrderRetail: number // Total retail value across all active orders
vendorOrders: { vendorOrders: {
vendor: string vendor: string
orders: number orders: number

View File

@@ -15,10 +15,10 @@ interface Product {
pid: number; pid: number;
sku: string; sku: string;
title: string; title: string;
daily_sales_avg: number; daily_sales_avg: string;
weekly_sales_avg: number; weekly_sales_avg: string;
growth_rate: number; growth_rate: string;
total_revenue: number; total_revenue: string;
} }
export function TrendingProducts() { export function TrendingProducts() {
@@ -75,20 +75,20 @@ export function TrendingProducts() {
</span> </span>
</div> </div>
</TableCell> </TableCell>
<TableCell>{parseFloat(product.daily_sales_avg).toFixed(1)}</TableCell> <TableCell>{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
{parseFloat(product.growth_rate) > 0 ? ( {Number(product.growth_rate) > 0 ? (
<TrendingUp className="h-4 w-4 text-success" /> <TrendingUp className="h-4 w-4 text-success" />
) : ( ) : (
<TrendingDown className="h-4 w-4 text-destructive" /> <TrendingDown className="h-4 w-4 text-destructive" />
)} )}
<span <span
className={ className={
parseFloat(product.growth_rate) > 0 ? "text-success" : "text-destructive" Number(product.growth_rate) > 0 ? "text-success" : "text-destructive"
} }
> >
{formatPercent(parseFloat(product.growth_rate))} {formatPercent(Number(product.growth_rate))}
</span> </span>
</div> </div>
</TableCell> </TableCell>

View File

@@ -24,7 +24,7 @@ type FilterValue = string | number | boolean;
type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between"; type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between";
interface FilterValueWithOperator { interface FilterValueWithOperator {
value: FilterValue | [number, number]; value: FilterValue | [string, string];
operator: ComparisonOperator; operator: ComparisonOperator;
} }
@@ -317,18 +317,32 @@ export function ProductFilters({
}); });
}, []); }, []);
const handleApplyFilter = (value: FilterValue | [number, number]) => { const handleApplyFilter = (value: FilterValue | [string, string]) => {
if (!selectedFilter) return; if (!selectedFilter) return;
const newFilters = { let filterValue: ActiveFilterValue;
...activeFilters,
[selectedFilter.id]: { if (selectedFilter.type === "number") {
value, if (selectedOperator === "between" && Array.isArray(value)) {
operator: selectedOperator, 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<string, ActiveFilterValue>);
handlePopoverClose(); handlePopoverClose();
}; };
@@ -394,38 +408,14 @@ export function ProductFilters({
const getFilterDisplayValue = (filter: ActiveFilter) => { const getFilterDisplayValue = (filter: ActiveFilter) => {
const filterValue = activeFilters[filter.id]; if (typeof filter.value === "object" && "operator" in filter.value) {
const filterOption = filterOptions.find((opt) => opt.id === filter.id); const { operator, value } = filter.value;
if (Array.isArray(value)) {
// For between ranges return `${operator} ${value[0]} and ${value[1]}`;
if (Array.isArray(filterValue)) { }
return `${filter.label} between ${filterValue[0]} and ${filterValue[1]}`; return `${operator} ${value}`;
} }
return filter.value.toString();
// 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 ( return (

View File

@@ -261,6 +261,11 @@ export function ProductTable({
return columnDef.format(num); 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 columnDef.format(value);
} }
return value ?? '-'; return value ?? '-';

View File

@@ -20,12 +20,21 @@ import {
PaginationPrevious, PaginationPrevious,
} from '../components/ui/pagination'; } from '../components/ui/pagination';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import {
PurchaseOrderStatus,
ReceivingStatus as ReceivingStatusCode,
getPurchaseOrderStatusLabel,
getReceivingStatusLabel,
getPurchaseOrderStatusVariant,
getReceivingStatusVariant
} from '../types/status-codes';
interface PurchaseOrder { interface PurchaseOrder {
id: number; id: number;
vendor_name: string; vendor_name: string;
order_date: string; order_date: string;
status: string; status: number;
receiving_status: number;
total_items: number; total_items: number;
total_quantity: number; total_quantity: number;
total_cost: number; total_cost: number;
@@ -113,6 +122,16 @@ export default function PurchaseOrders() {
limit: 100, 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 () => { const fetchData = async () => {
try { try {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
@@ -171,16 +190,25 @@ export default function PurchaseOrders() {
} }
}; };
const getStatusBadge = (status: string) => { const getStatusBadge = (status: number, receivingStatus: number) => {
const variants: Record<string, { variant: "default" | "secondary" | "destructive" | "outline"; label: string }> = { // If the PO is canceled, show that status
pending: { variant: "outline", label: "Pending" }, if (status === PurchaseOrderStatus.Canceled) {
received: { variant: "default", label: "Received" }, return <Badge variant={getPurchaseOrderStatusVariant(status)}>
partial: { variant: "secondary", label: "Partial" }, {getPurchaseOrderStatusLabel(status)}
cancelled: { variant: "destructive", label: "Cancelled" }, </Badge>;
}; }
const statusConfig = variants[status.toLowerCase()] || variants.pending; // If receiving has started, show receiving status
return <Badge variant={statusConfig.variant}>{statusConfig.label}</Badge>; if (status >= PurchaseOrderStatus.ReceivingStarted) {
return <Badge variant={getReceivingStatusVariant(receivingStatus)}>
{getReceivingStatusLabel(receivingStatus)}
</Badge>;
}
// Otherwise show PO status
return <Badge variant={getPurchaseOrderStatusVariant(status)}>
{getPurchaseOrderStatusLabel(status)}
</Badge>;
}; };
const formatNumber = (value: number) => { const formatNumber = (value: number) => {
@@ -252,45 +280,44 @@ export default function PurchaseOrders() {
</div> </div>
{/* Filters */} {/* Filters */}
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center"> <div className="mb-4 flex items-center gap-4">
<div className="flex items-center gap-2 flex-1"> <Input
<Input placeholder="Search orders..."
placeholder="Search orders..." value={filters.search}
value={filters.search} onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))} className="max-w-xs"
className="h-8 w-[300px]" />
/> <Select
</div> value={filters.status}
<div className="flex flex-wrap items-center gap-2"> onValueChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
<Select >
value={filters.status} <SelectTrigger className="w-[180px]">
onValueChange={(value) => setFilters(prev => ({ ...prev, status: value }))} <SelectValue placeholder="Select status" />
> </SelectTrigger>
<SelectTrigger className="h-8 w-[180px]"> <SelectContent>
<SelectValue placeholder="Status" /> {STATUS_FILTER_OPTIONS.map(option => (
</SelectTrigger> <SelectItem key={option.value} value={option.value}>
<SelectContent> {option.label}
<SelectItem value="all">All Statuses</SelectItem> </SelectItem>
{filterOptions.statuses.map(status => ( ))}
<SelectItem key={status} value={status}>{status}</SelectItem> </SelectContent>
))} </Select>
</SelectContent> <Select
</Select> value={filters.vendor}
<Select onValueChange={(value) => setFilters(prev => ({ ...prev, vendor: value }))}
value={filters.vendor} >
onValueChange={(value) => setFilters(prev => ({ ...prev, vendor: value }))} <SelectTrigger className="w-[180px]">
> <SelectValue placeholder="Select vendor" />
<SelectTrigger className="h-8 w-[180px]"> </SelectTrigger>
<SelectValue placeholder="Vendor" /> <SelectContent>
</SelectTrigger> <SelectItem value="all">All Vendors</SelectItem>
<SelectContent> {filterOptions.vendors.map(vendor => (
<SelectItem value="all">All Vendors</SelectItem> <SelectItem key={vendor} value={vendor}>
{filterOptions.vendors.map(vendor => ( {vendor}
<SelectItem key={vendor} value={vendor}>{vendor}</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div>
</div> </div>
{/* Purchase Orders Table */} {/* Purchase Orders Table */}
@@ -343,7 +370,7 @@ export default function PurchaseOrders() {
<TableCell>{po.id}</TableCell> <TableCell>{po.id}</TableCell>
<TableCell>{po.vendor_name}</TableCell> <TableCell>{po.vendor_name}</TableCell>
<TableCell>{new Date(po.order_date).toLocaleDateString()}</TableCell> <TableCell>{new Date(po.order_date).toLocaleDateString()}</TableCell>
<TableCell>{getStatusBadge(po.status)}</TableCell> <TableCell>{getStatusBadge(po.status, po.receiving_status)}</TableCell>
<TableCell>{po.total_items.toLocaleString()}</TableCell> <TableCell>{po.total_items.toLocaleString()}</TableCell>
<TableCell>{po.total_quantity.toLocaleString()}</TableCell> <TableCell>{po.total_quantity.toLocaleString()}</TableCell>
<TableCell>${formatNumber(po.total_cost)}</TableCell> <TableCell>${formatNumber(po.total_cost)}</TableCell>

View File

@@ -3,10 +3,10 @@ export interface Product {
title: string; title: string;
SKU: string; SKU: string;
stock_quantity: number; stock_quantity: number;
price: number; price: string; // DECIMAL(15,3)
regular_price: number; regular_price: string; // DECIMAL(15,3)
cost_price: number; cost_price: string; // DECIMAL(15,3)
landing_cost_price: number | null; landing_cost_price: string | null; // DECIMAL(15,3)
barcode: string; barcode: string;
vendor: string; vendor: string;
vendor_reference: string; vendor_reference: string;
@@ -24,32 +24,32 @@ export interface Product {
updated_at: string; updated_at: string;
// Metrics // Metrics
daily_sales_avg?: number; daily_sales_avg?: string; // DECIMAL(15,3)
weekly_sales_avg?: number; weekly_sales_avg?: string; // DECIMAL(15,3)
monthly_sales_avg?: number; monthly_sales_avg?: string; // DECIMAL(15,3)
avg_quantity_per_order?: number; avg_quantity_per_order?: string; // DECIMAL(15,3)
number_of_orders?: number; number_of_orders?: number;
first_sale_date?: string; first_sale_date?: string;
last_sale_date?: string; last_sale_date?: string;
last_purchase_date?: string; last_purchase_date?: string;
days_of_inventory?: number; days_of_inventory?: string; // DECIMAL(15,3)
weeks_of_inventory?: number; weeks_of_inventory?: string; // DECIMAL(15,3)
reorder_point?: number; reorder_point?: string; // DECIMAL(15,3)
safety_stock?: number; safety_stock?: string; // DECIMAL(15,3)
avg_margin_percent?: number; avg_margin_percent?: string; // DECIMAL(15,3)
total_revenue?: number; total_revenue?: string; // DECIMAL(15,3)
inventory_value?: number; inventory_value?: string; // DECIMAL(15,3)
cost_of_goods_sold?: number; cost_of_goods_sold?: string; // DECIMAL(15,3)
gross_profit?: number; gross_profit?: string; // DECIMAL(15,3)
gmroi?: number; gmroi?: string; // DECIMAL(15,3)
avg_lead_time_days?: number; avg_lead_time_days?: string; // DECIMAL(15,3)
last_received_date?: string; last_received_date?: string;
abc_class?: string; abc_class?: string;
stock_status?: string; stock_status?: string;
turnover_rate?: number; turnover_rate?: string; // DECIMAL(15,3)
current_lead_time?: number; current_lead_time?: string; // DECIMAL(15,3)
target_lead_time?: number; target_lead_time?: string; // DECIMAL(15,3)
lead_time_status?: string; lead_time_status?: string;
reorder_qty?: number; reorder_qty?: number;
overstocked_amt?: number; overstocked_amt?: string; // DECIMAL(15,3)
} }

View File

@@ -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, string> = {
[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, string> = {
[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';
}