2008 lines
57 KiB
JavaScript
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;
|