'use strict'; // services/oeeStore.js — "Overall Equipment Efficiency" (OEE) data entry. // Stored in the PORTAL's own app DB (not SAP), one document per (tab, month): // a monthly sheet for one production area (EBB Production, PD Production, …). // The row grid (date + shift + all the downtime/output inputs, plus the // derived rates) is kept verbatim as a JSON array — the per-tab column schema // and formulas live on the client (public/oee.html); the server just persists // what it is given and enforces per-user tab permissions (see routes/oee.js). const { getPool } = require('./appSqlPool'); const TABLE = `[dbo].[ZOEE_ENTRIES]`; async function exec(sqlQuery, params = []) { const pool = await getPool(); const req = pool.request(); let i = 0; const text = sqlQuery.replace(/\?/g, () => { const n = `p${i}`; req.input(n, params[i]); i++; return `@${n}`; }); const result = await req.query(text); return result.recordset || []; } function isAlreadyExists(e) { const m = (e.message || '').toLowerCase(); return m.includes('already exists') || m.includes('there is already an object'); } async function bootstrap() { console.log('[OEE] Checking table', TABLE, '…'); await exec(` CREATE TABLE ${TABLE} ( ID INT IDENTITY(1,1) PRIMARY KEY, COMPANY NVARCHAR(100), TAB NVARCHAR(50) NOT NULL, MONTH NVARCHAR(20) NOT NULL, ROWS_JSON NVARCHAR(MAX), REMARK NVARCHAR(MAX), CANCELED BIT DEFAULT 0, CREATED_BY NVARCHAR(50), CREATED_BY_ID INT, CREATED_AT DATETIME2, UPDATED_AT DATETIME2 ) `).catch(e => { if (!isAlreadyExists(e)) throw e; else console.log('[OEE] Table already exists — OK'); }); console.log('[OEE] ✅ Ready'); } function safeJson(v, fb) { if (!v) return fb; try { return JSON.parse(v); } catch (_e) { return fb; } } function fromRow(r, withRows) { if (!r) return null; const o = { id: r.ID, company: r.COMPANY || '', tab: r.TAB, month: r.MONTH, remark: r.REMARK || '', canceled: !!r.CANCELED, createdBy: r.CREATED_BY || '', createdById: r.CREATED_BY_ID || null, createdAt: r.CREATED_AT ? new Date(r.CREATED_AT).toISOString() : null, updatedAt: r.UPDATED_AT ? new Date(r.UPDATED_AT).toISOString() : null, rowCount: 0, }; const rows = safeJson(r.ROWS_JSON, []); o.rowCount = Array.isArray(rows) ? rows.length : 0; if (withRows) o.rows = Array.isArray(rows) ? rows : []; return o; } // List documents (headers only). Optional filters: company, tab, month, and // an allow-set of tabs (per-user restriction — omit for no restriction). async function list({ company, tab, month, monthFrom, monthTo, allowTabs, top = 30, skip = 0 } = {}) { const where = []; const params = []; if (company) { where.push(`COMPANY = ?`); params.push(company); } if (tab) { where.push(`TAB = ?`); params.push(tab); } if (month) { where.push(`MONTH = ?`); params.push(month); } if (monthFrom) { where.push(`MONTH >= ?`); params.push(monthFrom); } if (monthTo) { where.push(`MONTH <= ?`); params.push(monthTo); } if (Array.isArray(allowTabs) && allowTabs.length) { where.push(`TAB IN (${allowTabs.map(() => '?').join(',')})`); allowTabs.forEach(t => params.push(t)); } const w = where.length ? `WHERE ${where.join(' AND ')}` : ''; const rows = await exec( `SELECT * FROM ${TABLE} ${w} ORDER BY ID DESC OFFSET ${Math.max(0, skip | 0)} ROWS FETCH NEXT ${Math.min(100, top | 0)} ROWS ONLY`, params ); // Genuine total (same filter, no paging) — drives "Page X of Y" on the // client instead of an open-ended "Page X". Wrapped so a COUNT failure // never breaks the list itself — the page still loads, just without a total. let total = null; try { const countRows = await exec(`SELECT COUNT(*) AS n FROM ${TABLE} ${w}`, params); const n = Number(countRows?.[0]?.n); total = isNaN(n) ? null : n; } catch (e) { console.warn('[OEE-STORE] COUNT(*) failed — pager will show no total:', e.message); } return { data: rows.map(r => fromRow(r, false)), total }; } async function getById(id) { const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]); return rows.length ? fromRow(rows[0], true) : null; } // Every non-cancelled document (WITH its full row data) across a month // range, for a report that needs to scan actual shift rows rather than just // list documents — e.g. Downtime Analysis, which pulls every shift entry // across every tab/machine for a date range. Unbounded (no top/skip): this // is for server-side aggregation, not a paginated UI list. async function listAllInRange({ company, monthFrom, monthTo } = {}) { const where = [`(CANCELED = 0 OR CANCELED IS NULL)`]; const params = []; if (company) { where.push(`COMPANY = ?`); params.push(company); } if (monthFrom){ where.push(`MONTH >= ?`); params.push(monthFrom); } if (monthTo) { where.push(`MONTH <= ?`); params.push(monthTo); } const rows = await exec(`SELECT * FROM ${TABLE} WHERE ${where.join(' AND ')}`, params); return rows.map(r => fromRow(r, true)); } async function create({ company, tab, month, rows, remark, createdBy, createdById }) { const now = new Date().toISOString().replace('T', ' ').replace('Z', '').substring(0, 23); const idRows = await exec( `INSERT INTO ${TABLE} (COMPANY, TAB, MONTH, ROWS_JSON, REMARK, CANCELED, CREATED_BY, CREATED_BY_ID, CREATED_AT, UPDATED_AT) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?); SELECT SCOPE_IDENTITY() AS ID;`, [company || '', tab, month, JSON.stringify(Array.isArray(rows) ? rows : []), remark || null, createdBy || null, createdById || null, now, now] ); return idRows[0].ID; } async function update(id, { month, rows, remark }) { const sets = []; const vals = []; if (month !== undefined) { sets.push('MONTH = ?'); vals.push(month); } if (rows !== undefined) { sets.push('ROWS_JSON = ?'); vals.push(JSON.stringify(Array.isArray(rows) ? rows : [])); } if (remark !== undefined) { sets.push('REMARK = ?'); vals.push(remark || null); } sets.push('UPDATED_AT = ?'); vals.push(new Date().toISOString().replace('T', ' ').replace('Z', '').substring(0, 23)); vals.push(parseInt(id)); await exec(`UPDATE ${TABLE} SET ${sets.join(', ')} WHERE ID = ?`, vals); } async function cancel(id) { await exec(`UPDATE ${TABLE} SET CANCELED = 1, UPDATED_AT = SYSUTCDATETIME() WHERE ID = ?`, [parseInt(id)]); } module.exports = { bootstrap, list, getById, create, update, cancel, listAllInRange };