Add production execution workflow

This commit is contained in:
Cauê Faleiros
2026-08-25 10:44:39 -03:00
parent 713cc4a9fc
commit a226e6d9f4
8 changed files with 232 additions and 24 deletions

View File

@@ -488,6 +488,14 @@ const initDB = async () => {
ADD COLUMN IF NOT EXISTS completed_date DATE;
`).catch(() => {});
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS actual_cut_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS rejected_quantity NUMERIC(14, 4) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS finished_quantity NUMERIC(14, 4),
ADD COLUMN IF NOT EXISTS quality_notes TEXT;
`).catch(() => {});
await pool.query(`
CREATE TABLE IF NOT EXISTS production_order_import_runs (
id SERIAL PRIMARY KEY,

View File

@@ -1,6 +1,6 @@
const express = require('express');
const { authenticateAPIKey, verifyToken } = require('../auth');
const { createProductionOrders, importTinyProductCompositionExport, listProductionOrders, updateProductionOrderStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
const { createProductionOrders, importTinyProductCompositionExport, listProductionOrders, updateProductionOrderExecution, updateProductionOrderStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
const router = express.Router();
@@ -50,4 +50,13 @@ router.patch('/production-orders/:id/status', verifyToken, async (req, res) => {
}
});
router.patch('/production-orders/:id/execution', verifyToken, async (req, res) => {
try {
res.json(await updateProductionOrderExecution(req.params.id, req.body || {}));
} catch (error) {
console.error('Error updating production order execution:', error);
res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' });
}
});
module.exports = router;

View File

@@ -2,7 +2,9 @@ const { pool } = require('../db');
const STATUS_LABELS = {
open: 'Em aberto',
in_progress: 'Em andamento',
cutting: 'Em corte',
sewing: 'Em costura',
review: 'Em revisão',
finished: 'Finalizada',
canceled: 'Cancelada'
};
@@ -10,7 +12,9 @@ const STATUS_LABELS = {
const normalizeStatus = (status) => {
const normalizedStatus = String(status || 'open').trim().toLowerCase();
if (['open', 'em_aberto', 'em aberto', 'aberta'].includes(normalizedStatus)) return 'open';
if (['in_progress', 'andamento', 'em andamento'].includes(normalizedStatus)) return 'in_progress';
if (['in_progress', 'andamento', 'em andamento', 'em corte', 'corte', 'cutting'].includes(normalizedStatus)) return 'cutting';
if (['em costura', 'costura', 'sewing'].includes(normalizedStatus)) return 'sewing';
if (['em revisao', 'em revisão', 'revisao', 'revisão', 'review'].includes(normalizedStatus)) return 'review';
if (['finished', 'finalizada', 'finalizado'].includes(normalizedStatus)) return 'finished';
if (['canceled', 'cancelada', 'cancelado', 'cancelled'].includes(normalizedStatus)) return 'canceled';
return normalizedStatus || 'open';
@@ -60,6 +64,15 @@ const normalizeNullableQuantity = (value) => {
return quantity || null;
};
const normalizeNonNegativeQuantity = (value) => {
if (value === '' || value === null || value === undefined) return null;
const normalizedValue = typeof value === 'string' && value.includes(',')
? value.replace(/\./g, '').replace(',', '.')
: value;
const quantity = Number(normalizedValue);
return Number.isFinite(quantity) && quantity >= 0 ? quantity : null;
};
const normalizeInteger = (value) => {
const number = Number(value);
return Number.isInteger(number) ? number : null;
@@ -267,6 +280,10 @@ const mapProductionOrderRow = (row) => {
fabricKg: row.fabric_kg === null ? null : Number(row.fabric_kg),
ribKg: row.rib_kg === null ? null : Number(row.rib_kg),
yieldPiecesPerKg: row.yield_pieces_per_kg === null ? null : Number(row.yield_pieces_per_kg),
actualCutQuantity: row.actual_cut_quantity === null ? null : Number(row.actual_cut_quantity),
rejectedQuantity: Number(row.rejected_quantity || 0),
finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity),
qualityNotes: row.quality_notes || '',
markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [],
components: Array.isArray(row.components) ? row.components.map(mapComponent) : [],
steps: Array.isArray(row.steps) ? row.steps.map(mapStep) : [],
@@ -298,6 +315,10 @@ const getOrderById = async (id, client = pool) => {
po.fabric_kg,
po.rib_kg,
po.yield_pieces_per_kg,
po.actual_cut_quantity,
po.rejected_quantity,
po.finished_quantity,
po.quality_notes,
po.created_at,
po.updated_at,
COALESCE(
@@ -381,6 +402,10 @@ const baseProductionOrderSelect = `
po.fabric_kg,
po.rib_kg,
po.yield_pieces_per_kg,
po.actual_cut_quantity,
po.rejected_quantity,
po.finished_quantity,
po.quality_notes,
po.created_at,
po.updated_at,
COALESCE(
@@ -482,7 +507,9 @@ const listProductionOrders = async (filters = {}) => {
}, {
all: 0,
open: 0,
in_progress: 0,
cutting: 0,
sewing: 0,
review: 0,
finished: 0,
canceled: 0
});
@@ -514,7 +541,7 @@ const createProductionOrders = async (orders = []) => {
FROM production_orders
WHERE COALESCE(order_reference, '') = $1
AND COALESCE(product_sku, '') = $2
AND status IN ('open', 'in_progress')
AND status IN ('open', 'in_progress', 'cutting', 'sewing', 'review')
LIMIT 1;
`, [orderReference, productSku]);
@@ -1304,7 +1331,13 @@ const updateProductionOrderStatus = async (id, status) => {
const result = await pool.query(`
UPDATE production_orders
SET status = $1, updated_at = CURRENT_TIMESTAMP
SET status = $1,
completed_date = CASE
WHEN $1 = 'finished' THEN COALESCE(completed_date, CURRENT_DATE)
WHEN status = 'finished' THEN NULL
ELSE completed_date
END,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
RETURNING id;
`, [normalizedStatus, id]);
@@ -1318,6 +1351,58 @@ const updateProductionOrderStatus = async (id, status) => {
return getOrderById(id);
};
const updateProductionOrderExecution = async (id, payload = {}) => {
const actualCutQuantity = normalizeNonNegativeQuantity(payload.actualCutQuantity);
const rejectedQuantity = normalizeNonNegativeQuantity(payload.rejectedQuantity);
const finishedQuantity = normalizeNonNegativeQuantity(payload.finishedQuantity);
const qualityNotes = normalizeText(payload.qualityNotes);
if (actualCutQuantity === null && rejectedQuantity === null && finishedQuantity === null && !qualityNotes) {
const error = new Error('Informe uma quantidade de execução ou uma observação de qualidade.');
error.statusCode = 400;
throw error;
}
const currentResult = await pool.query(`
SELECT actual_cut_quantity, rejected_quantity, finished_quantity
FROM production_orders
WHERE id = $1;
`, [id]);
if (!currentResult.rowCount) {
const error = new Error('Production order not found');
error.statusCode = 404;
throw error;
}
const current = currentResult.rows[0];
const effectiveActual = actualCutQuantity === null ? (current.actual_cut_quantity === null ? null : Number(current.actual_cut_quantity)) : actualCutQuantity;
const effectiveRejected = rejectedQuantity === null ? Number(current.rejected_quantity || 0) : rejectedQuantity;
const effectiveFinished = finishedQuantity === null ? (current.finished_quantity === null ? null : Number(current.finished_quantity)) : finishedQuantity;
if (effectiveActual !== null && effectiveRejected > effectiveActual) {
const error = new Error('As perdas não podem ser maiores que a quantidade cortada.');
error.statusCode = 400;
throw error;
}
if (effectiveActual !== null && effectiveFinished !== null && effectiveFinished > effectiveActual - effectiveRejected) {
const error = new Error('A quantidade final não pode ser maior que o saldo após as perdas.');
error.statusCode = 400;
throw error;
}
await pool.query(`
UPDATE production_orders
SET actual_cut_quantity = $1,
rejected_quantity = $2,
finished_quantity = $3,
quality_notes = NULLIF($4, ''),
updated_at = CURRENT_TIMESTAMP
WHERE id = $5;
`, [effectiveActual, effectiveRejected, effectiveFinished, qualityNotes, id]);
return getOrderById(id);
};
module.exports = {
createProductionOrders,
deleteManualProductComposition,
@@ -1329,5 +1414,6 @@ module.exports = {
saveManualProductComposition,
upsertTinyProductComposition,
upsertTinyProductionOrderDetail,
updateProductionOrderExecution,
updateProductionOrderStatus
};