adamp 1ed2642e20 Add restock history tracking, payment tracking, and reports page
- New stock_adjustments table logs every stock change (restock, order
  create/update/delete) with reason and reference
- Orders now track payment_method and amount_paid with validation
- New /api/reports endpoint with 5 aggregation queries and date filtering
- Reports page with date range presets and sales, customer, revenue,
  status, and inventory sections
- Payment fields added to OrderNew and OrderDetail pages with balance due
- Girl Scouts trefoil logo added to header
- Vite dev server exposed on network for mobile access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:20:59 -06:00

122 lines
4.5 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { getDb } = require('../db');
router.get('/', (req, res) => {
try {
const db = getDb();
const rows = db.prepare('SELECT * FROM products ORDER BY name').all();
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/stock-history', (req, res) => {
try {
const db = getDb();
const { product_id, start, end, limit } = req.query;
let sql = `
SELECT sa.*, p.name as product_name
FROM stock_adjustments sa
JOIN products p ON p.id = sa.product_id
WHERE 1=1
`;
const params = [];
if (product_id) { sql += ' AND sa.product_id = ?'; params.push(product_id); }
if (start) { sql += ' AND sa.created_at >= ?'; params.push(start); }
if (end) { sql += ' AND sa.created_at <= ?'; params.push(end + ' 23:59:59'); }
sql += ' ORDER BY sa.created_at DESC';
if (limit) { sql += ' LIMIT ?'; params.push(Number(limit)); }
const rows = db.prepare(sql).all(...params);
res.json(rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/:id', (req, res) => {
try {
const db = getDb();
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Product not found' });
res.json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/', (req, res) => {
try {
const db = getDb();
const { name, price = 0, quantity_on_hand = 0, low_stock_threshold = 0 } = req.body;
if (!name || name.trim() === '') {
return res.status(400).json({ error: 'Name is required' });
}
const result = db.prepare(
'INSERT INTO products (name, price, quantity_on_hand, low_stock_threshold) VALUES (?, ?, ?, ?)'
).run(name.trim(), Number(price), Number(quantity_on_hand), Number(low_stock_threshold));
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/:id', (req, res) => {
try {
const db = getDb();
const { name, price, quantity_on_hand, low_stock_threshold } = req.body;
const existing = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ error: 'Product not found' });
const n = name !== undefined ? name.trim() : existing.name;
const p = price !== undefined ? Number(price) : existing.price;
const q = quantity_on_hand !== undefined ? Number(quantity_on_hand) : existing.quantity_on_hand;
const t = low_stock_threshold !== undefined ? Number(low_stock_threshold) : existing.low_stock_threshold;
if (!n) return res.status(400).json({ error: 'Name is required' });
db.prepare(
'UPDATE products SET name = ?, price = ?, quantity_on_hand = ?, low_stock_threshold = ? WHERE id = ?'
).run(n, p, q, t, req.params.id);
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id);
res.json(row);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.patch('/:id/stock', (req, res) => {
try {
const db = getDb();
const { adjustment } = req.body;
if (adjustment === undefined || adjustment === null) {
return res.status(400).json({ error: 'adjustment is required' });
}
const delta = Number(adjustment);
const row = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Product not found' });
const newQty = row.quantity_on_hand + delta;
if (newQty < 0) {
return res.status(400).json({ error: 'Quantity would go negative' });
}
db.prepare('UPDATE products SET quantity_on_hand = ? WHERE id = ?').run(newQty, req.params.id);
db.prepare('INSERT INTO stock_adjustments (product_id, adjustment, reason) VALUES (?, ?, ?)').run(req.params.id, delta, 'restock');
const updated = db.prepare('SELECT * FROM products WHERE id = ?').get(req.params.id);
res.json(updated);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/:id', (req, res) => {
try {
const db = getDb();
const result = db.prepare('DELETE FROM products WHERE id = ?').run(req.params.id);
if (result.changes === 0) return res.status(404).json({ error: 'Product not found' });
res.status(204).send();
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;