From 5370dffce27f7032e1753147b2c5afec5fc89ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 25 Aug 2026 11:58:43 -0300 Subject: [PATCH] Add production defect and workstation foundation --- backend/db.js | 37 +++++++++ backend/routes/productionOrderRoutes.js | 18 ++++- backend/services/productionOrderService.js | 93 +++++++++++++++++++++- 3 files changed, 145 insertions(+), 3 deletions(-) diff --git a/backend/db.js b/backend/db.js index 65ddbdf..595743c 100644 --- a/backend/db.js +++ b/backend/db.js @@ -236,6 +236,43 @@ const initDB = async () => { ); `); + await pool.query(` + CREATE TABLE IF NOT EXISTS production_workstations ( + id SERIAL PRIMARY KEY, + code VARCHAR(60) NOT NULL UNIQUE, + name VARCHAR(160) NOT NULL, + color VARCHAR(40) NOT NULL DEFAULT '#38bdf8', + sort_order INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ); + `); + await pool.query(` + INSERT INTO production_workstations (code, name, color, sort_order) + VALUES + ('cutting', 'Corte', '#fbbf24', 10), + ('sewing', 'Costura', '#38bdf8', 20), + ('review', 'Revisão', '#a78bfa', 30), + ('sac', 'SAC', '#fb7185', 40) + ON CONFLICT (code) DO NOTHING; + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS production_order_defects ( + id SERIAL PRIMARY KEY, + production_order_id INTEGER NOT NULL REFERENCES production_orders(id) ON DELETE CASCADE, + workstation_id INTEGER REFERENCES production_workstations(id) ON DELETE SET NULL, + defect_type VARCHAR(160) NOT NULL, + origin VARCHAR(160), + quantity NUMERIC(14, 4) NOT NULL CHECK (quantity > 0), + lot_code VARCHAR(120), + notes TEXT, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + await pool.query(` CREATE TABLE IF NOT EXISTS product_compositions ( id SERIAL PRIMARY KEY, diff --git a/backend/routes/productionOrderRoutes.js b/backend/routes/productionOrderRoutes.js index aeb7ca3..a03b5b1 100644 --- a/backend/routes/productionOrderRoutes.js +++ b/backend/routes/productionOrderRoutes.js @@ -1,6 +1,6 @@ const express = require('express'); const { authenticateAPIKey, verifyToken } = require('../auth'); -const { createProductionOrders, importTinyProductCompositionExport, listProductionOrders, listProductionPlans, updateProductionOrderExecution, updateProductionOrderStatus, updateProductionPlan, updateProductionPlanStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService'); +const { addProductionOrderDefect, createProductionOrders, importTinyProductCompositionExport, listProductionOrders, listProductionPlans, listProductionWorkstations, updateProductionOrderExecution, updateProductionOrderStatus, updateProductionPlan, updateProductionPlanStatus, upsertTinyProductionOrderDetail } = require('../services/productionOrderService'); const router = express.Router(); @@ -33,6 +33,14 @@ router.get('/production-plans', verifyToken, async (req, res) => { } }); +router.get('/production-workstations', verifyToken, async (req, res) => { + try { + res.json(await listProductionWorkstations()); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + router.put('/production-plans/:id', verifyToken, async (req, res) => { try { res.json(await updateProductionPlan(req.params.id, req.body || {})); @@ -87,4 +95,12 @@ router.patch('/production-orders/:id/execution', verifyToken, async (req, res) = } }); +router.post('/production-orders/:id/defects', verifyToken, async (req, res) => { + try { + res.status(201).json(await addProductionOrderDefect(req.params.id, req.body || {})); + } catch (error) { + res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' }); + } +}); + module.exports = router; diff --git a/backend/services/productionOrderService.js b/backend/services/productionOrderService.js index d198124..15c2a4c 100644 --- a/backend/services/productionOrderService.js +++ b/backend/services/productionOrderService.js @@ -267,6 +267,18 @@ const mapLotReservation = (reservation) => ({ lotSupplier: reservation.lot_supplier || '' }); +const mapDefect = (defect) => ({ + id: Number(defect.id), + workstationId: defect.workstation_id === null ? null : Number(defect.workstation_id), + workstationName: defect.workstation_name || '', + defectType: defect.defect_type || '', + origin: defect.origin || '', + quantity: Number(defect.quantity || 0), + lotCode: defect.lot_code || '', + notes: defect.notes || '', + occurredAt: defect.occurred_at || null +}); + const mapProductionOrderRow = (row) => { const status = normalizeStatus(row.status); @@ -301,6 +313,7 @@ const mapProductionOrderRow = (row) => { rejectedQuantity: Number(row.rejected_quantity || 0), finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity), qualityNotes: row.quality_notes || '', + defects: Array.isArray(row.defects) ? row.defects.map(mapDefect) : [], lotReservations: Array.isArray(row.lot_reservations) ? row.lot_reservations.map(mapLotReservation) : [], markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [], components: Array.isArray(row.components) ? row.components.map(mapComponent) : [], @@ -502,7 +515,26 @@ const getOrderById = async (id, client = pool) => { WHERE step.production_order_id = po.id ), '[]'::json - ) as steps + ) as steps, + COALESCE( + ( + SELECT JSON_AGG(JSON_BUILD_OBJECT( + 'id', defect.id, + 'workstation_id', defect.workstation_id, + 'workstation_name', workstation.name, + 'defect_type', defect.defect_type, + 'origin', defect.origin, + 'quantity', defect.quantity, + 'lot_code', defect.lot_code, + 'notes', defect.notes, + 'occurred_at', defect.occurred_at + ) ORDER BY defect.occurred_at DESC, defect.id DESC) + FROM production_order_defects defect + LEFT JOIN production_workstations workstation ON workstation.id = defect.workstation_id + WHERE defect.production_order_id = po.id + ), + '[]'::json + ) as defects FROM production_orders po LEFT JOIN production_plans plan ON plan.id = po.production_plan_id WHERE po.id = $1; @@ -621,7 +653,26 @@ const baseProductionOrderSelect = ` WHERE step.production_order_id = po.id ), '[]'::json - ) as steps + ) as steps, + COALESCE( + ( + SELECT JSON_AGG(JSON_BUILD_OBJECT( + 'id', defect.id, + 'workstation_id', defect.workstation_id, + 'workstation_name', workstation.name, + 'defect_type', defect.defect_type, + 'origin', defect.origin, + 'quantity', defect.quantity, + 'lot_code', defect.lot_code, + 'notes', defect.notes, + 'occurred_at', defect.occurred_at + ) ORDER BY defect.occurred_at DESC, defect.id DESC) + FROM production_order_defects defect + LEFT JOIN production_workstations workstation ON workstation.id = defect.workstation_id + WHERE defect.production_order_id = po.id + ), + '[]'::json + ) as defects FROM production_orders po LEFT JOIN production_plans plan ON plan.id = po.production_plan_id `; @@ -1739,12 +1790,49 @@ const updateProductionOrderExecution = async (id, payload = {}) => { return getOrderById(id); }; +const listProductionWorkstations = async () => { + const result = await pool.query(` + SELECT id, code, name, color, sort_order, is_active + FROM production_workstations + WHERE is_active = TRUE + ORDER BY sort_order, name; + `); + return result.rows.map(row => ({ id: Number(row.id), code: row.code, name: row.name, color: row.color, sortOrder: Number(row.sort_order), isActive: Boolean(row.is_active) })); +}; + +const addProductionOrderDefect = async (id, payload = {}) => { + const defectType = normalizeText(payload.defectType); + const quantity = normalizeQuantity(payload.quantity); + const workstationId = normalizeInteger(payload.workstationId); + if (!defectType || !quantity) { + const error = new Error('Informe o tipo e a quantidade do defeito.'); + error.statusCode = 400; + throw error; + } + if (workstationId) { + const workstation = await pool.query('SELECT id FROM production_workstations WHERE id = $1 AND is_active = TRUE;', [workstationId]); + if (!workstation.rowCount) { + const error = new Error('Posto de trabalho inválido.'); + error.statusCode = 400; + throw error; + } + } + const result = await pool.query(` + INSERT INTO production_order_defects (production_order_id, workstation_id, defect_type, origin, quantity, lot_code, notes) + VALUES ($1, $2, $3, NULLIF($4, ''), $5, NULLIF($6, ''), NULLIF($7, '')) + RETURNING id; + `, [id, workstationId, defectType, normalizeText(payload.origin), quantity, normalizeText(payload.lotCode), normalizeText(payload.notes)]); + if (!result.rowCount) throw new Error('Não foi possível registrar o defeito.'); + return getOrderById(id); +}; + module.exports = { createProductionOrders, deleteManualProductComposition, getProductComposition, importTinyProductCompositionExport, listProductionPlans, + listProductionWorkstations, listProductCompositions, listProductionOrders, normalizeStatus, @@ -1753,6 +1841,7 @@ module.exports = { upsertTinyProductionOrderDetail, updateProductionPlan, updateProductionPlanStatus, + addProductionOrderDefect, updateProductionOrderExecution, updateProductionOrderStatus };