Add production defect and workstation foundation

This commit is contained in:
Cauê Faleiros
2026-08-25 11:58:43 -03:00
parent 282f1e194a
commit 5370dffce2
3 changed files with 145 additions and 3 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
};