Auth fixes, show correct cost each value on pos
This commit is contained in:
@@ -11,6 +11,17 @@ async function executeQuery(sql, params = []) {
|
||||
return pool.query(sql, params);
|
||||
}
|
||||
|
||||
// Identity probe for the small dashboard / kiosk flow. Lives under /api/dashboard/*
|
||||
// so it sits behind Caddy's office-IP `client_ip` allowlist — the office kiosk can
|
||||
// reach it without a token, while non-office requests must carry a valid Bearer.
|
||||
// Reaches this handler only if shared `authenticate` middleware populated req.user.
|
||||
router.get('/whoami', (req, res) => {
|
||||
res.json({
|
||||
authenticated: !!req.user,
|
||||
is_kiosk: !!req.user?.is_kiosk,
|
||||
});
|
||||
});
|
||||
|
||||
// GET /dashboard/stock/metrics
|
||||
// Returns brand-level stock metrics
|
||||
router.get('/stock/metrics', async (req, res) => {
|
||||
|
||||
@@ -496,7 +496,15 @@ router.get('/search', async (req, res) => {
|
||||
|
||||
// Batch lookup of product display data by pid list (used by Create PO page)
|
||||
// Accepts ?pids=1,2,3 — comma-separated; de-duped server-side; capped at 500.
|
||||
// Optional ?supplierId=<n> — when present, current_cost_price is computed via
|
||||
// the same fallback chain the legacy PHP backend uses in clsPO::_product_add()
|
||||
// (po.class.php:189-209):
|
||||
// supplier_id == 92 (Notions) → products.notions_cost_each
|
||||
// else / fallback → products.supplier_cost_each
|
||||
// final fallback → most recent receivings.cost_each (>0)
|
||||
// Returns rows in the same order as the deduped input pids; missing pids are silently dropped.
|
||||
const NOTIONS_SUPPLIER_ID = 92;
|
||||
|
||||
router.get('/batch', async (req, res) => {
|
||||
const pool = req.app.locals.pool;
|
||||
const raw = req.query.pids;
|
||||
@@ -514,6 +522,11 @@ router.get('/batch', async (req, res) => {
|
||||
return res.status(400).json({ error: 'No valid pids provided' });
|
||||
}
|
||||
|
||||
const supplierIdRaw = req.query.supplierId;
|
||||
const supplierId = supplierIdRaw != null && /^\d+$/.test(String(supplierIdRaw))
|
||||
? parseInt(String(supplierIdRaw), 10)
|
||||
: null;
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(`
|
||||
SELECT
|
||||
@@ -528,7 +541,17 @@ router.get('/batch', async (req, res) => {
|
||||
p.baskets,
|
||||
pm.on_order_qty,
|
||||
p.total_sold,
|
||||
pm.current_cost_price,
|
||||
p.notions_cost_each,
|
||||
p.supplier_cost_each,
|
||||
(
|
||||
SELECT r.cost_each
|
||||
FROM receivings r
|
||||
WHERE r.pid = p.pid
|
||||
AND r.cost_each > 0
|
||||
AND r.status <> 'canceled'
|
||||
ORDER BY r.received_date DESC
|
||||
LIMIT 1
|
||||
) AS last_received_cost,
|
||||
pm.date_last_sold,
|
||||
pm.date_first_received,
|
||||
p.moq
|
||||
@@ -537,10 +560,30 @@ router.get('/batch', async (req, res) => {
|
||||
WHERE p.pid = ANY($1::bigint[])
|
||||
`, [pids]);
|
||||
|
||||
const pickCost = (r) => {
|
||||
// Treat 0 as "unset" the same way the PHP code does (`if (!$cost_each)`).
|
||||
const notions = Number(r.notions_cost_each) || 0;
|
||||
const supplier = Number(r.supplier_cost_each) || 0;
|
||||
const lastReceived = Number(r.last_received_cost) || 0;
|
||||
if (supplierId === NOTIONS_SUPPLIER_ID && notions > 0) return notions;
|
||||
if (supplier > 0) return supplier;
|
||||
if (lastReceived > 0) return lastReceived;
|
||||
return null;
|
||||
};
|
||||
|
||||
// products.pid is BIGINT, which the pg driver returns as a STRING by
|
||||
// default (to preserve precision for values > 2^53). Coerce to Number
|
||||
// so the JSON response has numeric pids and Map lookups work.
|
||||
const normalized = rows.map(r => ({ ...r, pid: Number(r.pid) }));
|
||||
const normalized = rows.map(r => ({
|
||||
...r,
|
||||
pid: Number(r.pid),
|
||||
notions_cost_each: r.notions_cost_each != null ? Number(r.notions_cost_each) : null,
|
||||
supplier_cost_each: r.supplier_cost_each != null ? Number(r.supplier_cost_each) : null,
|
||||
last_received_cost: r.last_received_cost != null ? Number(r.last_received_cost) : null,
|
||||
// current_cost_price = the value that will land on the PO if this product
|
||||
// is added with the given supplier (or non-Notions default when omitted).
|
||||
current_cost_price: pickCost(r),
|
||||
}));
|
||||
const byPid = new Map(normalized.map(r => [r.pid, r]));
|
||||
// Preserve the requested order so the frontend can append rows in input order
|
||||
const ordered = pids.map(pid => byPid.get(pid)).filter(Boolean);
|
||||
|
||||
@@ -967,6 +967,34 @@ router.get('/order-vs-received', async (req, res) => {
|
||||
});
|
||||
|
||||
// Get purchase order items
|
||||
// Lightweight lookup of a PO's supplier_id by po_id, used by the Create-PO
|
||||
// page's "Add to existing PO" mode so we can compute the right cost-each
|
||||
// fallback branch (Notions supplier 92 vs everyone else) without fetching
|
||||
// the full PO contents.
|
||||
router.get('/:id/supplier', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
const { id } = req.params;
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'Purchase order ID is required' });
|
||||
}
|
||||
const { rows } = await pool.query(
|
||||
`SELECT supplier_id, vendor FROM purchase_orders WHERE po_id = $1 LIMIT 1`,
|
||||
[String(id)]
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Purchase order not found' });
|
||||
}
|
||||
res.json({
|
||||
supplierId: rows[0].supplier_id != null ? Number(rows[0].supplier_id) : null,
|
||||
vendor: rows[0].vendor || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching PO supplier:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch PO supplier' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id/items', async (req, res) => {
|
||||
try {
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
@@ -78,6 +78,12 @@ requiredDirs.forEach((dir) => {
|
||||
|
||||
const app = express();
|
||||
|
||||
// Trust X-Forwarded-* only when the immediate hop is loopback (Caddy on the same
|
||||
// host). Anything stricter would leave req.ip as 127.0.0.1; anything looser would
|
||||
// let arbitrary clients spoof their source IP via X-Forwarded-For. Required for
|
||||
// the KIOSK_IPS bypass in shared/auth/middleware.js to match real client IPs.
|
||||
app.set('trust proxy', 'loopback');
|
||||
|
||||
// Phase 6.5/6.9: structured access log (replaces the previous header-dumping debug
|
||||
// middleware that wrote raw Authorization values to stdout). Pino redaction strips
|
||||
// `authorization` and `cookie` automatically — see shared/logging/logger.js.
|
||||
|
||||
Reference in New Issue
Block a user