Persist cutting rules in database
This commit is contained in:
@@ -122,6 +122,24 @@ const initDB = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS cutting_family_rules (
|
||||
family_key VARCHAR(20) PRIMARY KEY,
|
||||
units_per_roll NUMERIC(14, 4),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS cutting_product_overrides (
|
||||
product_id VARCHAR(100) PRIMARY KEY,
|
||||
family_key VARCHAR(20),
|
||||
color VARCHAR(100),
|
||||
size VARCHAR(40),
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE production_orders
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
@@ -130,6 +148,18 @@ const initDB = async () => {
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE cutting_family_rules
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE cutting_product_overrides
|
||||
ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS app_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -197,6 +227,7 @@ const initDB = async () => {
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_issue_date ON production_orders (issue_date DESC);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_orders_expected_date ON production_orders (expected_date DESC);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_markers_order_id ON production_order_markers (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cutting_product_overrides_family_key ON cutting_product_overrides (family_key);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`);
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_normalized_cliente_nome
|
||||
|
||||
23
backend/routes/cuttingSettingsRoutes.js
Normal file
23
backend/routes/cuttingSettingsRoutes.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const express = require('express');
|
||||
const { verifyToken } = require('../auth');
|
||||
const { listCuttingSettings, saveCuttingSettings } = require('../services/cuttingSettingsService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/cutting-settings', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await listCuttingSettings());
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/cutting-settings', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json(await saveCuttingSettings(req.body || {}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -9,6 +9,7 @@ const internalRoutes = require('./routes/internalRoutes');
|
||||
const analyticsRoutes = require('./routes/analyticsRoutes');
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const productionOrderRoutes = require('./routes/productionOrderRoutes');
|
||||
const cuttingSettingsRoutes = require('./routes/cuttingSettingsRoutes');
|
||||
|
||||
const createApp = () => {
|
||||
const app = express();
|
||||
@@ -21,6 +22,7 @@ const createApp = () => {
|
||||
app.use('/api', stockRoutes);
|
||||
app.use('/api', campaignRoutes);
|
||||
app.use('/api', productionOrderRoutes);
|
||||
app.use('/api', cuttingSettingsRoutes);
|
||||
app.use('/api', analyticsRoutes);
|
||||
app.use('/api', userRoutes);
|
||||
app.use('/api/internal', internalRoutes);
|
||||
|
||||
128
backend/services/cuttingSettingsService.js
Normal file
128
backend/services/cuttingSettingsService.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const { pool } = require('../db');
|
||||
|
||||
const FAMILY_KEYS = ['BLCS', 'BLOS', 'BLMC', 'BLPM'];
|
||||
const FAMILY_KEY_SET = new Set(FAMILY_KEYS);
|
||||
|
||||
const normalizeFamilyKey = (value) => {
|
||||
const familyKey = String(value || '').trim().toUpperCase();
|
||||
return FAMILY_KEY_SET.has(familyKey) ? familyKey : '';
|
||||
};
|
||||
|
||||
const normalizeNumber = (value) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? number : null;
|
||||
};
|
||||
|
||||
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
const normalizeFamilyYields = (familyYields = {}) => {
|
||||
return FAMILY_KEYS.reduce((normalized, familyKey) => {
|
||||
const unitsPerRoll = normalizeNumber(familyYields[familyKey]);
|
||||
if (unitsPerRoll) normalized[familyKey] = unitsPerRoll;
|
||||
return normalized;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const normalizeProductOverrides = (productOverrides = {}) => {
|
||||
return Object.entries(productOverrides).reduce((normalized, [productId, override]) => {
|
||||
const normalizedProductId = normalizeText(productId);
|
||||
if (!normalizedProductId || !override || typeof override !== 'object') return normalized;
|
||||
|
||||
const familyKey = normalizeFamilyKey(override.familyKey);
|
||||
const color = normalizeText(override.color);
|
||||
const size = normalizeText(override.size).toUpperCase();
|
||||
|
||||
if (!familyKey && !color && !size) return normalized;
|
||||
|
||||
normalized[normalizedProductId] = {
|
||||
familyKey,
|
||||
color,
|
||||
size
|
||||
};
|
||||
|
||||
return normalized;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const listCuttingSettings = async () => {
|
||||
const [familyResult, overrideResult] = await Promise.all([
|
||||
pool.query(`
|
||||
SELECT family_key, units_per_roll
|
||||
FROM cutting_family_rules
|
||||
WHERE units_per_roll IS NOT NULL AND units_per_roll > 0
|
||||
ORDER BY family_key
|
||||
`),
|
||||
pool.query(`
|
||||
SELECT product_id, family_key, color, size
|
||||
FROM cutting_product_overrides
|
||||
ORDER BY product_id
|
||||
`)
|
||||
]);
|
||||
|
||||
return {
|
||||
familyYields: familyResult.rows.reduce((settings, row) => {
|
||||
settings[row.family_key] = Number(row.units_per_roll);
|
||||
return settings;
|
||||
}, {}),
|
||||
productOverrides: overrideResult.rows.reduce((settings, row) => {
|
||||
settings[row.product_id] = {
|
||||
familyKey: row.family_key || '',
|
||||
color: row.color || '',
|
||||
size: row.size || ''
|
||||
};
|
||||
return settings;
|
||||
}, {})
|
||||
};
|
||||
};
|
||||
|
||||
const saveCuttingSettings = async ({ familyYields = {}, productOverrides = {} }) => {
|
||||
const normalizedFamilyYields = normalizeFamilyYields(familyYields);
|
||||
const normalizedProductOverrides = normalizeProductOverrides(productOverrides);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
await client.query('DELETE FROM cutting_family_rules');
|
||||
for (const [familyKey, unitsPerRoll] of Object.entries(normalizedFamilyYields)) {
|
||||
await client.query(`
|
||||
INSERT INTO cutting_family_rules (family_key, units_per_roll, updated_at)
|
||||
VALUES ($1, $2, CURRENT_TIMESTAMP)
|
||||
`, [familyKey, unitsPerRoll]);
|
||||
}
|
||||
|
||||
await client.query('DELETE FROM cutting_product_overrides');
|
||||
for (const [productId, override] of Object.entries(normalizedProductOverrides)) {
|
||||
await client.query(`
|
||||
INSERT INTO cutting_product_overrides (product_id, family_key, color, size, updated_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
`, [
|
||||
productId,
|
||||
override.familyKey || null,
|
||||
override.color || null,
|
||||
override.size || null
|
||||
]);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return {
|
||||
familyYields: normalizedFamilyYields,
|
||||
productOverrides: normalizedProductOverrides
|
||||
};
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
FAMILY_KEYS,
|
||||
listCuttingSettings,
|
||||
normalizeFamilyKey,
|
||||
normalizeFamilyYields,
|
||||
normalizeProductOverrides,
|
||||
saveCuttingSettings
|
||||
};
|
||||
40
backend/test/cuttingSettingsService.test.js
Normal file
40
backend/test/cuttingSettingsService.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
normalizeFamilyKey,
|
||||
normalizeFamilyYields,
|
||||
normalizeProductOverrides
|
||||
} = require('../services/cuttingSettingsService');
|
||||
|
||||
test('normalizeFamilyKey accepts only known cut families', () => {
|
||||
assert.equal(normalizeFamilyKey('blcs'), 'BLCS');
|
||||
assert.equal(normalizeFamilyKey(' BLOS '), 'BLOS');
|
||||
assert.equal(normalizeFamilyKey('OUTROS'), '');
|
||||
assert.equal(normalizeFamilyKey('unknown'), '');
|
||||
});
|
||||
|
||||
test('normalizeFamilyYields keeps positive numeric yield rules', () => {
|
||||
assert.deepEqual(normalizeFamilyYields({
|
||||
BLCS: '50',
|
||||
BLOS: 0,
|
||||
BLMC: -1,
|
||||
BLPM: '12.5',
|
||||
OUTROS: 99
|
||||
}), {
|
||||
BLCS: 50,
|
||||
BLPM: 12.5
|
||||
});
|
||||
});
|
||||
|
||||
test('normalizeProductOverrides trims and removes empty overrides', () => {
|
||||
assert.deepEqual(normalizeProductOverrides({
|
||||
' SKU-1 ': { familyKey: 'blcs', color: ' Preto ', size: ' m ' },
|
||||
'SKU-2': { familyKey: 'OUTROS', color: '', size: '' },
|
||||
'SKU-3': { familyKey: '', color: ' Branco ', size: '' },
|
||||
'SKU-4': null
|
||||
}), {
|
||||
'SKU-1': { familyKey: 'BLCS', color: 'Preto', size: 'M' },
|
||||
'SKU-3': { familyKey: '', color: 'Branco', size: '' }
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user