Add production defect and workstation foundation
This commit is contained in:
@@ -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(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS product_compositions (
|
CREATE TABLE IF NOT EXISTS product_compositions (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { authenticateAPIKey, verifyToken } = require('../auth');
|
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();
|
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) => {
|
router.put('/production-plans/:id', verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json(await updateProductionPlan(req.params.id, req.body || {}));
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -267,6 +267,18 @@ const mapLotReservation = (reservation) => ({
|
|||||||
lotSupplier: reservation.lot_supplier || ''
|
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 mapProductionOrderRow = (row) => {
|
||||||
const status = normalizeStatus(row.status);
|
const status = normalizeStatus(row.status);
|
||||||
|
|
||||||
@@ -301,6 +313,7 @@ const mapProductionOrderRow = (row) => {
|
|||||||
rejectedQuantity: Number(row.rejected_quantity || 0),
|
rejectedQuantity: Number(row.rejected_quantity || 0),
|
||||||
finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity),
|
finishedQuantity: row.finished_quantity === null ? null : Number(row.finished_quantity),
|
||||||
qualityNotes: row.quality_notes || '',
|
qualityNotes: row.quality_notes || '',
|
||||||
|
defects: Array.isArray(row.defects) ? row.defects.map(mapDefect) : [],
|
||||||
lotReservations: Array.isArray(row.lot_reservations) ? row.lot_reservations.map(mapLotReservation) : [],
|
lotReservations: Array.isArray(row.lot_reservations) ? row.lot_reservations.map(mapLotReservation) : [],
|
||||||
markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [],
|
markers: Array.isArray(row.markers) ? row.markers.filter(Boolean) : [],
|
||||||
components: Array.isArray(row.components) ? row.components.map(mapComponent) : [],
|
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
|
WHERE step.production_order_id = po.id
|
||||||
),
|
),
|
||||||
'[]'::json
|
'[]'::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
|
FROM production_orders po
|
||||||
LEFT JOIN production_plans plan ON plan.id = po.production_plan_id
|
LEFT JOIN production_plans plan ON plan.id = po.production_plan_id
|
||||||
WHERE po.id = $1;
|
WHERE po.id = $1;
|
||||||
@@ -621,7 +653,26 @@ const baseProductionOrderSelect = `
|
|||||||
WHERE step.production_order_id = po.id
|
WHERE step.production_order_id = po.id
|
||||||
),
|
),
|
||||||
'[]'::json
|
'[]'::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
|
FROM production_orders po
|
||||||
LEFT JOIN production_plans plan ON plan.id = po.production_plan_id
|
LEFT JOIN production_plans plan ON plan.id = po.production_plan_id
|
||||||
`;
|
`;
|
||||||
@@ -1739,12 +1790,49 @@ const updateProductionOrderExecution = async (id, payload = {}) => {
|
|||||||
return getOrderById(id);
|
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 = {
|
module.exports = {
|
||||||
createProductionOrders,
|
createProductionOrders,
|
||||||
deleteManualProductComposition,
|
deleteManualProductComposition,
|
||||||
getProductComposition,
|
getProductComposition,
|
||||||
importTinyProductCompositionExport,
|
importTinyProductCompositionExport,
|
||||||
listProductionPlans,
|
listProductionPlans,
|
||||||
|
listProductionWorkstations,
|
||||||
listProductCompositions,
|
listProductCompositions,
|
||||||
listProductionOrders,
|
listProductionOrders,
|
||||||
normalizeStatus,
|
normalizeStatus,
|
||||||
@@ -1753,6 +1841,7 @@ module.exports = {
|
|||||||
upsertTinyProductionOrderDetail,
|
upsertTinyProductionOrderDetail,
|
||||||
updateProductionPlan,
|
updateProductionPlan,
|
||||||
updateProductionPlanStatus,
|
updateProductionPlanStatus,
|
||||||
|
addProductionOrderDefect,
|
||||||
updateProductionOrderExecution,
|
updateProductionOrderExecution,
|
||||||
updateProductionOrderStatus
|
updateProductionOrderStatus
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user