Update frontend to match part 3
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user