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

@@ -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);