Allow updating production plan responsibility
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m13s

This commit is contained in:
Cauê Faleiros
2026-08-25 16:42:51 -03:00
parent c259ddea29
commit 3a0cc9ffe8
6 changed files with 97 additions and 41 deletions

View File

@@ -796,7 +796,8 @@ const initDB = async () => {
// role-based assignments be added without changing historical plans.
await pool.query(`
ALTER TABLE production_plans
ADD COLUMN IF NOT EXISTS responsible_user_id INTEGER REFERENCES app_users(id) ON DELETE SET NULL;
ADD COLUMN IF NOT EXISTS responsible_user_id INTEGER REFERENCES app_users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS responsible_name TEXT;
`).catch(() => {});
await pool.query(`

View File

@@ -1,5 +1,5 @@
const { pool } = require('../db');
const { getActiveOperationalUserById } = require('./userService');
const { getActiveOperationalUserById, SYSTEM_OPERATIONAL_USER_NAME } = require('./userService');
const STATUS_LABELS = {
open: 'Em aberto',
@@ -332,7 +332,7 @@ const mapProductionPlanRow = (row) => ({
statusLabel: STATUS_LABELS[normalizeStatus(row.status)] || row.status || 'Em aberto',
contractor: row.contractor || '',
responsibleUserId: row.responsible_user_id === null || row.responsible_user_id === undefined ? null : Number(row.responsible_user_id),
responsibleName: row.responsible_name || '',
responsibleName: row.responsible_user_name || row.responsible_name || '',
plannedStartDate: formatDate(row.planned_start_date),
expectedDate: formatDate(row.expected_date),
completedAt: row.completed_at || null,
@@ -354,6 +354,7 @@ const normalizePlanPayload = (payload = {}, fallback = {}) => ({
responsibleUserId: payload.responsibleUserId === undefined
? normalizeInteger(fallback.responsibleUserId)
: normalizeInteger(payload.responsibleUserId),
responsibleName: normalizeText(payload.responsibleName) || normalizeText(fallback.responsibleName),
plannedStartDate: normalizeDateParam(payload.plannedStartDate || fallback.plannedStartDate),
expectedDate: normalizeDateParam(payload.expectedDate || fallback.expectedDate),
notes: normalizeText(payload.notes),
@@ -364,7 +365,7 @@ const getProductionPlanById = async (id, client = pool) => {
const result = await client.query(`
SELECT
plan.id, plan.number, plan.name, plan.status, plan.contractor, plan.responsible_user_id,
responsible.name AS responsible_name,
responsible.name AS responsible_user_name, plan.responsible_name,
plan.planned_start_date, plan.expected_date, plan.completed_at,
plan.notes, plan.source, plan.created_at, plan.updated_at,
COUNT(po.id)::int AS order_count,
@@ -389,6 +390,26 @@ const getProductionPlanById = async (id, client = pool) => {
return result.rows[0] ? mapProductionPlanRow(result.rows[0]) : null;
};
const resolvePlanResponsible = async (plan, client = pool) => {
if (plan.responsibleUserId) {
const user = await getActiveOperationalUserById(plan.responsibleUserId, client);
if (!user) {
const error = new Error('O responsável selecionado não está disponível.');
error.statusCode = 400;
throw error;
}
return { userId: Number(user.id), name: user.name };
}
if (plan.responsibleName && plan.responsibleName !== SYSTEM_OPERATIONAL_USER_NAME) {
const error = new Error('O responsável selecionado não está disponível.');
error.statusCode = 400;
throw error;
}
return { userId: null, name: plan.responsibleName };
};
const createOrGetProductionPlan = async (client, payload, fallback) => {
const plan = normalizePlanPayload(payload, fallback);
if (plan.id) {
@@ -402,17 +423,13 @@ const createOrGetProductionPlan = async (client, payload, fallback) => {
}
if (!plan.name) return null;
if (plan.responsibleUserId && !await getActiveOperationalUserById(plan.responsibleUserId, client)) {
const error = new Error('O responsável selecionado não está disponível.');
error.statusCode = 400;
throw error;
}
const responsible = await resolvePlanResponsible(plan, client);
const inserted = await client.query(`
INSERT INTO production_plans (name, status, contractor, responsible_user_id, planned_start_date, expected_date, notes, source)
VALUES ($1, 'open', NULLIF($2, ''), $3, $4, $5, NULLIF($6, ''), $7)
INSERT INTO production_plans (name, status, contractor, responsible_user_id, responsible_name, planned_start_date, expected_date, notes, source)
VALUES ($1, 'open', NULLIF($2, ''), $3, NULLIF($4, ''), $5, $6, NULLIF($7, ''), $8)
RETURNING id;
`, [plan.name, plan.contractor, plan.responsibleUserId, plan.plannedStartDate, plan.expectedDate, plan.notes, plan.source]);
`, [plan.name, plan.contractor, responsible.userId, responsible.name, plan.plannedStartDate, plan.expectedDate, plan.notes, plan.source]);
const id = Number(inserted.rows[0].id);
await client.query(`UPDATE production_plans SET number = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2;`, [`PP-${String(id).padStart(5, '0')}`, id]);
return getProductionPlanById(id, client);
@@ -749,7 +766,7 @@ const listProductionPlans = async () => {
const result = await pool.query(`
SELECT
plan.id, plan.number, plan.name, plan.status, plan.contractor, plan.responsible_user_id,
responsible.name AS responsible_name,
responsible.name AS responsible_user_name, plan.responsible_name,
plan.planned_start_date, plan.expected_date, plan.completed_at,
plan.notes, plan.source, plan.created_at, plan.updated_at,
COUNT(po.id)::int AS order_count,
@@ -781,23 +798,20 @@ const updateProductionPlan = async (id, payload = {}) => {
error.statusCode = 400;
throw error;
}
if (plan.responsibleUserId && !await getActiveOperationalUserById(plan.responsibleUserId)) {
const error = new Error('O responsável selecionado não está disponível.');
error.statusCode = 400;
throw error;
}
const responsible = await resolvePlanResponsible(plan);
const result = await pool.query(`
UPDATE production_plans
SET name = $1,
contractor = NULLIF($2, ''),
responsible_user_id = $3,
planned_start_date = $4,
expected_date = $5,
notes = NULLIF($6, ''),
responsible_name = NULLIF($4, ''),
planned_start_date = $5,
expected_date = $6,
notes = NULLIF($7, ''),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
WHERE id = $8
RETURNING id;
`, [plan.name, plan.contractor, plan.responsibleUserId, plan.plannedStartDate, plan.expectedDate, plan.notes, id]);
`, [plan.name, plan.contractor, responsible.userId, responsible.name, plan.plannedStartDate, plan.expectedDate, plan.notes, id]);
if (!result.rowCount) {
const error = new Error('Plano de produção não encontrado.');
error.statusCode = 404;

View File

@@ -1,6 +1,8 @@
const crypto = require('crypto');
const { pool } = require('../db');
const SYSTEM_OPERATIONAL_USER_NAME = 'Super Admin';
const HASH_ALGORITHM = 'scrypt';
const KEY_LENGTH = 64;
@@ -70,10 +72,14 @@ const listActiveOperationalUsers = async () => {
ORDER BY LOWER(name), id`
);
return result.rows.map((row) => ({
id: Number(row.id),
name: row.name
}));
return [
{ id: null, name: SYSTEM_OPERATIONAL_USER_NAME, isSystem: true },
...result.rows.map((row) => ({
id: Number(row.id),
name: row.name,
isSystem: false
}))
];
};
const getActiveOperationalUserById = async (id, client = pool) => {
@@ -234,6 +240,7 @@ module.exports = {
listUsers,
normalizeEmail,
publicUserFields,
SYSTEM_OPERATIONAL_USER_NAME,
updateUser,
verifyPassword
};