Add production plan release checklist

This commit is contained in:
Cauê Faleiros
2026-08-27 10:51:41 -03:00
parent 442b23dc62
commit 791246c55e
6 changed files with 189 additions and 15 deletions

View File

@@ -189,6 +189,21 @@ const initDB = async () => {
);
`);
// The release decision remains auditable when the plan changes status.
await pool.query(`
CREATE TABLE IF NOT EXISTS production_plan_reviews (
id SERIAL PRIMARY KEY,
production_plan_id INTEGER NOT NULL UNIQUE REFERENCES production_plans(id) ON DELETE CASCADE,
decision VARCHAR(30) NOT NULL DEFAULT 'pending',
checklist JSONB NOT NULL DEFAULT '{}'::jsonb,
notes TEXT,
reviewed_by TEXT,
reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
`);
await pool.query(`
ALTER TABLE production_orders
ADD COLUMN IF NOT EXISTS production_plan_id INTEGER REFERENCES production_plans(id) ON DELETE SET NULL,

View File

@@ -1,6 +1,6 @@
const express = require('express');
const { authenticateAPIKey, verifyToken } = require('../auth');
const { addProductionOrderDefect, createProductionOrders, createProductionWorkstation, importTinyProductCompositionExport, listProductionOrders, listProductionPlans, listProductionWorkstations, updateProductionOrderExecution, updateProductionOrderStatus, updateProductionPlan, updateProductionPlanStatus, updateProductionWorkstation, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
const { addProductionOrderDefect, createProductionOrders, createProductionWorkstation, importTinyProductCompositionExport, listProductionOrders, listProductionPlans, listProductionWorkstations, saveProductionPlanReview, updateProductionOrderExecution, updateProductionOrderStatus, updateProductionPlan, updateProductionPlanStatus, updateProductionWorkstation, upsertTinyProductionOrderDetail } = require('../services/productionOrderService');
const router = express.Router();
@@ -75,6 +75,14 @@ router.patch('/production-plans/:id/status', verifyToken, async (req, res) => {
}
});
router.put('/production-plans/:id/review', verifyToken, async (req, res) => {
try {
res.json(await saveProductionPlanReview(req.params.id, req.body || {}));
} catch (error) {
res.status(error.statusCode || 500).json({ error: error.message || 'Internal Server Error' });
}
});
router.post('/production-orders/tiny-sync', authenticateAPIKey, async (req, res) => {
try {
res.status(201).json(await upsertTinyProductionOrderDetail(req.body || {}));

View File

@@ -351,10 +351,57 @@ const mapProductionPlanRow = (row) => ({
actualCutQuantity: Number(row.actual_cut_quantity || 0),
finishedQuantity: Number(row.finished_quantity || 0),
statusCounts: row.status_counts || {},
review: row.review_id ? {
id: Number(row.review_id),
decision: row.review_decision || 'pending',
checklist: row.review_checklist || {},
notes: row.review_notes || '',
reviewedBy: row.reviewed_by || '',
reviewedAt: row.reviewed_at || null,
updatedAt: row.review_updated_at || null
} : null,
createdAt: row.created_at || null,
updatedAt: row.updated_at || null
});
const REVIEW_DECISIONS = new Set(['pending', 'approved', 'adjustments_required']);
const REVIEW_CHECKLIST_KEYS = new Set(['materialsReserved', 'cutSpecificationReady', 'sizeAndColorReviewed', 'responsibleAndDateDefined']);
const normalizeProductionPlanReview = (payload = {}) => {
const decision = normalizeText(payload.decision).toLowerCase() || 'pending';
if (!REVIEW_DECISIONS.has(decision)) {
const error = new Error('Decisão de revisão inválida.');
error.statusCode = 400;
throw error;
}
const rawChecklist = payload.checklist && typeof payload.checklist === 'object' ? payload.checklist : {};
const checklist = {};
REVIEW_CHECKLIST_KEYS.forEach(key => { checklist[key] = Boolean(rawChecklist[key]); });
return { decision, checklist, notes: normalizeText(payload.notes), reviewedBy: normalizeText(payload.reviewedBy) };
};
const saveProductionPlanReview = async (id, payload = {}) => {
const review = normalizeProductionPlanReview(payload);
const planExists = await pool.query('SELECT id FROM production_plans WHERE id = $1', [id]);
if (!planExists.rowCount) {
const error = new Error('Plano de produção não encontrado.');
error.statusCode = 404;
throw error;
}
await pool.query(`
INSERT INTO production_plan_reviews (production_plan_id, decision, checklist, notes, reviewed_by, reviewed_at, updated_at)
VALUES ($1, $2, $3::jsonb, NULLIF($4, ''), NULLIF($5, ''), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (production_plan_id) DO UPDATE
SET decision = EXCLUDED.decision,
checklist = EXCLUDED.checklist,
notes = EXCLUDED.notes,
reviewed_by = EXCLUDED.reviewed_by,
reviewed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
`, [id, review.decision, JSON.stringify(review.checklist), review.notes, review.reviewedBy]);
return getProductionPlanById(id);
};
const normalizePlanPayload = (payload = {}, fallback = {}) => ({
id: normalizeInteger(payload.id),
name: normalizeText(payload.name) || normalizeText(fallback.name),
@@ -388,12 +435,16 @@ const getProductionPlanById = async (id, client = pool) => {
WHERE production_plan_id = plan.id
GROUP BY status
) grouped_statuses
), '{}'::json) AS status_counts
), '{}'::json) AS status_counts,
review.id AS review_id, review.decision AS review_decision,
review.checklist AS review_checklist, review.notes AS review_notes,
review.reviewed_by, review.reviewed_at, review.updated_at AS review_updated_at
FROM production_plans plan
LEFT JOIN production_orders po ON po.production_plan_id = plan.id
LEFT JOIN app_users responsible ON responsible.id = plan.responsible_user_id
LEFT JOIN production_plan_reviews review ON review.production_plan_id = plan.id
WHERE plan.id = $1
GROUP BY plan.id, responsible.id;
GROUP BY plan.id, responsible.id, review.id;
`, [id]);
return result.rows[0] ? mapProductionPlanRow(result.rows[0]) : null;
};
@@ -789,11 +840,15 @@ const listProductionPlans = async () => {
WHERE production_plan_id = plan.id
GROUP BY status
) grouped_statuses
), '{}'::json) AS status_counts
), '{}'::json) AS status_counts,
review.id AS review_id, review.decision AS review_decision,
review.checklist AS review_checklist, review.notes AS review_notes,
review.reviewed_by, review.reviewed_at, review.updated_at AS review_updated_at
FROM production_plans plan
LEFT JOIN production_orders po ON po.production_plan_id = plan.id
LEFT JOIN app_users responsible ON responsible.id = plan.responsible_user_id
GROUP BY plan.id, responsible.id
LEFT JOIN production_plan_reviews review ON review.production_plan_id = plan.id
GROUP BY plan.id, responsible.id, review.id
ORDER BY COALESCE(plan.planned_start_date, plan.created_at::date) DESC, plan.id DESC;
`);
return result.rows.map(mapProductionPlanRow);
@@ -1942,6 +1997,7 @@ module.exports = {
getProductComposition,
importTinyProductCompositionExport,
listProductionPlans,
saveProductionPlanReview,
listProductionWorkstations,
listProductCompositions,
listProductionOrders,