Update frontend to match part 3
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// Calculate growth rates and format data
|
// Get monthly sales data
|
||||||
const formattedMonthlySales = monthlySales.map(row => ({
|
const [monthlySales] = await pool.query(`
|
||||||
month: row.month,
|
SELECT
|
||||||
quantity: parseInt(row.quantity) || 0,
|
DATE_FORMAT(date, '%Y-%m') as month,
|
||||||
revenue: parseFloat(row.revenue) || 0,
|
COUNT(DISTINCT order_id) as order_count,
|
||||||
cost: parseFloat(row.cost) || 0,
|
SUM(quantity) as units_sold,
|
||||||
avg_price: parseFloat(row.avg_price) || 0,
|
CAST(SUM(price * quantity) AS DECIMAL(15,3)) as revenue,
|
||||||
profit_margin: parseFloat(row.profit_margin) || 0,
|
CAST(SUM((price - cost_price) * quantity) AS DECIMAL(15,3)) as profit
|
||||||
inventory_value: parseFloat(row.inventory_value) || 0,
|
FROM order_items
|
||||||
quantity_growth: row.prev_month_quantity ?
|
WHERE pid = ?
|
||||||
((row.quantity - row.prev_month_quantity) / row.prev_month_quantity) * 100 : 0,
|
AND canceled = false
|
||||||
revenue_growth: row.prev_month_revenue ?
|
GROUP BY DATE_FORMAT(date, '%Y-%m')
|
||||||
((row.revenue - row.prev_month_revenue) / row.prev_month_revenue) * 100 : 0
|
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(`
|
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
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 ?? '-';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
81
inventory/src/types/status-codes.ts
Normal file
81
inventory/src/types/status-codes.ts
Normal 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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user