253 lines
11 KiB
JavaScript
253 lines
11 KiB
JavaScript
'use strict';
|
||
const express = require('express');
|
||
const router = express.Router();
|
||
const { verifyToken } = require('../middleware/auth');
|
||
const { getPool } = require('../services/sqlPool');
|
||
|
||
// ── Open Production Orders ────────────────────────────────────────────────
|
||
function buildOpenProductionOrdersSQL(from, to, status, segment) {
|
||
const statusClause = status && status !== 'all'
|
||
? `AND OWOR.Status = '${status === 'Released' ? 'R' : 'P'}'`
|
||
: `AND OWOR.Status IN ('R','P')`;
|
||
|
||
const segmentClause = segment && segment !== 'all'
|
||
? `AND (
|
||
CASE
|
||
WHEN OITM.U_ItemSubGroup LIKE '%SFG Equipment%' OR OITM.U_ItemSubGroup LIKE '%EQUIPMENT%' THEN 'Equipment'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%Consumable%' OR OITM.U_ItemSubGroup LIKE '%Consumables%' THEN 'Consumable'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Stent%' THEN 'FG Stent'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG TT Device%' THEN 'FG TT Device'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%PLASTIC%' THEN 'PLASTIC'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%NEEDLE%' THEN 'NEEDLE'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%MOULDING%' THEN 'MOULDING'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG CAPD ACCESSORIES%' THEN 'FG CAPD ACCESSORIES'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG CAPD%' THEN 'FG CAPD'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%CAPD%' THEN 'CAPD'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Equipment%' THEN 'FG Equipment'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Blood Bag%' THEN 'FG Blood Bag'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%SFG Blood bag%' THEN 'SFG Blood bag'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%BLOOD BAG%' THEN 'BLOOD BAG'
|
||
ELSE 'Other'
|
||
END
|
||
) = '${segment.replace(/'/g, "''")}'`
|
||
: '';
|
||
|
||
return `
|
||
SELECT DISTINCT
|
||
OWOR.DocEntry,
|
||
k.SeriesName,
|
||
OWOR.DocNum,
|
||
CASE WHEN OWOR.Type = 'S' THEN 'Standard' ELSE 'Other' END AS Type,
|
||
CASE
|
||
WHEN OWOR.Status = 'R' THEN 'Released'
|
||
WHEN OWOR.Status = 'P' THEN 'Planned'
|
||
ELSE 'Unknown'
|
||
END AS Status,
|
||
OWOR.ItemCode AS ProductCode,
|
||
CASE
|
||
WHEN OITM.U_ItemSubGroup LIKE '%SFG Equipment%' OR OITM.U_ItemSubGroup LIKE '%EQUIPMENT%' THEN 'Equipment'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%Consumable%' OR OITM.U_ItemSubGroup LIKE '%Consumables%' THEN 'Consumable'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Stent%' THEN 'FG Stent'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG TT Device%' THEN 'FG TT Device'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%PLASTIC%' THEN 'PLASTIC'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%NEEDLE%' THEN 'NEEDLE'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%MOULDING%' THEN 'MOULDING'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG CAPD ACCESSORIES%' THEN 'FG CAPD ACCESSORIES'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG CAPD%' THEN 'FG CAPD'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%CAPD%' THEN 'CAPD'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Equipment%' THEN 'FG Equipment'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%FG Blood Bag%' THEN 'FG Blood Bag'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%SFG Blood bag%' THEN 'SFG Blood bag'
|
||
WHEN OITM.U_ItemSubGroup LIKE '%BLOOD BAG%' THEN 'BLOOD BAG'
|
||
ELSE 'Other'
|
||
END AS ProductSegment,
|
||
OWOR.ProdName AS ProductName,
|
||
OWOR.PlannedQty AS PlannedQuantity,
|
||
OWOR.CmpltQty AS CompletedQty,
|
||
CONVERT(VARCHAR(10), OWOR.PostDate, 105) AS PostDate,
|
||
CONVERT(VARCHAR(10), OWOR.StartDate, 105) AS StartDate,
|
||
CONVERT(VARCHAR(10), OWOR.DueDate, 105) AS DueDate
|
||
FROM OWOR
|
||
INNER JOIN WOR1 ON OWOR.DocEntry = WOR1.DocEntry
|
||
INNER JOIN OITM ON OWOR.ItemCode = OITM.ItemCode
|
||
LEFT JOIN NNM1 k ON k.Series = OWOR.Series
|
||
WHERE OWOR.PostDate BETWEEN @from AND @to
|
||
${statusClause}
|
||
AND OITM.U_ItemSubGroup <> 'FG Scrap'
|
||
${segmentClause}
|
||
ORDER BY OWOR.DocNum DESC`;
|
||
}
|
||
|
||
// ── GET /api/reports/open-production-orders ───────────────────────────────
|
||
router.get('/open-production-orders', verifyToken, async (req, res) => {
|
||
try {
|
||
let { from, to, status = 'all', segment = 'all' } = req.query;
|
||
|
||
// Default: last 15 days
|
||
if (!from || !to) {
|
||
const toDate = new Date();
|
||
toDate.setDate(toDate.getDate() - 1);
|
||
const frDate = new Date(toDate);
|
||
frDate.setDate(frDate.getDate() - 14);
|
||
to = toDate.toISOString().split('T')[0];
|
||
from = frDate.toISOString().split('T')[0];
|
||
}
|
||
|
||
const pool = await getPool();
|
||
const sql = buildOpenProductionOrdersSQL(from, to, status, segment);
|
||
const r = await pool.request()
|
||
.input('from', from)
|
||
.input('to', to)
|
||
.query(sql);
|
||
|
||
res.json({ success: true, data: r.recordset || [], count: (r.recordset || []).length, from, to });
|
||
} catch (err) {
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
// ── Revenue constants (mirrors chatbot.js) ────────────────────────────────
|
||
const EXCL_DOCS = `'552520145','552520146','552520147','552520465','552522652','552522653','552620186'`;
|
||
const EXCL_TM = `'4110201001','4110202001','4110201003','4110202003','4110201007','4110201002','4110201005'`;
|
||
const EXCL_PD = EXCL_TM + `,'4110202005','4110202007','4110201004'`;
|
||
const EXCL_ED = `'4110201001','4110201003','4110201007','4110201002','4110201005','4110201004'`;
|
||
const EXCL_EI = `'4110202001','4110202003','4110202005','4110202007'`;
|
||
|
||
function buildRevSubq(dateWhere, isCN) {
|
||
const tbl = isCN ? 'RIN1' : 'INV1';
|
||
const hdr = isCN ? 'ORIN' : 'OINV';
|
||
const sign = isCN ? '-' : '';
|
||
const extras = isCN ? '' : `
|
||
AND i.DocNum NOT IN (${EXCL_DOCS})
|
||
AND (t2.ItmsGrpNam IN ('FG CAPD','FG CAPD Accessories')
|
||
OR (i.DocStatus<>'C' OR i.InvntSttus='O'))`;
|
||
return `
|
||
SELECT
|
||
CASE WHEN (t2.ItmsGrpNam='FG BLOOD BAG'
|
||
OR (t2.ItmsGrpNam IN ('FG EQUIPMENT','SEMI FINISHED') AND t1.U_BagType='Equipment'))
|
||
AND t.AcctCode NOT IN (${EXCL_TM})
|
||
THEN ${sign}t.LineTotal ELSE 0 END AS TM,
|
||
CASE WHEN (t2.ItmsGrpNam IN ('FG CAPD','FG CAPD Accessories')
|
||
OR t1.U_PDGroup='Equipment')
|
||
AND t.AcctCode NOT IN (${EXCL_PD})
|
||
THEN ${sign}t.LineTotal ELSE 0 END AS PD,
|
||
CASE WHEN t.AcctCode IN (${EXCL_ED}) THEN ${sign}t.LineTotal ELSE 0 END AS ED,
|
||
CASE WHEN t.AcctCode IN (${EXCL_EI}) THEN ${sign}t.LineTotal ELSE 0 END AS EI
|
||
FROM ${tbl} t
|
||
LEFT JOIN OITM t1 ON t.ItemCode = t1.ItemCode
|
||
LEFT JOIN OITB t2 ON t1.ItmsGrpCod = t2.ItmsGrpCod
|
||
INNER JOIN ${hdr} i ON t.DocEntry = i.DocEntry
|
||
WHERE ${dateWhere} ${extras}`;
|
||
}
|
||
|
||
// ── GET /api/reports/sales?asOf=YYYY-MM-DD&period=all|daily|monthly|yearly ─
|
||
router.get('/sales', verifyToken, async (req, res) => {
|
||
try {
|
||
const { period = 'all' } = req.query;
|
||
|
||
const target = req.query.asOf ? new Date(req.query.asOf) : (() => {
|
||
const d = new Date(); d.setDate(d.getDate() - 1); return d;
|
||
})();
|
||
const toDate = target.toISOString().split('T')[0];
|
||
const dd = target.getDate(), mm = target.getMonth() + 1, yyyy = target.getFullYear();
|
||
|
||
const yearStart = (dd < 16 && mm <= 1) ? `${yyyy - 1}-01-16` : `${yyyy}-01-16`;
|
||
const pMm = mm === 1 ? 12 : mm - 1;
|
||
const pYyyy = mm === 1 ? yyyy - 1 : yyyy;
|
||
const monthStart = dd < 16
|
||
? `${pYyyy}-${String(pMm).padStart(2, '0')}-16`
|
||
: `${yyyy}-${String(mm).padStart(2, '0')}-16`;
|
||
|
||
const pt = period.toLowerCase();
|
||
const periods = [];
|
||
if (pt === 'daily' || pt === 'all') periods.push([`Daily (${toDate})`, `i.DocDate = '${toDate}'`]);
|
||
if (pt === 'monthly' || pt === 'all') periods.push([`Monthly (${monthStart} – ${toDate})`, `i.DocDate BETWEEN '${monthStart}' AND '${toDate}'`]);
|
||
if (pt === 'yearly' || pt === 'all') periods.push([`Yearly (${yearStart} – ${toDate})`, `i.DocDate BETWEEN '${yearStart}' AND '${toDate}'`]);
|
||
|
||
const pool = await getPool();
|
||
const rows = [];
|
||
for (const [label, dw] of periods) {
|
||
const sql = `
|
||
SELECT
|
||
'${label}' AS Period,
|
||
ROUND(SUM(TM), 2) AS TM_BloodBag,
|
||
ROUND(SUM(PD), 2) AS PD_CAPD,
|
||
ROUND(SUM(ED), 2) AS ExportDirect,
|
||
ROUND(SUM(EI), 2) AS ExportIndirect,
|
||
ROUND(SUM(TM)+SUM(PD)+SUM(ED)+SUM(EI), 2) AS Total
|
||
FROM (
|
||
${buildRevSubq(dw, false)}
|
||
UNION ALL
|
||
${buildRevSubq(dw, true)}
|
||
) CK`;
|
||
try {
|
||
const r = await pool.request().query(sql);
|
||
if (r.recordset?.[0]) rows.push(r.recordset[0]);
|
||
} catch (e) {
|
||
rows.push({ Period: label, error: e.message });
|
||
}
|
||
}
|
||
|
||
res.json({ success: true, data: rows, asOf: toDate });
|
||
} catch (err) {
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
// ── GET /api/reports/fg-inventory?from=&to=&warehouse=&itemGroup= ─────────
|
||
router.get('/fg-inventory', verifyToken, async (req, res) => {
|
||
try {
|
||
let { from, to, warehouse = '01', itemGroup = 'FG BLOOD BAG' } = req.query;
|
||
if (!from || !to) {
|
||
const d = new Date();
|
||
to = d.toISOString().split('T')[0];
|
||
const f = new Date(d.getFullYear(), d.getMonth(), 1);
|
||
from = f.toISOString().split('T')[0];
|
||
}
|
||
|
||
const pool = await getPool();
|
||
const r = await pool.request()
|
||
.input('FromDate', from)
|
||
.input('ToDate', to)
|
||
.input('WhsCode', warehouse)
|
||
.input('ItmsGrp', itemGroup)
|
||
.execute('INVENTORY_FG_AVG_PRICE');
|
||
|
||
const data = r.recordset || [];
|
||
res.json({ success: true, data, count: data.length, from, to, warehouse, itemGroup });
|
||
} catch (err) {
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
// ── GET /api/reports/bom-cost?itemGroup= ─────────────────────────────────
|
||
router.get('/bom-cost', verifyToken, async (req, res) => {
|
||
try {
|
||
const { itemGroup = 'FG BLOOD BAG' } = req.query;
|
||
const pool = await getPool();
|
||
const r = await pool.request()
|
||
.input('grp', itemGroup)
|
||
.query(`
|
||
SELECT T1.Code AS ItemCode, T0.ItemName, MIN(P.Price) AS Price
|
||
FROM OITT T1
|
||
INNER JOIN OITM T0 ON T0.ItemCode = T1.Code
|
||
LEFT JOIN ITM1 P ON P.ItemCode = T1.Code
|
||
LEFT JOIN OITB T2 ON T2.ItmsGrpCod = T0.ItmsGrpCod
|
||
WHERE T2.ItmsGrpNam = @grp AND P.Price > 0
|
||
GROUP BY T1.Code, T0.ItemName
|
||
ORDER BY T1.Code`);
|
||
|
||
const data = r.recordset || [];
|
||
res.json({ success: true, data, count: data.length, itemGroup });
|
||
} catch (err) {
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|
||
// Reused by routes/board.js so the Board Dashboard's Division Summary is
|
||
// computed by EXACTLY the same logic as this sales report (verified to match
|
||
// the management figures to the paisa).
|
||
module.exports.buildRevSubq = buildRevSubq;
|
||
module.exports.REV_CONST = { EXCL_DOCS, EXCL_TM, EXCL_PD, EXCL_ED, EXCL_EI };
|