first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
// services/productionPlanningStore.js
// Daily Production Planning — one row per (Date, Product) plan: how many
// packages × quantity per package are planned, the per-cycle quantity and
// cycles/day that implies, and whether it's actually been done yet.
const sql = require('mssql');
const TABLE = `[dbo].[ZPRODUCTION_PLANNING]`;
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');
}
function nowTs() { return new Date().toISOString().replace('T', ' ').substring(0, 23); }
async function bootstrap() {
console.log('[PRODUCTION-PLANNING] Checking table', TABLE, '...');
await exec(`
CREATE TABLE ${TABLE} (
ID INT IDENTITY(1,1) PRIMARY KEY,
PLAN_DATE NVARCHAR(20) NOT NULL,
PRODUCT_NAME NVARCHAR(200) NOT NULL,
PRODUCT_TYPE NVARCHAR(50),
MARKET NVARCHAR(100),
NO_OF_PACKAGE FLOAT DEFAULT 0,
QUANTITY FLOAT DEFAULT 0,
PROD_AS_PER_SINGLE FLOAT DEFAULT 0,
PER_CYCLE_QTY FLOAT DEFAULT 0,
NO_OF_CYCLE_DAY FLOAT DEFAULT 0,
STATUS NVARCHAR(20) DEFAULT 'PLANNED',
BATCH_NO NVARCHAR(60),
COMPANY NVARCHAR(60),
CREATED_BY NVARCHAR(50),
CREATED_NAME NVARCHAR(100),
CREATED_AT DATETIME2,
UPDATED_BY NVARCHAR(50),
UPDATED_NAME NVARCHAR(100),
UPDATED_AT DATETIME2,
APPROVED_BY NVARCHAR(50),
APPROVED_NAME NVARCHAR(100),
APPROVED_AT DATETIME2,
REJECTED_BY NVARCHAR(50),
REJECTED_NAME NVARCHAR(100),
REJECTED_AT DATETIME2,
REJECT_REASON NVARCHAR(500),
IS_DELETED BIT DEFAULT 0
)
`).catch(e => { if (!isAlreadyExists(e)) throw e; });
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='PRODUCT_TYPE') ALTER TABLE ${TABLE} ADD [PRODUCT_TYPE] NVARCHAR(50)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='MARKET') ALTER TABLE ${TABLE} ADD [MARKET] NVARCHAR(100)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='APPROVED_BY') ALTER TABLE ${TABLE} ADD [APPROVED_BY] NVARCHAR(50)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='APPROVED_NAME') ALTER TABLE ${TABLE} ADD [APPROVED_NAME] NVARCHAR(100)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='APPROVED_AT') ALTER TABLE ${TABLE} ADD [APPROVED_AT] DATETIME2`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='REJECTED_BY') ALTER TABLE ${TABLE} ADD [REJECTED_BY] NVARCHAR(50)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='REJECTED_NAME') ALTER TABLE ${TABLE} ADD [REJECTED_NAME] NVARCHAR(100)`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='REJECTED_AT') ALTER TABLE ${TABLE} ADD [REJECTED_AT] DATETIME2`).catch(() => {});
await exec(`IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='ZPRODUCTION_PLANNING' AND COLUMN_NAME='REJECT_REASON') ALTER TABLE ${TABLE} ADD [REJECT_REASON] NVARCHAR(500)`).catch(() => {});
console.log('[PRODUCTION-PLANNING] ✅ Ready');
}
function fromRow(row) {
return {
id: row.ID,
planDate: row.PLAN_DATE || '',
productName: row.PRODUCT_NAME || '',
productType: row.PRODUCT_TYPE || '',
market: row.MARKET || '',
noOfPackage: row.NO_OF_PACKAGE || 0,
quantity: row.QUANTITY || 0,
prodAsPerSingle: row.PROD_AS_PER_SINGLE || 0,
perCycleQty: row.PER_CYCLE_QTY || 0,
noOfCycleDay: row.NO_OF_CYCLE_DAY || 0,
status: row.STATUS || 'PLANNED',
batchNo: row.BATCH_NO || '',
company: row.COMPANY || '',
createdBy: row.CREATED_BY || '',
createdByName: row.CREATED_NAME || '',
createdAt: row.CREATED_AT ? new Date(row.CREATED_AT).toISOString() : null,
updatedBy: row.UPDATED_BY || '',
updatedByName: row.UPDATED_NAME || '',
updatedAt: row.UPDATED_AT ? new Date(row.UPDATED_AT).toISOString() : null,
approvedBy: row.APPROVED_BY || '',
approvedByName: row.APPROVED_NAME || '',
approvedAt: row.APPROVED_AT ? new Date(row.APPROVED_AT).toISOString() : null,
rejectedBy: row.REJECTED_BY || '',
rejectedByName: row.REJECTED_NAME || '',
rejectedAt: row.REJECTED_AT ? new Date(row.REJECTED_AT).toISOString() : null,
rejectReason: row.REJECT_REASON || '',
isDeleted: !!row.IS_DELETED,
};
}
async function listPlans({ company, from, to, status, productType, market, q, includeDeleted } = {}) {
const rows = await exec(`SELECT * FROM ${TABLE} ORDER BY PLAN_DATE DESC, ID DESC`);
const qLower = q ? String(q).trim().toLowerCase() : '';
return rows.map(fromRow).filter(p => {
if (!includeDeleted && p.isDeleted) return false;
if (company && p.company && p.company !== company) return false;
if (status && status !== 'ALL' && p.status !== status.toUpperCase()) return false;
if (from && p.planDate && p.planDate < from) return false;
if (to && p.planDate && p.planDate > to) return false;
if (productType && productType !== 'ALL' && p.productType !== productType) return false;
if (market && p.market !== market) return false;
if (qLower && !(p.productName || '').toLowerCase().includes(qLower) && !(p.batchNo || '').toLowerCase().includes(qLower)) return false;
return true;
});
}
async function findById(id) {
const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID=?`, [parseInt(id)]);
return rows.length ? fromRow(rows[0]) : null;
}
// "Prod. As per Single" is always No. of Package × Quantity — computed
// server-side so a client can never submit a mismatched figure.
function computeProdAsPerSingle(noOfPackage, quantity) {
return (parseFloat(noOfPackage) || 0) * (parseFloat(quantity) || 0);
}
// Quantity may never exceed the day's capacity (Per Cycle Qty × No. of Cycle/Day).
function validateQuantity(quantity, perCycleQty, noOfCycleDay) {
const totalPerDay = (parseFloat(perCycleQty) || 0) * (parseFloat(noOfCycleDay) || 0);
const qty = parseFloat(quantity) || 0;
if (totalPerDay > 0 && qty > totalPerDay) {
throw new Error(`Quantity (${qty}) cannot exceed Total Qty/Day (${totalPerDay})`);
}
}
async function createPlan(p) {
if (!p.planDate) throw new Error('Date is required');
if (!p.productName || !String(p.productName).trim()) throw new Error('Product Name is required');
validateQuantity(p.quantity, p.perCycleQty, p.noOfCycleDay);
const prodAsPerSingle = computeProdAsPerSingle(p.noOfPackage, p.quantity);
const now = nowTs();
const idRows = await exec(`
INSERT INTO ${TABLE} (
PLAN_DATE, PRODUCT_NAME, PRODUCT_TYPE, MARKET, NO_OF_PACKAGE, QUANTITY, PROD_AS_PER_SINGLE,
PER_CYCLE_QTY, NO_OF_CYCLE_DAY, STATUS, BATCH_NO, COMPANY,
CREATED_BY, CREATED_NAME, CREATED_AT
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
SELECT SCOPE_IDENTITY() AS ID;
`, [
p.planDate, String(p.productName).trim(), p.productType || '', p.market || '', parseFloat(p.noOfPackage) || 0, parseFloat(p.quantity) || 0, prodAsPerSingle,
parseFloat(p.perCycleQty) || 0, parseFloat(p.noOfCycleDay) || 0, (p.status === 'DONE' ? 'DONE' : 'PLANNED'), p.batchNo || '', p.company || '',
p.createdBy || '', p.createdByName || p.createdBy || '', now,
]);
return findById(idRows[0].ID);
}
async function updatePlan(id, p, { by, byName } = {}) {
const existing = await findById(id);
if (!existing) throw new Error('Plan not found');
// An approved plan is locked — no silent re-editing that would leave an
// old sign-off attached to changed numbers (or quietly invalidate it
// without the approver noticing). It must be rejected first, which
// reopens it for editing; editing a REJECTED plan is exactly how that
// rejection gets resolved, clearing it back to "pending review" so the
// approver sees it again.
if (existing.approvedBy) throw new Error('This plan is approved and locked. It must be rejected before it can be edited again.');
const noOfPackage = p.noOfPackage !== undefined ? p.noOfPackage : existing.noOfPackage;
const quantity = p.quantity !== undefined ? p.quantity : existing.quantity;
const perCycleQty = p.perCycleQty !== undefined ? p.perCycleQty : existing.perCycleQty;
const noOfCycleDay = p.noOfCycleDay !== undefined ? p.noOfCycleDay : existing.noOfCycleDay;
validateQuantity(quantity, perCycleQty, noOfCycleDay);
const prodAsPerSingle = computeProdAsPerSingle(noOfPackage, quantity);
await exec(`
UPDATE ${TABLE} SET
PLAN_DATE=?, PRODUCT_NAME=?, PRODUCT_TYPE=?, MARKET=?, NO_OF_PACKAGE=?, QUANTITY=?, PROD_AS_PER_SINGLE=?,
PER_CYCLE_QTY=?, NO_OF_CYCLE_DAY=?, STATUS=?, BATCH_NO=?,
UPDATED_BY=?, UPDATED_NAME=?, UPDATED_AT=?,
REJECTED_BY=NULL, REJECTED_NAME=NULL, REJECTED_AT=NULL, REJECT_REASON=NULL
WHERE ID=?
`, [
p.planDate !== undefined ? p.planDate : existing.planDate,
p.productName !== undefined ? String(p.productName).trim() : existing.productName,
p.productType !== undefined ? p.productType : existing.productType,
p.market !== undefined ? p.market : existing.market,
parseFloat(noOfPackage) || 0, parseFloat(quantity) || 0, prodAsPerSingle,
p.perCycleQty !== undefined ? (parseFloat(p.perCycleQty) || 0) : existing.perCycleQty,
p.noOfCycleDay !== undefined ? (parseFloat(p.noOfCycleDay) || 0) : existing.noOfCycleDay,
p.status !== undefined ? (p.status === 'DONE' ? 'DONE' : 'PLANNED') : existing.status,
p.batchNo !== undefined ? p.batchNo : existing.batchNo,
by || '', byName || by || '', nowTs(),
parseInt(id),
]);
return findById(id);
}
async function approvePlan(id, { by, byName } = {}) {
const existing = await findById(id);
if (!existing) throw new Error('Plan not found');
await exec(`
UPDATE ${TABLE} SET APPROVED_BY=?, APPROVED_NAME=?, APPROVED_AT=?,
REJECTED_BY=NULL, REJECTED_NAME=NULL, REJECTED_AT=NULL, REJECT_REASON=NULL
WHERE ID=?
`, [
by || '', byName || by || '', nowTs(), parseInt(id),
]);
return findById(id);
}
async function rejectPlan(id, reason, { by, byName } = {}) {
const existing = await findById(id);
if (!existing) throw new Error('Plan not found');
if (!reason || !String(reason).trim()) throw new Error('A reason is required to reject a plan');
await exec(`
UPDATE ${TABLE} SET REJECTED_BY=?, REJECTED_NAME=?, REJECTED_AT=?, REJECT_REASON=?,
APPROVED_BY=NULL, APPROVED_NAME=NULL, APPROVED_AT=NULL
WHERE ID=?
`, [
by || '', byName || by || '', nowTs(), String(reason).trim(), parseInt(id),
]);
return findById(id);
}
// Marking a plan Planned/Done is an operational progress flag, not a
// content edit — it must stay changeable even on an approved (locked)
// plan, otherwise nobody could ever record that an approved plan actually
// ran. Deliberately does NOT touch APPROVED_BY/REJECTED_BY — the approval
// itself still stands.
async function updateStatus(id, status, { by, byName } = {}) {
const existing = await findById(id);
if (!existing) throw new Error('Plan not found');
const s = status === 'DONE' ? 'DONE' : 'PLANNED';
await exec(`UPDATE ${TABLE} SET STATUS=?, UPDATED_BY=?, UPDATED_NAME=?, UPDATED_AT=? WHERE ID=?`, [
s, by || '', byName || by || '', nowTs(), parseInt(id),
]);
return findById(id);
}
async function softDeletePlan(id) {
const existing = await findById(id);
if (!existing) throw new Error('Plan not found');
if (existing.approvedBy) throw new Error('This plan is approved and locked. It must be rejected before it can be deleted.');
await exec(`UPDATE ${TABLE} SET IS_DELETED=1 WHERE ID=?`, [parseInt(id)]);
}
module.exports = {
bootstrap, listPlans, findById, createPlan, updatePlan, approvePlan, rejectPlan, updateStatus, softDeletePlan,
};