diff --git a/backend/db.js b/backend/db.js index e6fd74f..e130c89 100644 --- a/backend/db.js +++ b/backend/db.js @@ -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(` diff --git a/backend/services/productionOrderService.js b/backend/services/productionOrderService.js index 4dd0c4c..f89c341 100644 --- a/backend/services/productionOrderService.js +++ b/backend/services/productionOrderService.js @@ -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; diff --git a/backend/services/userService.js b/backend/services/userService.js index 2244453..9f7c1a5 100644 --- a/backend/services/userService.js +++ b/backend/services/userService.js @@ -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 }; diff --git a/src/pages/Cutting.tsx b/src/pages/Cutting.tsx index 590a80e..9c3f8b8 100644 --- a/src/pages/Cutting.tsx +++ b/src/pages/Cutting.tsx @@ -169,7 +169,7 @@ const Cutting = () => { const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10); const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState([]); const [lotSelections, setLotSelections] = useState>({}); - const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, expectedDate: '', notes: '' }); + const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, responsibleName: '', expectedDate: '', notes: '' }); const [operationalUsers, setOperationalUsers] = useState([]); const [isLoadingOperationalUsers, setIsLoadingOperationalUsers] = useState(false); const [isResponsiblePickerOpen, setIsResponsiblePickerOpen] = useState(false); @@ -399,8 +399,8 @@ const Cutting = () => { selectedPreviewRows.reduce((total, row) => total + row.suggestedCutQuantity, 0) ), [selectedPreviewRows]); const selectedResponsibleUser = useMemo(() => ( - operationalUsers.find((user) => user.id === productionPlanForm.responsibleUserId) || null - ), [operationalUsers, productionPlanForm.responsibleUserId]); + operationalUsers.find((user) => user.id === productionPlanForm.responsibleUserId && user.name === productionPlanForm.responsibleName) || null + ), [operationalUsers, productionPlanForm.responsibleName, productionPlanForm.responsibleUserId]); const cutPlanReference = useMemo(() => ( `Plano de corte ${formatDateKey(dateRange.start)} a ${formatDateKey(dateRange.end)} · ${targetCoverageDays} dias` @@ -676,7 +676,7 @@ const Cutting = () => { setPreviewPage(1); setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id)); setLotSelections({}); - setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, expectedDate: '', notes: '' }); + setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, responsibleName: '', expectedDate: '', notes: '' }); setIsPlanDetailsOpen(false); setIsResponsiblePickerOpen(false); setPreviewStep('selection'); @@ -765,6 +765,7 @@ const Cutting = () => { })), selectedLotReservations, { name: productionPlanForm.name.trim() || cutPlanReference, responsibleUserId: productionPlanForm.responsibleUserId, + responsibleName: productionPlanForm.responsibleName, plannedStartDate: issueDate, expectedDate: productionPlanForm.expectedDate || null, notes: productionPlanForm.notes.trim(), @@ -961,13 +962,13 @@ const Cutting = () => {
Usuários ativos do Graphs
{isLoadingOperationalUsers ?

Carregando usuários...

: operationalUsers.length ? operationalUsers.map(user => ( - )) :

Nenhum usuário ativo encontrado.

}
- {selectedResponsibleUser && } + {selectedResponsibleUser && } } {responsiblePickerPlanId === plan.id &&
Usuários ativos do Graphs
{operationalUsers.map(user => )}{!operationalUsers.length &&

Nenhum usuário ativo encontrado.

}
{plan.responsibleName && }
} ))} diff --git a/src/types.ts b/src/types.ts index c6a6453..b5773b3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -202,6 +202,7 @@ export type ProductionPlanPayload = { name: string; contractor?: string; responsibleUserId?: number | null; + responsibleName?: string; plannedStartDate?: string | null; expectedDate?: string | null; notes?: string; @@ -630,8 +631,9 @@ export interface ManagedUser { } export interface OperationalUser { - id: number; + id: number | null; name: string; + isSystem?: boolean; } export interface CreateUserResult {