Assign production plans to Graphs users
This commit is contained in:
@@ -792,6 +792,13 @@ const initDB = async () => {
|
||||
ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
|
||||
`).catch(() => {});
|
||||
|
||||
// A production plan is operated by a Graphs user. The ID link will let
|
||||
// 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;
|
||||
`).catch(() => {});
|
||||
|
||||
await pool.query(`
|
||||
ALTER TABLE stock_campaign_queue
|
||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
const express = require('express');
|
||||
const { verifySuperAdmin } = require('../auth');
|
||||
const { createUser, deleteUser, listUsers, updateUser } = require('../services/userService');
|
||||
const { verifySuperAdmin, verifyToken } = require('../auth');
|
||||
const { createUser, deleteUser, listActiveOperationalUsers, listUsers, updateUser } = require('../services/userService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/users/operational', verifyToken, async (req, res, next) => {
|
||||
try {
|
||||
res.json({ users: await listActiveOperationalUsers() });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users', verifySuperAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const users = await listUsers();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { pool } = require('../db');
|
||||
const { getActiveOperationalUserById } = require('./userService');
|
||||
|
||||
const STATUS_LABELS = {
|
||||
open: 'Em aberto',
|
||||
@@ -330,6 +331,8 @@ const mapProductionPlanRow = (row) => ({
|
||||
status: normalizeStatus(row.status),
|
||||
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 || '',
|
||||
plannedStartDate: formatDate(row.planned_start_date),
|
||||
expectedDate: formatDate(row.expected_date),
|
||||
completedAt: row.completed_at || null,
|
||||
@@ -348,6 +351,9 @@ const normalizePlanPayload = (payload = {}, fallback = {}) => ({
|
||||
id: normalizeInteger(payload.id),
|
||||
name: normalizeText(payload.name) || normalizeText(fallback.name),
|
||||
contractor: normalizeText(payload.contractor),
|
||||
responsibleUserId: payload.responsibleUserId === undefined
|
||||
? normalizeInteger(fallback.responsibleUserId)
|
||||
: normalizeInteger(payload.responsibleUserId),
|
||||
plannedStartDate: normalizeDateParam(payload.plannedStartDate || fallback.plannedStartDate),
|
||||
expectedDate: normalizeDateParam(payload.expectedDate || fallback.expectedDate),
|
||||
notes: normalizeText(payload.notes),
|
||||
@@ -357,7 +363,8 @@ const normalizePlanPayload = (payload = {}, fallback = {}) => ({
|
||||
const getProductionPlanById = async (id, client = pool) => {
|
||||
const result = await client.query(`
|
||||
SELECT
|
||||
plan.id, plan.number, plan.name, plan.status, plan.contractor,
|
||||
plan.id, plan.number, plan.name, plan.status, plan.contractor, plan.responsible_user_id,
|
||||
responsible.name AS 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,
|
||||
@@ -375,8 +382,9 @@ const getProductionPlanById = async (id, client = pool) => {
|
||||
), '{}'::json) AS status_counts
|
||||
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
|
||||
WHERE plan.id = $1
|
||||
GROUP BY plan.id;
|
||||
GROUP BY plan.id, responsible.id;
|
||||
`, [id]);
|
||||
return result.rows[0] ? mapProductionPlanRow(result.rows[0]) : null;
|
||||
};
|
||||
@@ -394,11 +402,17 @@ 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 inserted = await client.query(`
|
||||
INSERT INTO production_plans (name, status, contractor, planned_start_date, expected_date, notes, source)
|
||||
VALUES ($1, 'open', NULLIF($2, ''), $3, $4, NULLIF($5, ''), $6)
|
||||
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)
|
||||
RETURNING id;
|
||||
`, [plan.name, plan.contractor, plan.plannedStartDate, plan.expectedDate, plan.notes, plan.source]);
|
||||
`, [plan.name, plan.contractor, plan.responsibleUserId, 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);
|
||||
@@ -734,7 +748,8 @@ const listProductionOrders = async (filters = {}) => {
|
||||
const listProductionPlans = async () => {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
plan.id, plan.number, plan.name, plan.status, plan.contractor,
|
||||
plan.id, plan.number, plan.name, plan.status, plan.contractor, plan.responsible_user_id,
|
||||
responsible.name AS 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,
|
||||
@@ -752,7 +767,8 @@ const listProductionPlans = async () => {
|
||||
), '{}'::json) AS status_counts
|
||||
FROM production_plans plan
|
||||
LEFT JOIN production_orders po ON po.production_plan_id = plan.id
|
||||
GROUP BY plan.id
|
||||
LEFT JOIN app_users responsible ON responsible.id = plan.responsible_user_id
|
||||
GROUP BY plan.id, responsible.id
|
||||
ORDER BY COALESCE(plan.planned_start_date, plan.created_at::date) DESC, plan.id DESC;
|
||||
`);
|
||||
return result.rows.map(mapProductionPlanRow);
|
||||
@@ -765,17 +781,23 @@ 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 result = await pool.query(`
|
||||
UPDATE production_plans
|
||||
SET name = $1,
|
||||
contractor = NULLIF($2, ''),
|
||||
planned_start_date = $3,
|
||||
expected_date = $4,
|
||||
notes = NULLIF($5, ''),
|
||||
responsible_user_id = $3,
|
||||
planned_start_date = $4,
|
||||
expected_date = $5,
|
||||
notes = NULLIF($6, ''),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6
|
||||
WHERE id = $7
|
||||
RETURNING id;
|
||||
`, [plan.name, plan.contractor, plan.plannedStartDate, plan.expectedDate, plan.notes, id]);
|
||||
`, [plan.name, plan.contractor, plan.responsibleUserId, plan.plannedStartDate, plan.expectedDate, plan.notes, id]);
|
||||
if (!result.rowCount) {
|
||||
const error = new Error('Plano de produção não encontrado.');
|
||||
error.statusCode = 404;
|
||||
|
||||
@@ -59,6 +59,35 @@ const listUsers = async () => {
|
||||
return result.rows.map(publicUserFields);
|
||||
};
|
||||
|
||||
// This deliberately exposes only the identity needed to assign operational
|
||||
// responsibility. It is available to signed-in Graphs users, unlike the full
|
||||
// user-management list which remains super-admin only.
|
||||
const listActiveOperationalUsers = async () => {
|
||||
const result = await pool.query(
|
||||
`SELECT id, name
|
||||
FROM app_users
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY LOWER(name), id`
|
||||
);
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
id: Number(row.id),
|
||||
name: row.name
|
||||
}));
|
||||
};
|
||||
|
||||
const getActiveOperationalUserById = async (id, client = pool) => {
|
||||
const result = await client.query(
|
||||
`SELECT id, name
|
||||
FROM app_users
|
||||
WHERE id = $1 AND is_active = TRUE
|
||||
LIMIT 1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
};
|
||||
|
||||
const createUser = async ({ name, email, password }) => {
|
||||
const normalizedName = String(name || '').trim();
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
@@ -200,6 +229,8 @@ module.exports = {
|
||||
findUserById,
|
||||
findUserByEmail,
|
||||
generatePassword,
|
||||
getActiveOperationalUserById,
|
||||
listActiveOperationalUsers,
|
||||
listUsers,
|
||||
normalizeEmail,
|
||||
publicUserFields,
|
||||
|
||||
Reference in New Issue
Block a user