// services/rmOvgSettingsStore.js // Admin-configured Raw Material "Ovg%" override — maps an item code and/or // its SAP item group to a default overage percentage. Used by work-order.html // to pre-fill a raw material row's Ovg. cell and to recompute "Qty Req./100 ml" // as (BOM value) − (BOM value × Ovg%/100) instead of the plain BOM figure, // for any item this resolves a match for. See [[work-order-qty-req-two-way-sync]] // for the existing Raw Material calc model this feature layers on top of. const sql = require('mssql'); const TABLE = `[dbo].[ZRM_OVG_SETTINGS]`; let _conn = null; async function getConn() { if (_conn) return _conn; _conn = await sql.connect({ server: process.env.APP_SQL_HOST, port: parseInt(process.env.APP_SQL_PORT), user: process.env.APP_SQL_USER, password: process.env.APP_SQL_PASSWORD, database: process.env.APP_SQL_DATABASE, options: { encrypt: true, trustServerCertificate: true }, }); return _conn; } async function exec(sqlQuery, params = []) { const conn = await getConn(); const request = conn.request(); params.forEach((param, index) => { request.input(`param${index}`, param); }); const replacedSql = sqlQuery.replace(/\?/g, (m, offset, string) => { const i = (string.slice(0, offset).match(/\?/g) || []).length; return `@param${i}`; }); const result = await request.query(replacedSql); return result.recordset || []; } function isAlreadyExists(e) { const m = (e.message || '').toLowerCase(); return m.includes('already exists') || m.includes('duplicate') || m.includes('existing object') || m.includes('there is already an object'); } async function bootstrap() { console.log('[RM-OVG-SETTINGS] Checking table', TABLE, '...'); await exec(` CREATE TABLE ${TABLE} ( ID INT IDENTITY(1,1) PRIMARY KEY, ITEM_CODE NVARCHAR(60) NULL, ITEM_GROUP INT NULL, OVG_PERCENT FLOAT NOT NULL, CREATED_BY NVARCHAR(50), CREATED_AT DATETIME2, UPDATED_BY NVARCHAR(50), UPDATED_AT DATETIME2 ) `).catch(e => { if (isAlreadyExists(e)) { console.log('[RM-OVG-SETTINGS] Table exists — OK'); } else throw e; }); console.log('[RM-OVG-SETTINGS] ✅ Ready'); } function fromRow(row) { return { id: row.ID, itemCode: row.ITEM_CODE || '', itemGroup: row.ITEM_GROUP != null ? row.ITEM_GROUP : null, ovgPercent: row.OVG_PERCENT, createdBy: row.CREATED_BY || '', createdAt: row.CREATED_AT ? new Date(row.CREATED_AT).toISOString() : null, updatedBy: row.UPDATED_BY || '', updatedAt: row.UPDATED_AT ? new Date(row.UPDATED_AT).toISOString() : null, }; } async function listAll() { const rows = await exec(`SELECT * FROM ${TABLE} ORDER BY ITEM_CODE, ITEM_GROUP`); return rows.map(fromRow); } async function create({ itemCode, itemGroup, ovgPercent, by }) { const code = (itemCode || '').trim().toUpperCase() || null; const group = itemGroup != null && itemGroup !== '' ? parseInt(itemGroup) : null; if (!code && group == null) throw new Error('Provide an Item Code and/or Item Group'); const pct = parseFloat(ovgPercent); if (isNaN(pct)) throw new Error('Ovg% must be a number'); const now = new Date().toISOString(); const idRows = await exec(` INSERT INTO ${TABLE} (ITEM_CODE, ITEM_GROUP, OVG_PERCENT, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT) VALUES (?,?,?,?,?,?,?); SELECT SCOPE_IDENTITY() AS ID; `, [code, group, pct, by || '', now.replace('T', ' ').substring(0, 23), by || '', now.replace('T', ' ').substring(0, 23)]); const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [idRows[0].ID]); return fromRow(rows[0]); } async function update(id, { itemCode, itemGroup, ovgPercent, by }) { const code = (itemCode || '').trim().toUpperCase() || null; const group = itemGroup != null && itemGroup !== '' ? parseInt(itemGroup) : null; if (!code && group == null) throw new Error('Provide an Item Code and/or Item Group'); const pct = parseFloat(ovgPercent); if (isNaN(pct)) throw new Error('Ovg% must be a number'); const now = new Date().toISOString().replace('T', ' ').substring(0, 23); await exec(`UPDATE ${TABLE} SET ITEM_CODE=?, ITEM_GROUP=?, OVG_PERCENT=?, UPDATED_BY=?, UPDATED_AT=? WHERE ID=?`, [code, group, pct, by || '', now, parseInt(id)]); const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]); if (!rows.length) throw new Error('Not found'); return fromRow(rows[0]); } async function remove(id) { await exec(`DELETE FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]); } // Resolves an Ovg% for each item code, given that item's own SAP item group // (caller looks the group up from OITM — this store has no SAP connection of // its own). Item-code-specific rows win over item-group rows for the same // item. Returns { itemCode: percent } — items with no match are simply // absent from the result (never a 0 entry, since 0 IS a valid configured %). async function resolveForItems(itemCodesUpper, groupByItemCode) { const rows = await listAll(); const byCode = {}; const byGroup = {}; rows.forEach(r => { if (r.itemCode) byCode[r.itemCode] = r.ovgPercent; else if (r.itemGroup != null) byGroup[r.itemGroup] = r.ovgPercent; }); const out = {}; itemCodesUpper.forEach(code => { if (byCode[code] != null) { out[code] = byCode[code]; return; } const grp = groupByItemCode[code]; if (grp != null && byGroup[grp] != null) out[code] = byGroup[grp]; }); return out; } module.exports = { bootstrap, listAll, create, update, remove, resolveForItems };