Site/server/controllers/oriontardproController.js

2008 lines
57 KiB
JavaScript

require('../connector');
const express = require('express');
const router = express.Router();
const TIPOS_OS = new Set([
'CORRETIVA',
'PREVENTIVA',
'REFORMA',
'DESENVOLVIMENTO',
'FABRICACAO',
'GARANTIA',
'CORTESIA',
'INTERNA',
'OUTRO'
]);
const STATUS_OS = new Set([
'RASCUNHO',
'ABERTA',
'EM_DIAGNOSTICO',
'AGUARDANDO_APROVACAO',
'AGUARDANDO_PECA',
'EM_EXECUCAO',
'EM_TESTES',
'FINALIZADA',
'RETIRADA',
'CANCELADA'
]);
const STATUS_PAGAMENTO = new Set([
'NAO_APLICAVEL',
'PENDENTE',
'PARCIAL',
'RECEBIDO',
'CORTESIA'
]);
const PRIORIDADES = new Set(['BAIXA', 'NORMAL', 'ALTA', 'URGENTE']);
const PAPEIS_TECNICO = new Set(['RESPONSAVEL', 'EXECUTOR', 'APOIO']);
const FORMAS_PAGAMENTO = new Set([
'DINHEIRO',
'PIX',
'TRANSFERENCIA',
'BOLETO',
'CARTAO',
'OUTRO'
]);
const TIPOS_ODOMETRO = new Set([
'CADASTRO_INICIAL',
'ENTRADA_MANUTENCAO',
'SAIDA_MANUTENCAO',
'ATUALIZACAO_MANUAL',
'TELEMETRIA'
]);
function getPool() {
if (!global.ConexaoMySQL_Oriontard) {
throw new Error('Pool do banco Oriontard não foi inicializado.');
}
return global.ConexaoMySQL_Oriontard;
}
function dbQuery(sql, params = [], connection = null) {
const db = connection || getPool();
return new Promise((resolve, reject) => {
db.query(sql, params, (error, results, fields) => {
if (error) {
reject(error);
return;
}
resolve({ results, fields });
});
});
}
function getConnection() {
return new Promise((resolve, reject) => {
getPool().getConnection((error, connection) => {
if (error) {
reject(error);
return;
}
resolve(connection);
});
});
}
function beginTransaction(connection) {
return new Promise((resolve, reject) => {
connection.beginTransaction(error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function commit(connection) {
return new Promise((resolve, reject) => {
connection.commit(error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function rollback(connection) {
return new Promise(resolve => {
connection.rollback(() => resolve());
});
}
async function withTransaction(handler) {
const connection = await getConnection();
try {
await beginTransaction(connection);
const result = await handler(connection);
await commit(connection);
return result;
} catch (error) {
await rollback(connection);
throw error;
} finally {
connection.release();
}
}
function sendSuccess(res, response, status = 200) {
return res.status(status).json({
status,
error: null,
response
});
}
function sendError(req, res, error, status = 500, message = 'Erro interno do servidor.') {
console.error(
`${new Date().toUTCString()} - ${req.originalUrl} ${status} error`,
error
);
return res.status(status).json({
status,
error: {
code: error && error.code ? error.code : null,
message
},
response: null
});
}
function createHttpError(status, message) {
const error = new Error(message);
error.httpStatus = status;
return error;
}
function handleRouteError(req, res, error) {
const status = error.httpStatus || 500;
const message = status >= 500
? 'Erro interno ao processar a operação.'
: error.message;
return sendError(req, res, error, status, message);
}
function positiveInteger(value, fieldName, nullable = false) {
if ((value === null || value === undefined || value === '') && nullable) {
return null;
}
const number = Number(value);
if (!Number.isInteger(number) || number <= 0) {
throw createHttpError(400, `${fieldName} inválido.`);
}
return number;
}
function nonNegativeNumber(value, fieldName, defaultValue = 0) {
if (value === null || value === undefined || value === '') {
return defaultValue;
}
const number = Number(value);
if (!Number.isFinite(number) || number < 0) {
throw createHttpError(400, `${fieldName} deve ser um número maior ou igual a zero.`);
}
return number;
}
function nonNegativeInteger(value, fieldName, defaultValue = 0) {
const number = nonNegativeNumber(value, fieldName, defaultValue);
if (!Number.isInteger(number)) {
throw createHttpError(400, `${fieldName} deve ser informado em segundos inteiros.`);
}
return number;
}
function optionalText(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text || null;
}
function requiredText(value, fieldName) {
const text = optionalText(value);
if (!text) {
throw createHttpError(400, `${fieldName} é obrigatório.`);
}
return text;
}
function enumValue(value, allowedValues, fieldName, defaultValue = null) {
const finalValue = value || defaultValue;
if (!allowedValues.has(finalValue)) {
throw createHttpError(400, `${fieldName} inválido.`);
}
return finalValue;
}
function booleanToTinyInt(value, defaultValue = true) {
if (value === undefined || value === null) {
return defaultValue ? 1 : 0;
}
return value === true || value === 1 || value === '1' ? 1 : 0;
}
function normalizeNullableDate(value) {
return value === null || value === undefined || value === '' ? null : value;
}
function sanitizePagination(query) {
const page = Math.max(1, Number.parseInt(query.page, 10) || 1);
const pageSize = Math.min(200, Math.max(1, Number.parseInt(query.pageSize, 10) || 50));
return {
page,
pageSize,
offset: (page - 1) * pageSize
};
}
async function getEquipmentSnapshot(connection, equipamentoId, clienteIdInformado = null) {
const { results: equipamentos } = await dbQuery(
`
SELECT
e.id,
e.cliente_id,
e.nome,
e.numero_serie,
e.versao,
e.odometro_total_segundos,
e.odometro_conectado_segundos,
e.odometro_movimento_segundos,
e.odometro_parcial_segundos,
m.nome AS modelo_nome
FROM manut_equipamentos e
LEFT JOIN manut_equipamento_modelos m ON m.id = e.modelo_id
WHERE e.id = ?
AND e.ativo = 1
LIMIT 1;
`,
[equipamentoId],
connection
);
if (!equipamentos.length) {
throw createHttpError(404, 'Equipamento não encontrado ou inativo.');
}
const equipamento = equipamentos[0];
const clienteId = clienteIdInformado || equipamento.cliente_id || null;
let clienteNome = null;
if (clienteId) {
const { results: clientes } = await dbQuery(
`
SELECT id, nome_fantasia
FROM manut_clientes
WHERE id = ?
AND ativo = 1
LIMIT 1;
`,
[clienteId],
connection
);
if (!clientes.length) {
throw createHttpError(404, 'Cliente não encontrado ou inativo.');
}
clienteNome = clientes[0].nome_fantasia;
}
return {
equipamento,
clienteId,
clienteNome
};
}
function parseOdometer(payload) {
if (!payload) {
return null;
}
return {
total: nonNegativeInteger(
payload.odometro_total_segundos ?? payload.total_segundos,
'Odômetro total'
),
conectado: nonNegativeInteger(
payload.odometro_conectado_segundos ?? payload.conectado_segundos,
'Odômetro conectado'
),
movimento: nonNegativeInteger(
payload.odometro_movimento_segundos ?? payload.movimento_segundos,
'Odômetro de movimento'
),
parcial: nonNegativeInteger(
payload.odometro_parcial_segundos ?? payload.parcial_segundos,
'Odômetro parcial'
),
leituraEm: normalizeNullableDate(payload.leitura_em),
observacoes: optionalText(payload.observacoes)
};
}
function validateCumulativeOdometer(current, incoming) {
const comparisons = [
['total', Number(current.odometro_total_segundos || 0), incoming.total],
['conectado', Number(current.odometro_conectado_segundos || 0), incoming.conectado],
['movimento', Number(current.odometro_movimento_segundos || 0), incoming.movimento]
];
for (const [name, oldValue, newValue] of comparisons) {
if (newValue < oldValue) {
throw createHttpError(
409,
`O odômetro ${name} não pode diminuir. Atual: ${oldValue}; informado: ${newValue}.`
);
}
}
}
async function insertOdometerReading(
connection,
{
equipamento,
ordemServicoId,
odometro,
tipoLeitura,
usuarioId,
origem = 'SISTEMA'
}
) {
validateCumulativeOdometer(equipamento, odometro);
const leituraEm = odometro.leituraEm || new Date();
await dbQuery(
`
INSERT INTO manut_equipamento_odometro_leituras (
equipamento_id,
ordem_servico_id,
tipo_leitura,
origem,
leitura_em,
odometro_total_segundos,
odometro_conectado_segundos,
odometro_movimento_segundos,
odometro_parcial_segundos,
observacoes,
registrado_por_usuario_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`,
[
equipamento.id,
ordemServicoId,
tipoLeitura,
origem,
leituraEm,
odometro.total,
odometro.conectado,
odometro.movimento,
odometro.parcial,
odometro.observacoes,
usuarioId
],
connection
);
await dbQuery(
`
UPDATE manut_equipamentos
SET
odometro_total_segundos = ?,
odometro_conectado_segundos = ?,
odometro_movimento_segundos = ?,
odometro_parcial_segundos = ?,
odometro_atualizado_em = ?,
atualizado_por_usuario_id = ?
WHERE id = ?;
`,
[
odometro.total,
odometro.conectado,
odometro.movimento,
odometro.parcial,
leituraEm,
usuarioId,
equipamento.id
],
connection
);
}
async function recalculateOrderTotals(connection, ordemServicoId) {
const { results: rows } = await dbQuery(
`
SELECT
os.valor_desconto,
os.valor_acrescimo,
os.status_pagamento,
COALESCE(a.total_horas_expediente, 0) AS total_horas_expediente,
COALESCE(a.total_horas_extras, 0) AS total_horas_extras,
COALESCE(a.valor_mao_obra, 0) AS valor_mao_obra,
COALESCE(m.custo_insumos, 0) AS custo_insumos,
COALESCE(m.valor_insumos, 0) AS valor_insumos,
COALESCE(p.valor_pago, 0) AS valor_pago
FROM manut_ordens_servico os
LEFT JOIN (
SELECT
ordem_servico_id,
SUM(horas_expediente) AS total_horas_expediente,
SUM(horas_extras) AS total_horas_extras,
SUM(valor_mao_obra) AS valor_mao_obra
FROM manut_ordem_servico_atividades
WHERE ordem_servico_id = ?
GROUP BY ordem_servico_id
) a ON a.ordem_servico_id = os.id
LEFT JOIN (
SELECT
atv.ordem_servico_id,
SUM(mat.valor_custo_total) AS custo_insumos,
SUM(mat.valor_cobrado_total) AS valor_insumos
FROM manut_ordem_servico_atividades atv
INNER JOIN manut_ordem_servico_atividade_materiais mat
ON mat.atividade_id = atv.id
WHERE atv.ordem_servico_id = ?
GROUP BY atv.ordem_servico_id
) m ON m.ordem_servico_id = os.id
LEFT JOIN (
SELECT
ordem_servico_id,
SUM(valor) AS valor_pago
FROM manut_ordem_servico_pagamentos
WHERE ordem_servico_id = ?
AND cancelado_em IS NULL
GROUP BY ordem_servico_id
) p ON p.ordem_servico_id = os.id
WHERE os.id = ?
AND os.excluido_em IS NULL
LIMIT 1;
`,
[ordemServicoId, ordemServicoId, ordemServicoId, ordemServicoId],
connection
);
if (!rows.length) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
const data = rows[0];
const valorTotal = Math.max(
0,
Number(data.valor_mao_obra) +
Number(data.valor_insumos) +
Number(data.valor_acrescimo) -
Number(data.valor_desconto)
);
const valorPago = Number(data.valor_pago);
let statusPagamento = data.status_pagamento;
if (!['CORTESIA', 'NAO_APLICAVEL'].includes(statusPagamento)) {
if (valorPago <= 0) {
statusPagamento = 'PENDENTE';
} else if (valorPago + 0.005 < valorTotal) {
statusPagamento = 'PARCIAL';
} else {
statusPagamento = 'RECEBIDO';
}
}
await dbQuery(
`
UPDATE manut_ordens_servico
SET
total_horas_expediente = ?,
total_horas_extras = ?,
valor_mao_obra = ?,
custo_insumos = ?,
valor_insumos = ?,
valor_total = ?,
valor_pago = ?,
status_pagamento = ?
WHERE id = ?;
`,
[
data.total_horas_expediente,
data.total_horas_extras,
data.valor_mao_obra,
data.custo_insumos,
data.valor_insumos,
valorTotal,
valorPago,
statusPagamento,
ordemServicoId
],
connection
);
return {
total_horas_expediente: Number(data.total_horas_expediente),
total_horas_extras: Number(data.total_horas_extras),
valor_mao_obra: Number(data.valor_mao_obra),
custo_insumos: Number(data.custo_insumos),
valor_insumos: Number(data.valor_insumos),
valor_total: valorTotal,
valor_pago: valorPago,
status_pagamento: statusPagamento
};
}
async function insertActivityTechnicians(connection, atividadeId, tecnicos) {
if (!Array.isArray(tecnicos) || !tecnicos.length) {
return;
}
const uniqueTechnicians = new Map();
for (const item of tecnicos) {
const tecnicoId = positiveInteger(item.tecnico_id, 'Técnico');
uniqueTechnicians.set(tecnicoId, {
tecnicoId,
papel: enumValue(item.papel, PAPEIS_TECNICO, 'Papel do técnico', 'EXECUTOR'),
horasExpediente: item.horas_trabalhadas_expediente === null || item.horas_trabalhadas_expediente === undefined
? null
: nonNegativeNumber(item.horas_trabalhadas_expediente, 'Horas trabalhadas em expediente'),
horasExtras: item.horas_trabalhadas_extras === null || item.horas_trabalhadas_extras === undefined
? null
: nonNegativeNumber(item.horas_trabalhadas_extras, 'Horas trabalhadas extras')
});
}
for (const item of uniqueTechnicians.values()) {
await dbQuery(
`
INSERT INTO manut_ordem_servico_atividade_tecnicos (
atividade_id,
tecnico_id,
papel,
horas_trabalhadas_expediente,
horas_trabalhadas_extras
)
VALUES (?, ?, ?, ?, ?);
`,
[
atividadeId,
item.tecnicoId,
item.papel,
item.horasExpediente,
item.horasExtras
],
connection
);
}
}
async function resolveMaterialSnapshot(connection, material) {
const materialId = material.material_id
? positiveInteger(material.material_id, 'Material')
: null;
let catalogMaterial = null;
if (materialId) {
const { results } = await dbQuery(
`
SELECT id, descricao, unidade, custo_padrao, preco_venda_padrao
FROM manut_materiais
WHERE id = ?
AND ativo = 1
LIMIT 1;
`,
[materialId],
connection
);
if (!results.length) {
throw createHttpError(404, `Material ${materialId} não encontrado ou inativo.`);
}
catalogMaterial = results[0];
}
return {
materialId,
descricao: optionalText(material.descricao_snapshot) ||
(catalogMaterial ? catalogMaterial.descricao : null),
unidade: optionalText(material.unidade_snapshot) ||
(catalogMaterial ? catalogMaterial.unidade : 'UN'),
quantidade: nonNegativeNumber(material.quantidade, 'Quantidade', 1),
custoUnitario: nonNegativeNumber(
material.valor_custo_unitario,
'Valor de custo unitário',
catalogMaterial ? Number(catalogMaterial.custo_padrao) : 0
),
cobradoUnitario: nonNegativeNumber(
material.valor_cobrado_unitario,
'Valor cobrado unitário',
catalogMaterial ? Number(catalogMaterial.preco_venda_padrao) : 0
),
cobravel: booleanToTinyInt(material.cobravel, true),
observacoes: optionalText(material.observacoes)
};
}
async function insertActivityMaterials(connection, atividadeId, materiais) {
if (!Array.isArray(materiais) || !materiais.length) {
return;
}
for (const material of materiais) {
const item = await resolveMaterialSnapshot(connection, material);
if (!item.descricao) {
throw createHttpError(400, 'A descrição do material é obrigatória.');
}
if (item.quantidade <= 0) {
throw createHttpError(400, 'A quantidade do material deve ser maior que zero.');
}
await dbQuery(
`
INSERT INTO manut_ordem_servico_atividade_materiais (
atividade_id,
material_id,
descricao_snapshot,
unidade_snapshot,
quantidade,
valor_custo_unitario,
valor_cobrado_unitario,
cobravel,
observacoes
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
`,
[
atividadeId,
item.materialId,
item.descricao,
item.unidade,
item.quantidade,
item.custoUnitario,
item.cobradoUnitario,
item.cobravel,
item.observacoes
],
connection
);
}
}
async function getOrderIdByActivity(connection, atividadeId) {
const { results } = await dbQuery(
`
SELECT ordem_servico_id
FROM manut_ordem_servico_atividades
WHERE id = ?
LIMIT 1;
`,
[atividadeId],
connection
);
if (!results.length) {
throw createHttpError(404, 'Atividade não encontrada.');
}
return results[0].ordem_servico_id;
}
var routes = function () {
// ==========================================================
// LOGS EXISTENTES
// ==========================================================
router.get('/getAllLogs', async function (req, res) {
try {
const { results } = await dbQuery(`
SELECT *
FROM vw_oriontard_logs
WHERE dataLog > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 MONTH)
ORDER BY dataLog DESC;
`);
return sendSuccess(res, results);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.post('/insertLog', async function (req, res) {
try {
const cpf = optionalText(req.body.cpf);
const perfil = optionalText(req.body.perfil);
const log = requiredText(req.body.log, 'Log');
// Mantém a compatibilidade histórica com a view atual.
const latitude = req.body.longitude ?? null;
const longitude = req.body.latitude ?? null;
const { results } = await dbQuery(
`
INSERT INTO orionTardProLogs (
cpf,
perfil,
latitude,
longitude,
log
)
VALUES (?, ?, ?, ?, ?);
`,
[cpf, perfil, latitude, longitude, log]
);
return sendSuccess(res, { id: results.insertId }, 201);
} catch (error) {
return handleRouteError(req, res, error);
}
});
// ==========================================================
// CONSULTAS AUXILIARES PARA COMBOS DO ANGULAR
// Cadastro continua sendo feito diretamente no banco por agora.
// ==========================================================
router.get('/getClientesManutencao', async function (req, res) {
try {
const { results } = await dbQuery(`
SELECT id, nome_fantasia, razao_social, documento
FROM manut_clientes
WHERE ativo = 1
ORDER BY nome_fantasia;
`);
return sendSuccess(res, results);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.get('/getEquipamentosManutencao', async function (req, res) {
try {
const { results } = await dbQuery(`
SELECT
e.id,
e.cliente_id,
e.modelo_id,
e.codigo_interno,
e.nome,
e.numero_serie,
e.versao,
e.odometro_total_segundos,
e.odometro_conectado_segundos,
e.odometro_movimento_segundos,
e.odometro_parcial_segundos,
e.odometro_atualizado_em,
m.nome AS modelo_nome,
c.nome_fantasia AS cliente_nome
FROM manut_equipamentos e
LEFT JOIN manut_equipamento_modelos m ON m.id = e.modelo_id
LEFT JOIN manut_clientes c ON c.id = e.cliente_id
WHERE e.ativo = 1
ORDER BY e.nome, e.numero_serie;
`);
return sendSuccess(res, results);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.get('/getTecnicosManutencao', async function (req, res) {
try {
const { results } = await dbQuery(`
SELECT
id,
usuario_sistema_id,
nome,
especialidade,
valor_hora_expediente_padrao,
valor_hora_extra_padrao
FROM manut_tecnicos
WHERE ativo = 1
ORDER BY nome;
`);
return sendSuccess(res, results);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.get('/getMateriaisManutencao', async function (req, res) {
try {
const { results } = await dbQuery(`
SELECT
id,
codigo,
descricao,
tipo,
unidade,
custo_padrao,
preco_venda_padrao
FROM manut_materiais
WHERE ativo = 1
ORDER BY descricao;
`);
return sendSuccess(res, results);
} catch (error) {
return handleRouteError(req, res, error);
}
});
// ==========================================================
// ORDENS DE SERVICO
// ==========================================================
router.get('/getOrdensServico', async function (req, res) {
try {
const { page, pageSize, offset } = sanitizePagination(req.query);
const where = ['os.excluido_em IS NULL'];
const params = [];
if (req.query.status) {
where.push('os.status = ?');
params.push(enumValue(req.query.status, STATUS_OS, 'Status'));
}
if (req.query.status_pagamento) {
where.push('os.status_pagamento = ?');
params.push(enumValue(
req.query.status_pagamento,
STATUS_PAGAMENTO,
'Status do pagamento'
));
}
if (req.query.tipo) {
where.push('os.tipo = ?');
params.push(enumValue(req.query.tipo, TIPOS_OS, 'Tipo da OS'));
}
if (req.query.equipamento_id) {
where.push('os.equipamento_id = ?');
params.push(positiveInteger(req.query.equipamento_id, 'Equipamento'));
}
if (req.query.cliente_id) {
where.push('os.cliente_id = ?');
params.push(positiveInteger(req.query.cliente_id, 'Cliente'));
}
if (req.query.data_inicio) {
where.push('os.data_entrada >= ?');
params.push(req.query.data_inicio);
}
if (req.query.data_fim) {
where.push('os.data_entrada < DATE_ADD(?, INTERVAL 1 DAY)');
params.push(req.query.data_fim);
}
if (req.query.busca) {
const search = `%${String(req.query.busca).trim()}%`;
where.push(`(
CAST(os.id AS CHAR) LIKE ? OR
os.equipamento_nome_snapshot LIKE ? OR
os.numero_serie_snapshot LIKE ? OR
os.problema_relatado LIKE ? OR
os.cliente_nome_snapshot LIKE ?
)`);
params.push(search, search, search, search, search);
}
const whereSql = where.join(' AND ');
const { results: countRows } = await dbQuery(
`
SELECT COUNT(*) AS total
FROM manut_ordens_servico os
WHERE ${whereSql};
`,
params
);
const listParams = [...params, pageSize, offset];
const { results } = await dbQuery(
`
SELECT
os.id,
os.cliente_id,
os.equipamento_id,
os.tecnico_responsavel_id,
os.cliente_nome_snapshot,
os.equipamento_nome_snapshot,
os.numero_serie_snapshot,
os.modelo_snapshot,
os.versao_snapshot,
os.tipo,
os.status,
os.status_pagamento,
os.prioridade,
os.data_entrada,
os.data_previsao,
os.data_finalizacao,
os.data_retirada,
os.problema_relatado,
os.total_horas_expediente,
os.total_horas_extras,
os.valor_mao_obra,
os.valor_insumos,
os.valor_desconto,
os.valor_acrescimo,
os.valor_total,
os.valor_pago,
GREATEST(os.valor_total - os.valor_pago, 0) AS valor_pendente,
t.nome AS tecnico_responsavel_nome,
os.created_at,
os.updated_at
FROM manut_ordens_servico os
LEFT JOIN manut_tecnicos t ON t.id = os.tecnico_responsavel_id
WHERE ${whereSql}
ORDER BY os.data_entrada DESC, os.id DESC
LIMIT ? OFFSET ?;
`,
listParams
);
return sendSuccess(res, {
items: results,
pagination: {
page,
pageSize,
total: Number(countRows[0].total),
totalPages: Math.ceil(Number(countRows[0].total) / pageSize)
}
});
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.get('/getOrdemServico/:id', async function (req, res) {
try {
const id = positiveInteger(req.params.id, 'Ordem de serviço');
const [
{ results: orders },
{ results: activities },
{ results: technicians },
{ results: materials },
{ results: odometers },
{ results: payments },
{ results: statusHistory }
] = await Promise.all([
dbQuery(
`
SELECT
os.*,
t.nome AS tecnico_responsavel_nome,
GREATEST(os.valor_total - os.valor_pago, 0) AS valor_pendente
FROM manut_ordens_servico os
LEFT JOIN manut_tecnicos t ON t.id = os.tecnico_responsavel_id
WHERE os.id = ?
AND os.excluido_em IS NULL
LIMIT 1;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_ordem_servico_atividades
WHERE ordem_servico_id = ?
ORDER BY data_atividade, ordem_exibicao, id;
`,
[id]
),
dbQuery(
`
SELECT
at.atividade_id,
at.tecnico_id,
at.papel,
at.horas_trabalhadas_expediente,
at.horas_trabalhadas_extras,
t.nome AS tecnico_nome
FROM manut_ordem_servico_atividade_tecnicos at
INNER JOIN manut_tecnicos t ON t.id = at.tecnico_id
INNER JOIN manut_ordem_servico_atividades a ON a.id = at.atividade_id
WHERE a.ordem_servico_id = ?
ORDER BY t.nome;
`,
[id]
),
dbQuery(
`
SELECT mat.*
FROM manut_ordem_servico_atividade_materiais mat
INNER JOIN manut_ordem_servico_atividades a ON a.id = mat.atividade_id
WHERE a.ordem_servico_id = ?
ORDER BY mat.id;
`,
[id]
),
dbQuery(
`
SELECT
l.*,
LAG(l.odometro_total_segundos) OVER (
PARTITION BY l.equipamento_id ORDER BY l.leitura_em, l.id
) AS odometro_total_anterior,
LAG(l.odometro_conectado_segundos) OVER (
PARTITION BY l.equipamento_id ORDER BY l.leitura_em, l.id
) AS odometro_conectado_anterior,
LAG(l.odometro_movimento_segundos) OVER (
PARTITION BY l.equipamento_id ORDER BY l.leitura_em, l.id
) AS odometro_movimento_anterior
FROM manut_equipamento_odometro_leituras l
WHERE l.equipamento_id = (
SELECT equipamento_id
FROM manut_ordens_servico
WHERE id = ?
)
ORDER BY l.leitura_em, l.id;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_ordem_servico_pagamentos
WHERE ordem_servico_id = ?
ORDER BY data_pagamento, id;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_ordem_servico_historico_status
WHERE ordem_servico_id = ?
ORDER BY created_at, id;
`,
[id]
)
]);
if (!orders.length) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
const techniciansByActivity = new Map();
for (const item of technicians) {
if (!techniciansByActivity.has(item.atividade_id)) {
techniciansByActivity.set(item.atividade_id, []);
}
techniciansByActivity.get(item.atividade_id).push(item);
}
const materialsByActivity = new Map();
for (const item of materials) {
if (!materialsByActivity.has(item.atividade_id)) {
materialsByActivity.set(item.atividade_id, []);
}
materialsByActivity.get(item.atividade_id).push(item);
}
const nestedActivities = activities.map(activity => ({
...activity,
tecnicos: techniciansByActivity.get(activity.id) || [],
materiais: materialsByActivity.get(activity.id) || []
}));
const orderOdometers = odometers.filter(item => Number(item.ordem_servico_id) === id);
const entryOdometer = orderOdometers.find(item => item.tipo_leitura === 'ENTRADA_MANUTENCAO') || null;
let intervalSincePreviousMaintenance = null;
if (entryOdometer) {
intervalSincePreviousMaintenance = {
total_segundos: entryOdometer.odometro_total_anterior === null
? null
: Number(entryOdometer.odometro_total_segundos) - Number(entryOdometer.odometro_total_anterior),
conectado_segundos: entryOdometer.odometro_conectado_anterior === null
? null
: Number(entryOdometer.odometro_conectado_segundos) - Number(entryOdometer.odometro_conectado_anterior),
movimento_segundos: entryOdometer.odometro_movimento_anterior === null
? null
: Number(entryOdometer.odometro_movimento_segundos) - Number(entryOdometer.odometro_movimento_anterior)
};
}
return sendSuccess(res, {
...orders[0],
atividades: nestedActivities,
odometros_da_os: orderOdometers,
intervalo_desde_leitura_anterior: intervalSincePreviousMaintenance,
pagamentos: payments,
historico_status: statusHistory
});
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.post('/insertOrdemServico', async function (req, res) {
try {
const result = await withTransaction(async connection => {
const body = req.body || {};
const status = enumValue(body.status, STATUS_OS, 'Status', 'ABERTA');
const tipo = enumValue(body.tipo, TIPOS_OS, 'Tipo', 'CORRETIVA');
const prioridade = enumValue(body.prioridade, PRIORIDADES, 'Prioridade', 'NORMAL');
let statusPagamentoPadrao = 'PENDENTE';
if (tipo === 'CORTESIA') {
statusPagamentoPadrao = 'CORTESIA';
} else if (tipo === 'GARANTIA') {
statusPagamentoPadrao = 'NAO_APLICAVEL';
}
const statusPagamento = enumValue(
body.status_pagamento,
STATUS_PAGAMENTO,
'Status do pagamento',
statusPagamentoPadrao
);
const equipamentoId = body.equipamento_id
? positiveInteger(body.equipamento_id, 'Equipamento')
: null;
const clienteIdInformado = body.cliente_id
? positiveInteger(body.cliente_id, 'Cliente')
: null;
const tecnicoResponsavelId = body.tecnico_responsavel_id
? positiveInteger(body.tecnico_responsavel_id, 'Técnico responsável')
: null;
const usuarioId = body.usuario_id
? positiveInteger(body.usuario_id, 'Usuário')
: null;
const problemaRelatado = status === 'RASCUNHO'
? optionalText(body.problema_relatado)
: requiredText(body.problema_relatado, 'Problema relatado');
let snapshot = {
clienteId: clienteIdInformado,
clienteNome: null,
equipamento: null
};
if (equipamentoId) {
snapshot = await getEquipmentSnapshot(
connection,
equipamentoId,
clienteIdInformado
);
} else if (clienteIdInformado) {
const { results: clientes } = await dbQuery(
`
SELECT id, nome_fantasia
FROM manut_clientes
WHERE id = ? AND ativo = 1
LIMIT 1;
`,
[clienteIdInformado],
connection
);
if (!clientes.length) {
throw createHttpError(404, 'Cliente não encontrado ou inativo.');
}
snapshot.clienteNome = clientes[0].nome_fantasia;
}
const odometroEntrada = parseOdometer(body.odometro_entrada);
if (equipamentoId && status !== 'RASCUNHO' && !odometroEntrada) {
throw createHttpError(
400,
'Informe o odômetro de entrada do equipamento.'
);
}
if (!equipamentoId && odometroEntrada) {
throw createHttpError(
400,
'Não é possível registrar odômetro sem selecionar um equipamento.'
);
}
const { results: insertResult } = await dbQuery(
`
INSERT INTO manut_ordens_servico (
cliente_id,
equipamento_id,
tecnico_responsavel_id,
cliente_nome_snapshot,
equipamento_nome_snapshot,
numero_serie_snapshot,
modelo_snapshot,
versao_snapshot,
tipo,
status,
status_pagamento,
prioridade,
data_entrada,
data_previsao,
data_finalizacao,
data_retirada,
problema_relatado,
diagnostico,
solucao_resumo,
responsavel_entrega,
responsavel_retirada,
observacoes_internas,
observacoes_cliente,
garantia_dias,
garantia_ate,
valor_desconto,
valor_acrescimo,
justificativa_ajuste_valor,
origem,
criado_por_usuario_id,
atualizado_por_usuario_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'SISTEMA', ?, ?);
`,
[
snapshot.clienteId,
equipamentoId,
tecnicoResponsavelId,
snapshot.clienteNome,
snapshot.equipamento ? snapshot.equipamento.nome : optionalText(body.equipamento_nome),
snapshot.equipamento ? snapshot.equipamento.numero_serie : optionalText(body.numero_serie),
snapshot.equipamento ? snapshot.equipamento.modelo_nome : optionalText(body.modelo),
snapshot.equipamento ? snapshot.equipamento.versao : optionalText(body.versao),
tipo,
status,
statusPagamento,
prioridade,
normalizeNullableDate(body.data_entrada),
normalizeNullableDate(body.data_previsao),
normalizeNullableDate(body.data_finalizacao),
normalizeNullableDate(body.data_retirada),
problemaRelatado,
optionalText(body.diagnostico),
optionalText(body.solucao_resumo),
optionalText(body.responsavel_entrega),
optionalText(body.responsavel_retirada),
optionalText(body.observacoes_internas),
optionalText(body.observacoes_cliente),
body.garantia_dias === null || body.garantia_dias === undefined
? null
: nonNegativeInteger(body.garantia_dias, 'Garantia em dias'),
normalizeNullableDate(body.garantia_ate),
nonNegativeNumber(body.valor_desconto, 'Valor de desconto'),
nonNegativeNumber(body.valor_acrescimo, 'Valor de acréscimo'),
optionalText(body.justificativa_ajuste_valor),
usuarioId,
usuarioId
],
connection
);
const ordemServicoId = insertResult.insertId;
await dbQuery(
`
INSERT INTO manut_ordem_servico_historico_status (
ordem_servico_id,
status_anterior,
status_novo,
observacoes,
alterado_por_usuario_id
)
VALUES (?, NULL, ?, 'Ordem de serviço criada.', ?);
`,
[ordemServicoId, status, usuarioId],
connection
);
if (snapshot.equipamento && odometroEntrada) {
await insertOdometerReading(connection, {
equipamento: snapshot.equipamento,
ordemServicoId,
odometro: odometroEntrada,
tipoLeitura: 'ENTRADA_MANUTENCAO',
usuarioId
});
}
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: ordemServicoId,
...totals
};
});
return sendSuccess(res, result, 201);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.put('/updateOrdemServico/:id', async function (req, res) {
try {
const id = positiveInteger(req.params.id, 'Ordem de serviço');
const result = await withTransaction(async connection => {
const { results: currentRows } = await dbQuery(
`
SELECT *
FROM manut_ordens_servico
WHERE id = ?
AND excluido_em IS NULL
FOR UPDATE;
`,
[id],
connection
);
if (!currentRows.length) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
const current = currentRows[0];
const body = req.body || {};
if (
body.equipamento_id !== undefined &&
Number(body.equipamento_id) !== Number(current.equipamento_id)
) {
throw createHttpError(
409,
'O equipamento da OS não pode ser alterado nesta versão. Exclua a OS em rascunho e crie outra.'
);
}
const fields = [];
const params = [];
function setField(column, value) {
fields.push(`${column} = ?`);
params.push(value);
}
if (body.tecnico_responsavel_id !== undefined) {
setField(
'tecnico_responsavel_id',
body.tecnico_responsavel_id
? positiveInteger(body.tecnico_responsavel_id, 'Técnico responsável')
: null
);
}
if (body.tipo !== undefined) {
setField('tipo', enumValue(body.tipo, TIPOS_OS, 'Tipo'));
}
let newStatus = current.status;
if (body.status !== undefined) {
newStatus = enumValue(body.status, STATUS_OS, 'Status');
setField('status', newStatus);
}
if (body.status_pagamento !== undefined) {
setField(
'status_pagamento',
enumValue(body.status_pagamento, STATUS_PAGAMENTO, 'Status do pagamento')
);
}
if (body.prioridade !== undefined) {
setField('prioridade', enumValue(body.prioridade, PRIORIDADES, 'Prioridade'));
}
const plainFields = [
['data_entrada', 'data_entrada', normalizeNullableDate],
['data_previsao', 'data_previsao', normalizeNullableDate],
['data_finalizacao', 'data_finalizacao', normalizeNullableDate],
['data_retirada', 'data_retirada', normalizeNullableDate],
['problema_relatado', 'problema_relatado', optionalText],
['diagnostico', 'diagnostico', optionalText],
['solucao_resumo', 'solucao_resumo', optionalText],
['responsavel_entrega', 'responsavel_entrega', optionalText],
['responsavel_retirada', 'responsavel_retirada', optionalText],
['observacoes_internas', 'observacoes_internas', optionalText],
['observacoes_cliente', 'observacoes_cliente', optionalText],
['garantia_ate', 'garantia_ate', normalizeNullableDate],
['justificativa_ajuste_valor', 'justificativa_ajuste_valor', optionalText]
];
for (const [bodyField, column, transformer] of plainFields) {
if (body[bodyField] !== undefined) {
setField(column, transformer(body[bodyField]));
}
}
if (body.garantia_dias !== undefined) {
setField(
'garantia_dias',
body.garantia_dias === null
? null
: nonNegativeInteger(body.garantia_dias, 'Garantia em dias')
);
}
if (body.valor_desconto !== undefined) {
setField(
'valor_desconto',
nonNegativeNumber(body.valor_desconto, 'Valor de desconto')
);
}
if (body.valor_acrescimo !== undefined) {
setField(
'valor_acrescimo',
nonNegativeNumber(body.valor_acrescimo, 'Valor de acréscimo')
);
}
const usuarioId = body.usuario_id
? positiveInteger(body.usuario_id, 'Usuário')
: null;
if (newStatus !== 'RASCUNHO') {
const finalProblem = body.problema_relatado !== undefined
? optionalText(body.problema_relatado)
: optionalText(current.problema_relatado);
if (!finalProblem) {
throw createHttpError(400, 'Problema relatado é obrigatório fora de rascunho.');
}
}
if (newStatus === 'FINALIZADA' && !body.data_finalizacao && !current.data_finalizacao) {
fields.push('data_finalizacao = CURRENT_TIMESTAMP');
}
if (newStatus === 'RETIRADA' && !body.data_retirada && !current.data_retirada) {
fields.push('data_retirada = CURRENT_TIMESTAMP');
}
if (fields.length) {
setField('atualizado_por_usuario_id', usuarioId);
params.push(id);
await dbQuery(
`
UPDATE manut_ordens_servico
SET ${fields.join(', ')}
WHERE id = ?;
`,
params,
connection
);
}
if (newStatus !== current.status) {
await dbQuery(
`
INSERT INTO manut_ordem_servico_historico_status (
ordem_servico_id,
status_anterior,
status_novo,
observacoes,
alterado_por_usuario_id
)
VALUES (?, ?, ?, ?, ?);
`,
[
id,
current.status,
newStatus,
optionalText(body.observacao_status),
usuarioId
],
connection
);
}
const totals = await recalculateOrderTotals(connection, id);
return { id, ...totals };
});
return sendSuccess(res, result);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.delete('/deleteOrdemServico/:id', async function (req, res) {
try {
const id = positiveInteger(req.params.id, 'Ordem de serviço');
const usuarioId = req.body && req.body.usuario_id
? positiveInteger(req.body.usuario_id, 'Usuário')
: null;
const { results } = await dbQuery(
`
UPDATE manut_ordens_servico
SET
excluido_em = CURRENT_TIMESTAMP,
excluido_por_usuario_id = ?,
atualizado_por_usuario_id = ?
WHERE id = ?
AND excluido_em IS NULL;
`,
[usuarioId, usuarioId, id]
);
if (!results.affectedRows) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
return sendSuccess(res, { id, excluida: true });
} catch (error) {
return handleRouteError(req, res, error);
}
});
// ==========================================================
// ATIVIDADES DA OS
// ==========================================================
router.post('/insertAtividadeOrdemServico/:ordemServicoId', async function (req, res) {
try {
const ordemServicoId = positiveInteger(
req.params.ordemServicoId,
'Ordem de serviço'
);
const result = await withTransaction(async connection => {
const { results: orders } = await dbQuery(
`
SELECT id
FROM manut_ordens_servico
WHERE id = ?
AND excluido_em IS NULL
LIMIT 1;
`,
[ordemServicoId],
connection
);
if (!orders.length) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
const body = req.body || {};
const { results: orderRows } = await dbQuery(
`
SELECT COALESCE(MAX(ordem_exibicao), 0) + 1 AS proxima_ordem
FROM manut_ordem_servico_atividades
WHERE ordem_servico_id = ?;
`,
[ordemServicoId],
connection
);
const usuarioId = body.usuario_id
? positiveInteger(body.usuario_id, 'Usuário')
: null;
const { results: insertResult } = await dbQuery(
`
INSERT INTO manut_ordem_servico_atividades (
ordem_servico_id,
data_atividade,
descricao,
ordem_exibicao,
horas_expediente,
horas_extras,
valor_hora_expediente,
valor_hora_extra,
cobravel,
observacoes_internas,
observacoes_cliente,
criado_por_usuario_id,
atualizado_por_usuario_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`,
[
ordemServicoId,
body.data_atividade || new Date(),
requiredText(body.descricao, 'Descrição da atividade'),
body.ordem_exibicao === undefined
? orderRows[0].proxima_ordem
: nonNegativeInteger(body.ordem_exibicao, 'Ordem de exibição'),
nonNegativeNumber(body.horas_expediente, 'Horas de expediente'),
nonNegativeNumber(body.horas_extras, 'Horas extras'),
nonNegativeNumber(body.valor_hora_expediente, 'Valor da hora de expediente'),
nonNegativeNumber(body.valor_hora_extra, 'Valor da hora extra'),
booleanToTinyInt(body.cobravel, true),
optionalText(body.observacoes_internas),
optionalText(body.observacoes_cliente),
usuarioId,
usuarioId
],
connection
);
const atividadeId = insertResult.insertId;
await insertActivityTechnicians(connection, atividadeId, body.tecnicos);
await insertActivityMaterials(connection, atividadeId, body.materiais);
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: atividadeId,
ordem_servico_id: ordemServicoId,
totais_os: totals
};
});
return sendSuccess(res, result, 201);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.put('/updateAtividadeOrdemServico/:atividadeId', async function (req, res) {
try {
const atividadeId = positiveInteger(req.params.atividadeId, 'Atividade');
const result = await withTransaction(async connection => {
const ordemServicoId = await getOrderIdByActivity(connection, atividadeId);
const body = req.body || {};
const fields = [];
const params = [];
function setField(column, value) {
fields.push(`${column} = ?`);
params.push(value);
}
const simpleFields = [
['data_atividade', 'data_atividade', value => value],
['descricao', 'descricao', value => requiredText(value, 'Descrição da atividade')],
['observacoes_internas', 'observacoes_internas', optionalText],
['observacoes_cliente', 'observacoes_cliente', optionalText]
];
for (const [bodyField, column, transformer] of simpleFields) {
if (body[bodyField] !== undefined) {
setField(column, transformer(body[bodyField]));
}
}
const numericFields = [
['ordem_exibicao', 'ordem_exibicao', true],
['horas_expediente', 'horas_expediente', false],
['horas_extras', 'horas_extras', false],
['valor_hora_expediente', 'valor_hora_expediente', false],
['valor_hora_extra', 'valor_hora_extra', false]
];
for (const [bodyField, column, integer] of numericFields) {
if (body[bodyField] !== undefined) {
setField(
column,
integer
? nonNegativeInteger(body[bodyField], bodyField)
: nonNegativeNumber(body[bodyField], bodyField)
);
}
}
if (body.cobravel !== undefined) {
setField('cobravel', booleanToTinyInt(body.cobravel, true));
}
if (body.usuario_id !== undefined) {
setField(
'atualizado_por_usuario_id',
body.usuario_id
? positiveInteger(body.usuario_id, 'Usuário')
: null
);
}
if (fields.length) {
params.push(atividadeId);
await dbQuery(
`
UPDATE manut_ordem_servico_atividades
SET ${fields.join(', ')}
WHERE id = ?;
`,
params,
connection
);
}
if (Array.isArray(body.tecnicos)) {
await dbQuery(
`DELETE FROM manut_ordem_servico_atividade_tecnicos WHERE atividade_id = ?;`,
[atividadeId],
connection
);
await insertActivityTechnicians(connection, atividadeId, body.tecnicos);
}
if (Array.isArray(body.materiais)) {
await dbQuery(
`DELETE FROM manut_ordem_servico_atividade_materiais WHERE atividade_id = ?;`,
[atividadeId],
connection
);
await insertActivityMaterials(connection, atividadeId, body.materiais);
}
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: atividadeId,
ordem_servico_id: ordemServicoId,
totais_os: totals
};
});
return sendSuccess(res, result);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.delete('/deleteAtividadeOrdemServico/:atividadeId', async function (req, res) {
try {
const atividadeId = positiveInteger(req.params.atividadeId, 'Atividade');
const result = await withTransaction(async connection => {
const ordemServicoId = await getOrderIdByActivity(connection, atividadeId);
await dbQuery(
`DELETE FROM manut_ordem_servico_atividades WHERE id = ?;`,
[atividadeId],
connection
);
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: atividadeId,
excluida: true,
ordem_servico_id: ordemServicoId,
totais_os: totals
};
});
return sendSuccess(res, result);
} catch (error) {
return handleRouteError(req, res, error);
}
});
// ==========================================================
// ODOMETROS
// ==========================================================
router.post('/insertOdometroOrdemServico/:ordemServicoId', async function (req, res) {
try {
const ordemServicoId = positiveInteger(
req.params.ordemServicoId,
'Ordem de serviço'
);
const result = await withTransaction(async connection => {
const { results: orders } = await dbQuery(
`
SELECT equipamento_id
FROM manut_ordens_servico
WHERE id = ?
AND excluido_em IS NULL
LIMIT 1;
`,
[ordemServicoId],
connection
);
if (!orders.length) {
throw createHttpError(404, 'Ordem de serviço não encontrada.');
}
if (!orders[0].equipamento_id) {
throw createHttpError(409, 'A OS não possui equipamento associado.');
}
const { equipamento } = await getEquipmentSnapshot(
connection,
orders[0].equipamento_id
);
const odometro = parseOdometer(req.body);
if (!odometro) {
throw createHttpError(400, 'Informe os dados do odômetro.');
}
const tipoLeitura = enumValue(
req.body.tipo_leitura,
TIPOS_ODOMETRO,
'Tipo de leitura',
'SAIDA_MANUTENCAO'
);
const usuarioId = req.body.usuario_id
? positiveInteger(req.body.usuario_id, 'Usuário')
: null;
await insertOdometerReading(connection, {
equipamento,
ordemServicoId,
odometro,
tipoLeitura,
usuarioId
});
return {
ordem_servico_id: ordemServicoId,
equipamento_id: equipamento.id,
tipo_leitura: tipoLeitura
};
});
return sendSuccess(res, result, 201);
} catch (error) {
return handleRouteError(req, res, error);
}
});
// ==========================================================
// PAGAMENTOS
// ==========================================================
router.post('/insertPagamentoOrdemServico/:ordemServicoId', async function (req, res) {
try {
const ordemServicoId = positiveInteger(
req.params.ordemServicoId,
'Ordem de serviço'
);
const result = await withTransaction(async connection => {
const valor = nonNegativeNumber(req.body.valor, 'Valor do pagamento');
if (valor <= 0) {
throw createHttpError(400, 'O valor do pagamento deve ser maior que zero.');
}
const formaPagamento = enumValue(
req.body.forma_pagamento,
FORMAS_PAGAMENTO,
'Forma de pagamento',
'PIX'
);
const usuarioId = req.body.usuario_id
? positiveInteger(req.body.usuario_id, 'Usuário')
: null;
const { results: insertResult } = await dbQuery(
`
INSERT INTO manut_ordem_servico_pagamentos (
ordem_servico_id,
data_pagamento,
valor,
forma_pagamento,
referencia,
observacoes,
registrado_por_usuario_id
)
VALUES (?, COALESCE(?, CURRENT_TIMESTAMP), ?, ?, ?, ?, ?);
`,
[
ordemServicoId,
normalizeNullableDate(req.body.data_pagamento),
valor,
formaPagamento,
optionalText(req.body.referencia),
optionalText(req.body.observacoes),
usuarioId
],
connection
);
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: insertResult.insertId,
ordem_servico_id: ordemServicoId,
totais_os: totals
};
});
return sendSuccess(res, result, 201);
} catch (error) {
return handleRouteError(req, res, error);
}
});
router.delete('/cancelPagamentoOrdemServico/:pagamentoId', async function (req, res) {
try {
const pagamentoId = positiveInteger(req.params.pagamentoId, 'Pagamento');
const result = await withTransaction(async connection => {
const { results: paymentRows } = await dbQuery(
`
SELECT ordem_servico_id
FROM manut_ordem_servico_pagamentos
WHERE id = ?
AND cancelado_em IS NULL
LIMIT 1;
`,
[pagamentoId],
connection
);
if (!paymentRows.length) {
throw createHttpError(404, 'Pagamento não encontrado ou já cancelado.');
}
const ordemServicoId = paymentRows[0].ordem_servico_id;
const usuarioId = req.body && req.body.usuario_id
? positiveInteger(req.body.usuario_id, 'Usuário')
: null;
await dbQuery(
`
UPDATE manut_ordem_servico_pagamentos
SET
cancelado_em = CURRENT_TIMESTAMP,
cancelado_por_usuario_id = ?
WHERE id = ?;
`,
[usuarioId, pagamentoId],
connection
);
const totals = await recalculateOrderTotals(connection, ordemServicoId);
return {
id: pagamentoId,
cancelado: true,
ordem_servico_id: ordemServicoId,
totais_os: totals
};
});
return sendSuccess(res, result);
} catch (error) {
return handleRouteError(req, res, error);
}
});
return router;
};
module.exports = routes;