sale order
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s

This commit is contained in:
John
2026-10-05 18:45:17 +05:30
parent e725a6571b
commit eead8f5ffd
129 changed files with 14252 additions and 452 deletions
+290
View File
@@ -0,0 +1,290 @@
'use strict';
// services/downtimeAnalysisStore.js — "Downtime Analysis" report: aggregates
// raw OEE shift entries (services/oeeStore.js) across EVERY tab, into one
// standardized downtime taxonomy (Admin → System Settings → "Downtime
// Analysis" — see appSettingsStore.downtimeAnalysisConfig()). Different OEE
// tabs use different field names for conceptually the same cause (e.g.
// Sheet's "Head Clean" vs EBB's "Micro Stops") — tabFieldMap is what
// reconciles that, so a machine logged under one tab still rolls up next to
// a machine logged under another.
//
// Three views over the same underlying data:
// - buildReport() — grouped by the shift's own Machine Name field.
// - buildTabReport() — grouped by OEE FORM (tab) instead — "how much
// downtime did EBB Production log overall", not
// "how much did this one machine log".
// - buildMonthlyReport() — grouped by month, every machine/tab combined into
// one Down Time vs M/C Run Time total.
//
// "Base Days" is a company working-day calendar count for the whole report
// period (e.g. 330 days for a ~13-month period) — admin-entered, not derived
// from OEE data, since it reflects the plant's real working calendar
// (weekly-offs/holidays), not anything OEE rows can tell us. A group (machine
// or tab) that only started being logged partway through the period (its own
// first OEE entry falls after periodFrom) gets this prorated down
// automatically — same daily "working-day density" applied to a shorter
// span, not the full period's Base Days.
const appSettings = require('./appSettingsStore');
const oeeStore = require('./oeeStore');
function daysBetween(fromStr, toStr) {
const a = new Date(fromStr + 'T00:00:00Z');
const b = new Date(toStr + 'T00:00:00Z');
return Math.round((b - a) / 86400000);
}
function resolvePeriod({ periodFrom, periodTo, baseDays }, cfg) {
const from = (periodFrom || cfg.periodFrom || '').slice(0, 10);
const to = (periodTo || cfg.periodTo || '').slice(0, 10);
if (!from || !to) throw new Error('periodFrom and periodTo are required (set a default in Admin → System Settings → Downtime Analysis, or pass them explicitly).');
const baseDaysCommon = Number(baseDays != null ? baseDays : cfg.baseDays) || 0;
return { from, to, baseDaysCommon };
}
// Shared aggregation core — buckets every in-range shift row by whatever
// `keyOf(row, doc)` returns (a machine name, or a tab label), sums its
// mapped cause minutes + shift minutes into that bucket, then turns each
// bucket into the same derived-metrics shape (Base Days, % by cause, Net/
// Total/Remainder %) buildReport()/buildTabReport() both return.
async function aggregate({ company, from, to, baseDaysCommon, causes, tabFieldMap, keyOf }) {
const monthFrom = from.slice(0, 7);
const monthTo = to.slice(0, 7);
const docs = await oeeStore.listAllInRange({ company, monthFrom, monthTo });
const totalPeriodDays = Math.max(1, daysBetween(from, to) + 1);
const buckets = {}; // key -> { causeTotals, totalShiftMin, activeDates:Set, firstDate }
docs.forEach(doc => {
const map = tabFieldMap[doc.tab] || {};
(doc.rows || []).forEach(row => {
const d = String(row.date || '').slice(0, 10);
if (!d || d < from || d > to) return;
const key = keyOf(row, doc);
if (!key) return;
if (!buckets[key]) buckets[key] = { causeTotals: {}, totalShiftMin: 0, activeDates: new Set(), firstDate: null };
const b = buckets[key];
const shiftHrs = parseFloat(row.shiftLenHrs) || 0;
b.totalShiftMin += shiftHrs * 60;
b.activeDates.add(d);
if (!b.firstDate || d < b.firstDate) b.firstDate = d;
Object.keys(map).forEach(field => {
const cause = map[field];
if (!cause) return;
const v = parseFloat(row[field]) || 0;
b.causeTotals[cause] = (b.causeTotals[cause] || 0) + v;
});
});
});
const keys = Object.keys(buckets).sort();
const perGroup = keys.map(key => {
const b = buckets[key];
let bDays = baseDaysCommon;
if (b.firstDate && b.firstDate > from) {
const daysSinceStart = Math.max(1, daysBetween(b.firstDate, to) + 1);
bDays = Math.round((daysSinceStart / totalPeriodDays) * baseDaysCommon);
}
const baseMin = bDays * 1440;
const totalShiftMin = Math.round(b.totalShiftMin);
const plantInoperationMin = Math.max(0, baseMin - totalShiftMin);
const causeTotals = {};
causes.forEach(c => { causeTotals[c] = Math.round(b.causeTotals[c] || 0); });
const causePcts = {};
causes.forEach(c => { causePcts[c] = baseMin > 0 ? (causeTotals[c] / baseMin * 100) : 0; });
const netDowntimeMin = causes.reduce((s, c) => s + causeTotals[c], 0);
const netDowntimePct = baseMin > 0 ? (netDowntimeMin / baseMin * 100) : 0;
const plantInoperationPct = baseMin > 0 ? (plantInoperationMin / baseMin * 100) : 0;
const totalPct = netDowntimePct + plantInoperationPct;
return {
group: key, baseDays: bDays, baseMin,
actualActiveDays: b.activeDates.size, totalShiftMin,
plantInoperationMin, plantInoperationPct,
causeTotals, causePcts, netDowntimeMin, netDowntimePct,
totalPct, remainderPct: 100 - totalPct,
firstActiveDate: b.firstDate,
};
});
const net = {
group: 'NET',
baseDays: perGroup.reduce((s, g) => s + g.baseDays, 0),
baseMin: perGroup.reduce((s, g) => s + g.baseMin, 0),
actualActiveDays: perGroup.reduce((s, g) => s + g.actualActiveDays, 0),
totalShiftMin: perGroup.reduce((s, g) => s + g.totalShiftMin, 0),
plantInoperationMin: perGroup.reduce((s, g) => s + g.plantInoperationMin, 0),
};
const netCauseTotals = {};
causes.forEach(c => { netCauseTotals[c] = perGroup.reduce((s, g) => s + (g.causeTotals[c] || 0), 0); });
net.causeTotals = netCauseTotals;
const netCausePcts = {};
causes.forEach(c => { netCausePcts[c] = net.baseMin > 0 ? (netCauseTotals[c] / net.baseMin * 100) : 0; });
net.causePcts = netCausePcts;
net.netDowntimeMin = causes.reduce((s, c) => s + netCauseTotals[c], 0);
net.netDowntimePct = net.baseMin > 0 ? (net.netDowntimeMin / net.baseMin * 100) : 0;
net.plantInoperationPct = net.baseMin > 0 ? (net.plantInoperationMin / net.baseMin * 100) : 0;
net.totalPct = net.netDowntimePct + net.plantInoperationPct;
net.remainderPct = 100 - net.totalPct;
return { perGroup, net };
}
async function buildReport({ company, periodFrom, periodTo, baseDays } = {}) {
const cfg = appSettings.downtimeAnalysisConfig();
const { from, to, baseDaysCommon } = resolvePeriod({ periodFrom, periodTo, baseDays }, cfg);
const causes = Array.isArray(cfg.causes) ? cfg.causes.filter(Boolean) : [];
const tabFieldMap = cfg.tabFieldMap || {};
const { perGroup, net } = await aggregate({
company, from, to, baseDaysCommon, causes, tabFieldMap,
keyOf: row => String(row.machineName || '').trim() || null,
});
const machines = perGroup.map(g => ({ ...g, machine: g.group }));
return { periodFrom: from, periodTo: to, baseDaysCommon, causes, machines, net: { ...net, machine: 'NET' } };
}
const TAB_LABELS = { ebb: 'EBB Production', pd: 'PD Production', sheet: 'Plastic Sheet Plant', moulding: 'Moulding', autoclave: 'Autoclave' };
// Same shape as buildReport(), grouped by OEE FORM (tab) instead of Machine
// Name — "how much downtime did EBB Production log overall" rather than
// "how much did this one machine log". A tab with no rows in range is simply
// absent (not shown as a zero row).
async function buildTabReport({ company, periodFrom, periodTo, baseDays } = {}) {
const cfg = appSettings.downtimeAnalysisConfig();
const { from, to, baseDaysCommon } = resolvePeriod({ periodFrom, periodTo, baseDays }, cfg);
const causes = Array.isArray(cfg.causes) ? cfg.causes.filter(Boolean) : [];
const tabFieldMap = cfg.tabFieldMap || {};
const { perGroup, net } = await aggregate({
company, from, to, baseDaysCommon, causes, tabFieldMap,
keyOf: (row, doc) => doc.tab || null,
});
const tabs = perGroup.map(g => ({ ...g, tab: g.group, tabLabel: TAB_LABELS[g.group] || g.group }));
return { periodFrom: from, periodTo: to, baseDaysCommon, causes, tabs, net: { ...net, tab: 'NET', tabLabel: 'NET' } };
}
// Shift-level detail, one sheet's worth per OEE tab — every individual shift
// row (date/shift/shift length/cause minutes/computed Total Down Time & %),
// NOT aggregated by machine or summed into one number, matching the OEE
// module's own per-shift sheet layout. A tab absent from the result had no
// rows in range at all.
// - Total Down Time (per row) = sum of every cause column mapped for that
// tab (both "Availability" and "Performance" causes together).
// - % Of Down Time (per row) = Total Down Time ÷ that row's own Shift
// Length (Equ. Min) × 100.
// - The TOTAL row's % column is the SUM of every row's own % (not a
// recomputed ratio from the totals) — matches the source Master Sheet's
// own convention.
async function buildDetailByTab({ company, periodFrom, periodTo } = {}) {
const cfg = appSettings.downtimeAnalysisConfig();
const from = (periodFrom || cfg.periodFrom || '').slice(0, 10);
const to = (periodTo || cfg.periodTo || '').slice(0, 10);
if (!from || !to) throw new Error('periodFrom and periodTo are required.');
const causes = Array.isArray(cfg.causes) ? cfg.causes.filter(Boolean) : [];
const tabFieldMap = cfg.tabFieldMap || {};
const monthFrom = from.slice(0, 7);
const monthTo = to.slice(0, 7);
const docs = await oeeStore.listAllInRange({ company, monthFrom, monthTo });
const byTab = {}; // tab -> { rows:[], causesUsed:Set }
docs.forEach(doc => {
const map = tabFieldMap[doc.tab] || {};
const mappedFields = Object.keys(map).filter(f => map[f]);
if (!mappedFields.length) return; // tab has no cause mapping at all (e.g. Autoclave by default) — nothing meaningful to show
(doc.rows || []).forEach(row => {
const d = String(row.date || '').slice(0, 10);
if (!d || d < from || d > to) return;
if (!byTab[doc.tab]) byTab[doc.tab] = [];
const equMin = (parseFloat(row.shiftLenHrs) || 0) * 60;
const causeMins = {};
let totalDownMin = 0;
mappedFields.forEach(field => {
const v = parseFloat(row[field]) || 0;
const cause = map[field];
causeMins[cause] = (causeMins[cause] || 0) + v;
totalDownMin += v;
});
byTab[doc.tab].push({
date: d, shift: row.shift || '', shiftLenHrs: parseFloat(row.shiftLenHrs) || 0,
equMin: Math.round(equMin), goldStd: parseFloat(row.goldStd) || 0,
causeMins, totalDownMin: Math.round(totalDownMin),
pctDown: equMin > 0 ? (totalDownMin / equMin * 100) : 0,
});
});
});
const tabs = {};
Object.keys(byTab).forEach(tab => {
const rows = byTab[tab].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
const totals = {
equMin: rows.reduce((s, r) => s + r.equMin, 0),
totalDownMin: rows.reduce((s, r) => s + r.totalDownMin, 0),
pctDownSum: rows.reduce((s, r) => s + r.pctDown, 0),
};
tabs[tab] = { tabLabel: TAB_LABELS[tab] || tab, rows, totals };
});
return { periodFrom: from, periodTo: to, causes, tabs };
}
function monthLabel(ym) {
const [y, m] = ym.split('-').map(Number);
return new Date(Date.UTC(y, m - 1, 1)).toLocaleDateString('en-US', { month: 'short', year: '2-digit', timeZone: 'UTC' });
}
// Month-wise summary — ALL machines/tabs combined into one Down Time total
// and one M/C Run Time total per month (not split by machine or cause; see
// buildReport() above for the per-machine/per-cause breakdown). % of Down
// Time here is Down Time ÷ M/C Run Time (actual logged shift time), NOT ÷
// Base Days×1440 like the per-machine report — this is "of the time the
// machines actually ran, how much was downtime", a different question than
// "of the full working calendar, how much was downtime".
async function buildMonthlyReport({ company, periodFrom, periodTo } = {}) {
const cfg = appSettings.downtimeAnalysisConfig();
const from = (periodFrom || cfg.periodFrom || '').slice(0, 10);
const to = (periodTo || cfg.periodTo || '').slice(0, 10);
if (!from || !to) throw new Error('periodFrom and periodTo are required.');
const tabFieldMap = cfg.tabFieldMap || {};
const monthFrom = from.slice(0, 7);
const monthTo = to.slice(0, 7);
const docs = await oeeStore.listAllInRange({ company, monthFrom, monthTo });
const months = {}; // 'YYYY-MM' -> { downMin, runMin }
docs.forEach(doc => {
const map = tabFieldMap[doc.tab] || {};
(doc.rows || []).forEach(row => {
const d = String(row.date || '').slice(0, 10);
if (!d || d < from || d > to) return;
const ym = d.slice(0, 7);
if (!months[ym]) months[ym] = { downMin: 0, runMin: 0 };
months[ym].runMin += (parseFloat(row.shiftLenHrs) || 0) * 60;
Object.keys(map).forEach(field => {
if (!map[field]) return;
months[ym].downMin += parseFloat(row[field]) || 0;
});
});
});
const rows = Object.keys(months).sort().map((ym, i) => {
const d = months[ym];
const downMin = Math.round(d.downMin);
const runMin = Math.round(d.runMin);
const netRunMin = Math.max(0, runMin - downMin);
return {
no: i + 1, month: ym, monthLabel: monthLabel(ym),
downMin, runMin, netRunMin,
pctDown: runMin > 0 ? (downMin / runMin * 100) : 0,
};
});
const total = {
downMin: rows.reduce((s, r) => s + r.downMin, 0),
runMin: rows.reduce((s, r) => s + r.runMin, 0),
};
total.netRunMin = Math.max(0, total.runMin - total.downMin);
total.pctDown = total.runMin > 0 ? (total.downMin / total.runMin * 100) : 0;
return { periodFrom: from, periodTo: to, rows, total };
}
module.exports = { buildReport, buildTabReport, buildDetailByTab, buildMonthlyReport };