ajustado dashboard telemetria oriontardpro
This commit is contained in:
parent
445b7b5812
commit
59e0ab7e12
|
|
@ -24,6 +24,7 @@ const STATUS_OS = new Set([
|
|||
'EM_EXECUCAO',
|
||||
'EM_TESTES',
|
||||
'FINALIZADA',
|
||||
'AGUARDANDO_RETIRADA',
|
||||
'RETIRADA',
|
||||
'CANCELADA'
|
||||
]);
|
||||
|
|
@ -177,6 +178,27 @@ function handleRouteError(req, res, error) {
|
|||
return sendError(req, res, error, status, message);
|
||||
}
|
||||
|
||||
function parsePagination(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
|
||||
};
|
||||
}
|
||||
|
||||
function positiveInteger(value, fieldName, nullable = false) {
|
||||
if ((value === null || value === undefined || value === '') && nullable) {
|
||||
return null;
|
||||
|
|
@ -728,14 +750,87 @@ var routes = function () {
|
|||
// ==========================================================
|
||||
router.get('/getAllLogs', async function (req, res) {
|
||||
try {
|
||||
// A dashboard precisa enxergar o registro bruto independentemente da
|
||||
// versão do JSON. A view histórica continua sendo usada somente para
|
||||
// expor dataLog/filtro temporal; o conteúdo do campo log é normalizado
|
||||
// abaixo para manter compatibilidade entre os schemas antigos e o v2.
|
||||
const mesesSolicitados = Number.parseInt(req.query.meses, 10);
|
||||
const meses = Number.isFinite(mesesSolicitados)
|
||||
? Math.min(12, Math.max(1, mesesSolicitados))
|
||||
: 3;
|
||||
|
||||
// dataLog não é coluna física de orionTardProLogs. Ela é exposta
|
||||
// pela view histórica vw_oriontard_logs. Mantemos a view somente como
|
||||
// fonte SQL para paginação/filtro temporal e fazemos toda a
|
||||
// compatibilidade de schema 1/2 abaixo, a partir do campo log bruto.
|
||||
const { results } = await dbQuery(`
|
||||
SELECT *
|
||||
FROM vw_oriontard_logs
|
||||
WHERE dataLog > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 MONTH)
|
||||
WHERE dataLog > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ${meses} MONTH)
|
||||
ORDER BY dataLog DESC;
|
||||
`);
|
||||
|
||||
return sendSuccess(res, results);
|
||||
const normalizados = results.map(row => {
|
||||
let parsed = null;
|
||||
let jsonValido = false;
|
||||
|
||||
try {
|
||||
parsed = typeof row.log === 'string' ? JSON.parse(row.log) : row.log;
|
||||
if (typeof parsed === 'string') {
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
jsonValido = !!parsed && typeof parsed === 'object';
|
||||
} catch (error) {
|
||||
parsed = null;
|
||||
}
|
||||
|
||||
const latitudeJson = parsed ? Number(parsed.latitude) : NaN;
|
||||
const longitudeJson = parsed ? Number(parsed.longitude) : NaN;
|
||||
|
||||
// Os campos latitude/longitude da tabela possuem inversão histórica
|
||||
// por compatibilidade. Para JSON válido, o próprio payload é a fonte
|
||||
// de verdade. Para legado inválido, preservamos o fallback invertido.
|
||||
const latitude = Number.isFinite(latitudeJson)
|
||||
? latitudeJson
|
||||
: Number(row.longitude);
|
||||
const longitude = Number.isFinite(longitudeJson)
|
||||
? longitudeJson
|
||||
: Number(row.latitude);
|
||||
|
||||
const dataOperacao = parsed
|
||||
? (parsed.periodoFim || parsed.data || row.dataLog)
|
||||
: row.dataLog;
|
||||
|
||||
return {
|
||||
...row,
|
||||
nome: (parsed && (parsed.usuario || parsed.cpf)) || row.cpf || 'Não identificado',
|
||||
perfil: (parsed && parsed.perfil) || row.perfil || 'Sem perfil',
|
||||
coordenadas: Number.isFinite(latitude) && Number.isFinite(longitude)
|
||||
? `${latitude},${longitude}`
|
||||
: '',
|
||||
dataOperacao,
|
||||
schemaVersao: parsed && parsed.schemaVersao !== undefined
|
||||
? Number(parsed.schemaVersao)
|
||||
: null,
|
||||
tipoEvento: parsed ? (parsed.tipoEvento || null) : null,
|
||||
periodoInicio: parsed ? (parsed.periodoInicio || null) : null,
|
||||
periodoFim: parsed ? (parsed.periodoFim || null) : null,
|
||||
jsonValido
|
||||
};
|
||||
});
|
||||
|
||||
// dataLog representa a inserção no banco. Para logs reenviados após
|
||||
// ficar offline, a data real da operação pode ser anterior. Ordenamos
|
||||
// pela data do payload quando ela existe e usamos dataLog como fallback.
|
||||
normalizados.sort((a, b) => {
|
||||
const dataA = new Date(a.dataOperacao || a.dataLog).getTime();
|
||||
const dataB = new Date(b.dataOperacao || b.dataLog).getTime();
|
||||
const valorA = Number.isFinite(dataA) ? dataA : 0;
|
||||
const valorB = Number.isFinite(dataB) ? dataB : 0;
|
||||
return valorB - valorA;
|
||||
});
|
||||
|
||||
return sendSuccess(res, normalizados);
|
||||
} catch (error) {
|
||||
return handleRouteError(req, res, error);
|
||||
}
|
||||
|
|
@ -821,6 +916,551 @@ var routes = function () {
|
|||
}
|
||||
});
|
||||
|
||||
// ==========================================================
|
||||
// PRONTUARIO DOS EQUIPAMENTOS
|
||||
// ==========================================================
|
||||
router.get('/getEquipamentosHistorico', async function (req, res) {
|
||||
try {
|
||||
const { page, pageSize, offset } = parsePagination(req.query);
|
||||
const where = ['e.ativo = 1'];
|
||||
const params = [];
|
||||
|
||||
if (req.query.busca) {
|
||||
const busca = `%${String(req.query.busca).trim()}%`;
|
||||
where.push(`(
|
||||
e.numero_serie LIKE ? OR
|
||||
e.codigo_interno LIKE ? OR
|
||||
e.nome LIKE ? OR
|
||||
e.versao LIKE ? OR
|
||||
m.nome LIKE ? OR
|
||||
c.nome_fantasia LIKE ?
|
||||
)`);
|
||||
params.push(busca, busca, busca, busca, busca, busca);
|
||||
}
|
||||
|
||||
if (req.query.modelo_id) {
|
||||
where.push('e.modelo_id = ?');
|
||||
params.push(positiveInteger(req.query.modelo_id, 'Modelo'));
|
||||
}
|
||||
|
||||
if (req.query.modelo) {
|
||||
where.push('m.nome LIKE ?');
|
||||
params.push(`%${String(req.query.modelo).trim()}%`);
|
||||
}
|
||||
|
||||
if (req.query.cliente_id) {
|
||||
where.push('e.cliente_id = ?');
|
||||
params.push(positiveInteger(req.query.cliente_id, 'Cliente'));
|
||||
}
|
||||
|
||||
if (req.query.versao) {
|
||||
where.push('e.versao LIKE ?');
|
||||
params.push(`%${String(req.query.versao).trim()}%`);
|
||||
}
|
||||
|
||||
if (req.query.possui_os_aberta !== undefined) {
|
||||
const possuiOsAberta = String(req.query.possui_os_aberta).toLowerCase();
|
||||
if (!['true', 'false', '1', '0'].includes(possuiOsAberta)) {
|
||||
throw createHttpError(400, 'Filtro de OS aberta invalido.');
|
||||
}
|
||||
|
||||
where.push(`${['true', '1'].includes(possuiOsAberta) ? '' : 'NOT '}EXISTS (
|
||||
SELECT 1
|
||||
FROM manut_ordens_servico os_aberta
|
||||
WHERE os_aberta.equipamento_id = e.id
|
||||
AND os_aberta.excluido_em IS NULL
|
||||
AND os_aberta.status NOT IN ('RASCUNHO', 'RETIRADA', 'CANCELADA')
|
||||
)`);
|
||||
}
|
||||
|
||||
if (req.query.status_ultima_os) {
|
||||
const statusUltimaOs = enumValue(
|
||||
req.query.status_ultima_os,
|
||||
STATUS_OS,
|
||||
'Status da ultima OS'
|
||||
);
|
||||
where.push(`(
|
||||
SELECT os_status.status
|
||||
FROM manut_ordens_servico os_status
|
||||
WHERE os_status.equipamento_id = e.id
|
||||
AND os_status.excluido_em IS NULL
|
||||
ORDER BY os_status.data_entrada DESC, os_status.id DESC
|
||||
LIMIT 1
|
||||
) = ?`);
|
||||
params.push(statusUltimaOs);
|
||||
}
|
||||
|
||||
const whereSql = where.join('\n AND ');
|
||||
const { results: countRows } = await dbQuery(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
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 ${whereSql};
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
const listParams = [...params, pageSize, offset];
|
||||
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,
|
||||
COALESCE(resumo.quantidade_os, 0) AS quantidade_os,
|
||||
COALESCE(resumo.quantidade_os_abertas, 0) AS quantidade_os_abertas,
|
||||
COALESCE(resumo.valor_mao_obra, 0) AS valor_mao_obra_historico,
|
||||
COALESCE(resumo.valor_insumos, 0) AS valor_insumos_historico,
|
||||
COALESCE(resumo.valor_total, 0) AS valor_total_historico,
|
||||
COALESCE(resumo.valor_pago, 0) AS valor_pago_historico,
|
||||
COALESCE(resumo.valor_pendente, 0) AS valor_pendente_historico,
|
||||
ultima_os.id AS ultima_os_id,
|
||||
ultima_os.status AS ultima_os_status,
|
||||
ultima_os.tipo AS ultima_os_tipo,
|
||||
ultima_os.data_entrada AS ultima_os_data_entrada,
|
||||
ultima_os.data_finalizacao AS ultima_os_data_finalizacao,
|
||||
ultima_os.data_retirada AS ultima_os_data_retirada
|
||||
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
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
equipamento_id,
|
||||
COUNT(*) AS quantidade_os,
|
||||
SUM(status NOT IN ('RASCUNHO', 'RETIRADA', 'CANCELADA')) AS quantidade_os_abertas,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_mao_obra ELSE 0 END) AS valor_mao_obra,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_insumos ELSE 0 END) AS valor_insumos,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_total ELSE 0 END) AS valor_total,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_pago ELSE 0 END) AS valor_pago,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN GREATEST(valor_total - valor_pago, 0) ELSE 0 END) AS valor_pendente
|
||||
FROM manut_ordens_servico
|
||||
WHERE excluido_em IS NULL
|
||||
GROUP BY equipamento_id
|
||||
) resumo ON resumo.equipamento_id = e.id
|
||||
LEFT JOIN manut_ordens_servico ultima_os
|
||||
ON ultima_os.id = (
|
||||
SELECT os_ultima.id
|
||||
FROM manut_ordens_servico os_ultima
|
||||
WHERE os_ultima.equipamento_id = e.id
|
||||
AND os_ultima.excluido_em IS NULL
|
||||
ORDER BY os_ultima.data_entrada DESC, os_ultima.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE ${whereSql}
|
||||
ORDER BY e.nome, e.numero_serie
|
||||
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('/getEquipamentoProntuario/:equipamentoId', async function (req, res) {
|
||||
try {
|
||||
const equipamentoId = positiveInteger(req.params.equipamentoId, 'Equipamento');
|
||||
|
||||
const [
|
||||
{ results: equipamentos },
|
||||
{ results: totaisRows },
|
||||
{ results: ultimaOsRows },
|
||||
{ results: leiturasManutencao }
|
||||
] = await Promise.all([
|
||||
dbQuery(
|
||||
`
|
||||
SELECT
|
||||
e.*,
|
||||
m.nome AS modelo_nome,
|
||||
c.nome_fantasia AS cliente_nome,
|
||||
c.razao_social AS cliente_razao_social
|
||||
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.id = ?
|
||||
AND e.ativo = 1
|
||||
LIMIT 1;
|
||||
`,
|
||||
[equipamentoId]
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) AS quantidade_os,
|
||||
SUM(status NOT IN ('RASCUNHO', 'RETIRADA', 'CANCELADA')) AS quantidade_os_abertas,
|
||||
SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN 1 ELSE 0 END) AS quantidade_os_efetivas,
|
||||
SUM(CASE WHEN tipo = 'CORRETIVA' AND status NOT IN ('RASCUNHO', 'CANCELADA') THEN 1 ELSE 0 END) AS quantidade_corretivas,
|
||||
SUM(CASE WHEN tipo = 'PREVENTIVA' AND status NOT IN ('RASCUNHO', 'CANCELADA') THEN 1 ELSE 0 END) AS quantidade_preventivas,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN total_horas_expediente ELSE 0 END), 0) AS total_horas_expediente,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN total_horas_extras ELSE 0 END), 0) AS total_horas_extras,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_mao_obra ELSE 0 END), 0) AS valor_mao_obra,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN custo_insumos ELSE 0 END), 0) AS custo_insumos,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_insumos ELSE 0 END), 0) AS valor_insumos,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_desconto ELSE 0 END), 0) AS valor_desconto,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_acrescimo ELSE 0 END), 0) AS valor_acrescimo,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_total ELSE 0 END), 0) AS valor_total,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN valor_pago ELSE 0 END), 0) AS valor_pago,
|
||||
COALESCE(SUM(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN GREATEST(valor_total - valor_pago, 0) ELSE 0 END), 0) AS valor_pendente,
|
||||
MIN(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN data_entrada END) AS primeira_manutencao_em,
|
||||
MAX(CASE WHEN status NOT IN ('RASCUNHO', 'CANCELADA') THEN data_entrada END) AS ultima_manutencao_em
|
||||
FROM manut_ordens_servico
|
||||
WHERE equipamento_id = ?
|
||||
AND excluido_em IS NULL;
|
||||
`,
|
||||
[equipamentoId]
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT
|
||||
id, tipo, status, status_pagamento, prioridade,
|
||||
data_entrada, data_finalizacao, data_retirada,
|
||||
problema_relatado, diagnostico, solucao_resumo,
|
||||
valor_mao_obra, valor_insumos, valor_total
|
||||
FROM manut_ordens_servico
|
||||
WHERE equipamento_id = ?
|
||||
AND excluido_em IS NULL
|
||||
ORDER BY data_entrada DESC, id DESC
|
||||
LIMIT 1;
|
||||
`,
|
||||
[equipamentoId]
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
ordem_servico_id,
|
||||
tipo_leitura,
|
||||
leitura_em,
|
||||
odometro_total_segundos,
|
||||
odometro_conectado_segundos,
|
||||
odometro_movimento_segundos,
|
||||
odometro_parcial_segundos,
|
||||
observacoes
|
||||
FROM manut_equipamento_odometro_leituras
|
||||
WHERE equipamento_id = ?
|
||||
AND tipo_leitura IN ('ENTRADA_MANUTENCAO', 'SAIDA_MANUTENCAO')
|
||||
ORDER BY leitura_em, id;
|
||||
`,
|
||||
[equipamentoId]
|
||||
)
|
||||
]);
|
||||
|
||||
if (!equipamentos.length) {
|
||||
throw createHttpError(404, 'Equipamento nao encontrado ou inativo.');
|
||||
}
|
||||
|
||||
const equipamento = equipamentos[0];
|
||||
const totais = totaisRows[0];
|
||||
const entradas = leiturasManutencao.filter(
|
||||
leitura => leitura.tipo_leitura === 'ENTRADA_MANUTENCAO'
|
||||
);
|
||||
const intervalosMovimento = [];
|
||||
|
||||
for (let index = 1; index < entradas.length; index += 1) {
|
||||
intervalosMovimento.push(
|
||||
Number(entradas[index].odometro_movimento_segundos) -
|
||||
Number(entradas[index - 1].odometro_movimento_segundos)
|
||||
);
|
||||
}
|
||||
|
||||
const ultimaSaida = [...leiturasManutencao]
|
||||
.reverse()
|
||||
.find(leitura => leitura.tipo_leitura === 'SAIDA_MANUTENCAO') || null;
|
||||
const movimentoAtual = Number(equipamento.odometro_movimento_segundos || 0);
|
||||
const horasMovimento = movimentoAtual / 3600;
|
||||
const valorTotal = Number(totais.valor_total || 0);
|
||||
|
||||
return sendSuccess(res, {
|
||||
equipamento,
|
||||
indicadores: {
|
||||
...totais,
|
||||
horas_movimento: horasMovimento,
|
||||
horas_conectado: Number(equipamento.odometro_conectado_segundos || 0) / 3600,
|
||||
horas_total: Number(equipamento.odometro_total_segundos || 0) / 3600,
|
||||
horas_parcial: Number(equipamento.odometro_parcial_segundos || 0) / 3600,
|
||||
movimento_desde_ultima_saida_segundos: ultimaSaida
|
||||
? Math.max(0, movimentoAtual - Number(ultimaSaida.odometro_movimento_segundos))
|
||||
: null,
|
||||
media_movimento_entre_manutencoes_segundos: intervalosMovimento.length
|
||||
? intervalosMovimento.reduce((total, valor) => total + valor, 0) / intervalosMovimento.length
|
||||
: null,
|
||||
custo_medio_por_os: Number(totais.quantidade_os_efetivas || 0)
|
||||
? valorTotal / Number(totais.quantidade_os_efetivas)
|
||||
: 0,
|
||||
custo_por_hora_movimento: horasMovimento > 0 ? valorTotal / horasMovimento : null
|
||||
},
|
||||
ultima_os: ultimaOsRows[0] || null,
|
||||
ultima_saida_manutencao: ultimaSaida,
|
||||
leituras_manutencao: leiturasManutencao
|
||||
});
|
||||
} catch (error) {
|
||||
return handleRouteError(req, res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getEquipamentoTimeline/:equipamentoId', async function (req, res) {
|
||||
try {
|
||||
const equipamentoId = positiveInteger(req.params.equipamentoId, 'Equipamento');
|
||||
const { page, pageSize, offset } = parsePagination(req.query);
|
||||
const where = [
|
||||
'os.equipamento_id = ?',
|
||||
'os.excluido_em IS NULL'
|
||||
];
|
||||
const params = [equipamentoId];
|
||||
|
||||
if (req.query.tipo) {
|
||||
where.push('os.tipo = ?');
|
||||
params.push(enumValue(req.query.tipo, TIPOS_OS, 'Tipo'));
|
||||
}
|
||||
|
||||
if (req.query.status) {
|
||||
where.push('os.status = ?');
|
||||
params.push(enumValue(req.query.status, STATUS_OS, 'Status'));
|
||||
}
|
||||
|
||||
if (req.query.data_inicio) {
|
||||
where.push('os.data_entrada >= ?');
|
||||
params.push(normalizeNullableDate(req.query.data_inicio));
|
||||
}
|
||||
|
||||
if (req.query.data_fim) {
|
||||
where.push('os.data_entrada < DATE_ADD(?, INTERVAL 1 DAY)');
|
||||
params.push(normalizeNullableDate(req.query.data_fim));
|
||||
}
|
||||
|
||||
if (req.query.somente_com_materiais === 'true' || req.query.somente_com_materiais === '1') {
|
||||
where.push(`EXISTS (
|
||||
SELECT 1
|
||||
FROM manut_ordem_servico_atividades atv_mat
|
||||
INNER JOIN manut_ordem_servico_atividade_materiais mat_busca
|
||||
ON mat_busca.atividade_id = atv_mat.id
|
||||
WHERE atv_mat.ordem_servico_id = os.id
|
||||
)`);
|
||||
}
|
||||
|
||||
if (req.query.busca) {
|
||||
const busca = `%${String(req.query.busca).trim()}%`;
|
||||
where.push(`(
|
||||
CAST(os.id AS CHAR) LIKE ? OR
|
||||
os.problema_relatado LIKE ? OR
|
||||
os.diagnostico LIKE ? OR
|
||||
os.solucao_resumo LIKE ? OR
|
||||
os.observacoes_internas LIKE ? OR
|
||||
os.observacoes_cliente LIKE ? OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM manut_ordem_servico_atividades atv_busca
|
||||
WHERE atv_busca.ordem_servico_id = os.id
|
||||
AND (
|
||||
atv_busca.descricao LIKE ? OR
|
||||
atv_busca.observacoes_internas LIKE ? OR
|
||||
atv_busca.observacoes_cliente LIKE ?
|
||||
)
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM manut_ordem_servico_atividades atv_busca_mat
|
||||
INNER JOIN manut_ordem_servico_atividade_materiais mat_busca_texto
|
||||
ON mat_busca_texto.atividade_id = atv_busca_mat.id
|
||||
WHERE atv_busca_mat.ordem_servico_id = os.id
|
||||
AND (
|
||||
mat_busca_texto.descricao_snapshot LIKE ? OR
|
||||
mat_busca_texto.observacoes LIKE ?
|
||||
)
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM manut_ordem_servico_historico_status hs_busca
|
||||
WHERE hs_busca.ordem_servico_id = os.id
|
||||
AND hs_busca.observacoes LIKE ?
|
||||
)
|
||||
)`);
|
||||
params.push(
|
||||
busca, busca, busca, busca, busca, busca,
|
||||
busca, busca, busca, busca, busca, busca
|
||||
);
|
||||
}
|
||||
|
||||
const whereSql = where.join('\n AND ');
|
||||
const { results: countRows } = await dbQuery(
|
||||
`SELECT COUNT(*) AS total FROM manut_ordens_servico os WHERE ${whereSql};`,
|
||||
params
|
||||
);
|
||||
|
||||
const { results: orders } = await 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 ${whereSql}
|
||||
ORDER BY os.data_entrada DESC, os.id DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
`,
|
||||
[...params, pageSize, offset]
|
||||
);
|
||||
|
||||
if (!orders.length) {
|
||||
return sendSuccess(res, {
|
||||
items: [],
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total: Number(countRows[0].total),
|
||||
totalPages: Math.ceil(Number(countRows[0].total) / pageSize)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const orderIds = orders.map(order => Number(order.id));
|
||||
const placeholders = orderIds.map(() => '?').join(', ');
|
||||
const [
|
||||
{ results: activities },
|
||||
{ results: technicians },
|
||||
{ results: materials },
|
||||
{ results: odometers },
|
||||
{ results: statusHistory }
|
||||
] = await Promise.all([
|
||||
dbQuery(
|
||||
`SELECT * FROM manut_ordem_servico_atividades WHERE ordem_servico_id IN (${placeholders}) ORDER BY data_atividade, ordem_exibicao, id;`,
|
||||
orderIds
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT
|
||||
a.ordem_servico_id,
|
||||
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 IN (${placeholders})
|
||||
ORDER BY t.nome;
|
||||
`,
|
||||
orderIds
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT a.ordem_servico_id, 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 IN (${placeholders})
|
||||
ORDER BY mat.id;
|
||||
`,
|
||||
orderIds
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT *
|
||||
FROM manut_equipamento_odometro_leituras
|
||||
WHERE equipamento_id = ?
|
||||
AND ordem_servico_id IN (${placeholders})
|
||||
ORDER BY leitura_em, id;
|
||||
`,
|
||||
[equipamentoId, ...orderIds]
|
||||
),
|
||||
dbQuery(
|
||||
`
|
||||
SELECT *
|
||||
FROM manut_ordem_servico_historico_status
|
||||
WHERE ordem_servico_id IN (${placeholders})
|
||||
ORDER BY created_at, id;
|
||||
`,
|
||||
orderIds
|
||||
)
|
||||
]);
|
||||
|
||||
const techniciansByActivity = new Map();
|
||||
for (const technician of technicians) {
|
||||
if (!techniciansByActivity.has(technician.atividade_id)) {
|
||||
techniciansByActivity.set(technician.atividade_id, []);
|
||||
}
|
||||
techniciansByActivity.get(technician.atividade_id).push(technician);
|
||||
}
|
||||
|
||||
const materialsByActivity = new Map();
|
||||
for (const material of materials) {
|
||||
if (!materialsByActivity.has(material.atividade_id)) {
|
||||
materialsByActivity.set(material.atividade_id, []);
|
||||
}
|
||||
materialsByActivity.get(material.atividade_id).push(material);
|
||||
}
|
||||
|
||||
const activitiesByOrder = new Map();
|
||||
for (const activity of activities) {
|
||||
if (!activitiesByOrder.has(activity.ordem_servico_id)) {
|
||||
activitiesByOrder.set(activity.ordem_servico_id, []);
|
||||
}
|
||||
activitiesByOrder.get(activity.ordem_servico_id).push({
|
||||
...activity,
|
||||
tecnicos: techniciansByActivity.get(activity.id) || [],
|
||||
materiais: materialsByActivity.get(activity.id) || []
|
||||
});
|
||||
}
|
||||
|
||||
const groupByOrder = rows => {
|
||||
const map = new Map();
|
||||
for (const row of rows) {
|
||||
if (!map.has(row.ordem_servico_id)) {
|
||||
map.set(row.ordem_servico_id, []);
|
||||
}
|
||||
map.get(row.ordem_servico_id).push(row);
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const odometersByOrder = groupByOrder(odometers);
|
||||
const statusByOrder = groupByOrder(statusHistory);
|
||||
const items = orders.map(order => ({
|
||||
...order,
|
||||
atividades: activitiesByOrder.get(order.id) || [],
|
||||
odometros: odometersByOrder.get(order.id) || [],
|
||||
historico_status: statusByOrder.get(order.id) || []
|
||||
}));
|
||||
|
||||
return sendSuccess(res, {
|
||||
items,
|
||||
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('/getTecnicosManutencao', async function (req, res) {
|
||||
try {
|
||||
const { results } = await dbQuery(`
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import { PoliticaPrivacidadeComponent } from './politica-privacidade/politica-pr
|
|||
import { SobreComponent } from './sobre/sobre.component';
|
||||
import { OrionOrdensServicoComponent } from './oriontard/orion-ordens-servico/orion-ordens-servico.component';
|
||||
import { OrionOrdemServicoFormComponent } from './oriontard/orion-ordem-servico-form/orion-ordem-servico-form.component';
|
||||
import { OrionEquipamentosComponent } from './oriontard/orion-equipamentos/orion-equipamentos.component';
|
||||
import { OrionEquipamentoProntuarioComponent } from './oriontard/orion-equipamento-prontuario/orion-equipamento-prontuario.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
|
|
@ -168,6 +170,14 @@ const routes: Routes = [
|
|||
path: 'orion-ordens-servico',
|
||||
component: OrionOrdensServicoComponent
|
||||
},
|
||||
{
|
||||
path: 'orion-equipamentos',
|
||||
component: OrionEquipamentosComponent
|
||||
},
|
||||
{
|
||||
path: 'orion-equipamentos/:id/historico',
|
||||
component: OrionEquipamentoProntuarioComponent
|
||||
},
|
||||
{
|
||||
path: 'agrobase',
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ import { PoliticaPrivacidadeComponent } from './politica-privacidade/politica-pr
|
|||
import { TermosUsoComponent } from './termos-uso/termos-uso.component';
|
||||
import { OrionOrdensServicoComponent } from './oriontard/orion-ordens-servico/orion-ordens-servico.component';
|
||||
import { OrionOrdemServicoFormComponent } from './oriontard/orion-ordem-servico-form/orion-ordem-servico-form.component';
|
||||
import { OrionEquipamentosComponent } from './oriontard/orion-equipamentos/orion-equipamentos.component';
|
||||
import { OrionEquipamentoProntuarioComponent } from './oriontard/orion-equipamento-prontuario/orion-equipamento-prontuario.component';
|
||||
|
||||
|
||||
// Registrar a localidade pt-BR
|
||||
|
|
@ -81,7 +83,14 @@ registerLocaleData(localePt);
|
|||
ProdutosComponent,
|
||||
AgrobaseLoginComponent,
|
||||
AgrobaseVersionamentoComponent,
|
||||
TipoArquivoNomePipe, SobreComponent, PoliticaPrivacidadeComponent, TermosUsoComponent, OrionOrdensServicoComponent, OrionOrdemServicoFormComponent
|
||||
TipoArquivoNomePipe,
|
||||
SobreComponent,
|
||||
PoliticaPrivacidadeComponent,
|
||||
TermosUsoComponent,
|
||||
OrionOrdensServicoComponent,
|
||||
OrionOrdemServicoFormComponent,
|
||||
OrionEquipamentosComponent,
|
||||
OrionEquipamentoProntuarioComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
|
|
|||
|
|
@ -175,6 +175,16 @@ export class DashboardComponent implements OnInit {
|
|||
issub: true,
|
||||
sub: null,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
texto: "Equipamentos",
|
||||
link: "/orion-equipamentos",
|
||||
icon: "update",
|
||||
havesub: false,
|
||||
dropicon: "",
|
||||
issub: true,
|
||||
sub: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,14 +1,44 @@
|
|||
export class OrionLogsModel {
|
||||
dataLog: Date;
|
||||
id?: number;
|
||||
dataLog: Date | string;
|
||||
nome: string;
|
||||
coordenadas: string;
|
||||
perfil: string;
|
||||
log: string;
|
||||
logObj: OrionLogsDetalhesModel;
|
||||
|
||||
// Metadados normalizados pela API/tela. Mantêm compatibilidade com logs antigos.
|
||||
dataOperacao?: Date | string;
|
||||
schemaVersao?: number;
|
||||
tipoEvento?: string;
|
||||
periodoInicio?: Date | string;
|
||||
periodoFim?: Date | string;
|
||||
jsonValido?: boolean;
|
||||
erroParse?: string;
|
||||
}
|
||||
|
||||
export class OrionLogsDetalhesModel {
|
||||
data: Date;
|
||||
// Schema 2
|
||||
schemaVersao?: number;
|
||||
logId?: string;
|
||||
tipoEvento?: string;
|
||||
mensagemEvento?: string;
|
||||
periodoInicio?: Date | string;
|
||||
periodoFim?: Date | string;
|
||||
protocoloVersao?: number;
|
||||
revisaoHardware?: string;
|
||||
capacidades?: string;
|
||||
mascaraSaude?: number;
|
||||
alarmeInfiltracao?: boolean;
|
||||
infiltracaoRaw?: number;
|
||||
potenciaW?: number;
|
||||
rollDeg?: number;
|
||||
pitchDeg?: number;
|
||||
firmwareVersaoStr?: string;
|
||||
idCliente?: number;
|
||||
|
||||
// Campos históricos
|
||||
data: Date | string;
|
||||
firmwareVersao: number;
|
||||
robotNs: number;
|
||||
modelo: string;
|
||||
|
|
@ -33,6 +63,7 @@ export class OrionLogsDetalhesModel {
|
|||
conectado: boolean;
|
||||
distancia: number;
|
||||
|
||||
// Campos derivados exclusivamente para exibição.
|
||||
HoraInicio: string;
|
||||
HoraFim: string;
|
||||
Duracao: string;
|
||||
|
|
@ -57,6 +88,8 @@ export class OrionLogSensorModel {
|
|||
media: number;
|
||||
moda: number;
|
||||
mediana: number;
|
||||
ultimo?: number;
|
||||
quantidadeAmostras?: number;
|
||||
lista: number[];
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +169,111 @@ export interface EquipamentoManutencaoModel {
|
|||
cliente_nome?: string;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HISTORICO / PRONTUARIO DOS EQUIPAMENTOS
|
||||
// ============================================================
|
||||
|
||||
export interface EquipamentoHistoricoListaModel extends EquipamentoManutencaoModel {
|
||||
quantidade_os: number;
|
||||
quantidade_os_abertas: number;
|
||||
|
||||
valor_mao_obra_historico: number;
|
||||
valor_insumos_historico: number;
|
||||
valor_total_historico: number;
|
||||
valor_pago_historico: number;
|
||||
valor_pendente_historico: number;
|
||||
|
||||
ultima_os_id?: number;
|
||||
ultima_os_status?: OrdemServicoStatus;
|
||||
ultima_os_tipo?: OrdemServicoTipo;
|
||||
ultima_os_data_entrada?: string;
|
||||
ultima_os_data_finalizacao?: string;
|
||||
ultima_os_data_retirada?: string;
|
||||
}
|
||||
|
||||
export interface EquipamentoHistoricoFiltrosModel {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
busca?: string;
|
||||
modelo_id?: number;
|
||||
modelo?: string;
|
||||
cliente_id?: number;
|
||||
versao?: string;
|
||||
possui_os_aberta?: boolean | '';
|
||||
status_ultima_os?: OrdemServicoStatus | '';
|
||||
}
|
||||
|
||||
export interface EquipamentoHistoricoListaResponseModel {
|
||||
items: EquipamentoHistoricoListaModel[];
|
||||
pagination: OrdemServicoPaginacaoModel;
|
||||
}
|
||||
|
||||
export interface EquipamentoProntuarioModel extends EquipamentoManutencaoModel {
|
||||
cliente_razao_social?: string;
|
||||
ativo?: boolean | number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface EquipamentoProntuarioIndicadoresModel {
|
||||
quantidade_os: number;
|
||||
quantidade_os_abertas: number;
|
||||
quantidade_os_efetivas: number;
|
||||
quantidade_corretivas: number;
|
||||
quantidade_preventivas: number;
|
||||
|
||||
total_horas_expediente: number;
|
||||
total_horas_extras: number;
|
||||
horas_movimento: number;
|
||||
horas_conectado: number;
|
||||
horas_total: number;
|
||||
horas_parcial: number;
|
||||
|
||||
valor_mao_obra: number;
|
||||
custo_insumos: number;
|
||||
valor_insumos: number;
|
||||
valor_desconto: number;
|
||||
valor_acrescimo: number;
|
||||
valor_total: number;
|
||||
valor_pago: number;
|
||||
valor_pendente: number;
|
||||
|
||||
primeira_manutencao_em?: string;
|
||||
ultima_manutencao_em?: string;
|
||||
movimento_desde_ultima_saida_segundos?: number;
|
||||
media_movimento_entre_manutencoes_segundos?: number;
|
||||
custo_medio_por_os: number;
|
||||
custo_por_hora_movimento?: number;
|
||||
}
|
||||
|
||||
export interface EquipamentoProntuarioResponseModel {
|
||||
equipamento: EquipamentoProntuarioModel;
|
||||
indicadores: EquipamentoProntuarioIndicadoresModel;
|
||||
ultima_os?: OrdemServicoListaModel;
|
||||
ultima_saida_manutencao?: OrdemServicoOdometroModel;
|
||||
leituras_manutencao: OrdemServicoOdometroModel[];
|
||||
}
|
||||
|
||||
export interface EquipamentoTimelineFiltrosModel {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
busca?: string;
|
||||
tipo?: OrdemServicoTipo | '';
|
||||
status?: OrdemServicoStatus | '';
|
||||
data_inicio?: string;
|
||||
data_fim?: string;
|
||||
somente_com_materiais?: boolean;
|
||||
}
|
||||
|
||||
export interface EquipamentoTimelineItemModel extends OrdemServicoDetalhesModel {
|
||||
odometros: OrdemServicoOdometroModel[];
|
||||
}
|
||||
|
||||
export interface EquipamentoTimelineResponseModel {
|
||||
items: EquipamentoTimelineItemModel[];
|
||||
pagination: OrdemServicoPaginacaoModel;
|
||||
}
|
||||
|
||||
export interface TecnicoManutencaoModel {
|
||||
id: number;
|
||||
usuario_sistema_id?: number;
|
||||
|
|
@ -510,4 +648,4 @@ export type OrdemServicoTipoLeituraOdometro =
|
|||
| 'ENTRADA_MANUTENCAO'
|
||||
| 'SAIDA_MANUTENCAO'
|
||||
| 'ATUALIZACAO_MANUAL'
|
||||
| 'TELEMETRIA';
|
||||
| 'TELEMETRIA';
|
||||
|
|
|
|||
|
|
@ -1,181 +1,435 @@
|
|||
<div>
|
||||
<mat-sidenav-container>
|
||||
<mat-sidenav mode="side" opened>
|
||||
<div style="margin-left: 1em; margin-right: 1em;">
|
||||
<label>Data</label>
|
||||
<br>
|
||||
<mat-form-field>
|
||||
<input matInput [matDatepicker]="picker" [(ngModel)]="DataSelecionada">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker [dateClass]="highlightDates"></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<div class="dashboard-shell">
|
||||
<aside class="filters-panel">
|
||||
<div class="brand-block">
|
||||
<div class="eyebrow">ORION TARD PRO</div>
|
||||
<h2>Telemetria de operação</h2>
|
||||
<p>Histórico, sessões e leitura operacional do robô.</p>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<div class="filter-block">
|
||||
<label>Data</label>
|
||||
<mat-form-field appearance="outline">
|
||||
<input matInput [matDatepicker]="picker" [(ngModel)]="DataSelecionada" placeholder="Escolha um dia">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker [dateClass]="highlightDates"></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<label>Hora</label>
|
||||
<ng5-slider [(value)]="HoraInicial" [(highValue)]="HoraFinal" [options]="sliderOptions"></ng5-slider>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="btnFiltrarClick()">Filtrar</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<label *ngIf="NomesFiltro.length > 0">Usuário</label>
|
||||
<br>
|
||||
<mat-form-field *ngIf="NomesFiltro.length > 0">
|
||||
<mat-select (selectionChange)="txtNomeChange($event.value)">
|
||||
<mat-option *ngFor="let Nome of NomesFiltro" [value]="Nome">{{Nome}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<br><br>
|
||||
|
||||
<label *ngIf="NomesFiltro.length > 0 && PerfisFiltro.length > 0">Perfil</label>
|
||||
<br>
|
||||
<mat-form-field *ngIf="NomesFiltro.length > 0 && PerfisFiltro.length > 0">
|
||||
<mat-select (selectionChange)="txtPerfilChange($event.value)">
|
||||
<mat-option *ngFor="let Perfil of PerfisFiltro" [value]="Perfil">{{Perfil}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<br><br>
|
||||
|
||||
<label *ngIf="trabalhos.length > 1">Trabalho</label>
|
||||
<br>
|
||||
<mat-form-field *ngIf="trabalhos.length > 1">
|
||||
<mat-select (selectionChange)="onTrabalhoSelecionado($event.value)">
|
||||
<mat-option *ngFor="let trabalho of trabalhos" [value]="trabalho">
|
||||
{{trabalho[0].dataLog | date: 'HH:mm'}} ({{trabalho.length}} logs)
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button mat-raised-button color="accent" (click)="btnAtualizarClick()" *ngIf="NomesFiltro.length > 0 && PerfisFiltro.length > 0">Atualizar</button>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content style="height: 100vh;">
|
||||
<div *ngIf="FiltroSelecionado != null">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="TabelaDados">
|
||||
<label>Data: {{FiltroSelecionado.data | date:'dd/MM/yyyy'}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Início: {{FiltroSelecionado.HoraInicio}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Fim: {{FiltroSelecionado.HoraFim}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Duração: {{FiltroSelecionado.Duracao}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Serviço: {{(FiltroSelecionado.servico != null) ? FiltroSelecionado.servico.Nome : ""}}</label><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="TabelaDados">
|
||||
<label>Versão Software: {{FiltroSelecionado.versao}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Conectado: {{FiltroSelecionado.conectado == true ? "Sim" : "Não"}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Porta COM: {{FiltroSelecionado.portaCOM}}</label><br>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Baud Rate: {{FiltroSelecionado.baudRate}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>{{FiltroSelecionado.camera}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="TabelaDados">
|
||||
<label>Odômetro Total: {{SegundosParaTempo(FiltroSelecionado.odometroTotal)}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Odômetro Conectado: {{SegundosParaTempo(FiltroSelecionado.odometroConectado)}}</label><br>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Odômetro Motores: {{SegundosParaTempo(FiltroSelecionado.odometroMotores)}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Odômetro Motores Parcial: {{SegundosParaTempo(FiltroSelecionado.odometroMotoresParcial)}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Tempo Conectado: {{SegundosParaTempo(FiltroSelecionado.tempoConectado)}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="TabelaDados">
|
||||
<Label>Equipamento: {{FiltroSelecionado.modelo}}</Label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<Label>NS: {{FiltroSelecionado.robotNs}}</Label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<Label>Versão da Firmware: {{FiltroSelecionado.firmwareVersao}}</Label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<label>Coordenadas: {{FiltroSelecionado.Coordenadas}}</label>
|
||||
</td>
|
||||
<td class="TabelaDados">
|
||||
<Label>Localização: {{FiltroSelecionado.Localizacao}}</Label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-tab-group (selectedTabChange)="onTabChange($event)">
|
||||
<mat-tab *ngFor="let Sensor of FiltroSelecionado.sensores" label="{{Sensor.descricao}}">
|
||||
<canvas [id]="'chart_' + Sensor.sensor + '_' + Sensor.descricao"></canvas>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div class="calendar-legend">
|
||||
<span><i class="legend-dot v2"></i> operação / log schema 2</span>
|
||||
<span><i class="legend-dot legacy"></i> log legado</span>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<label>Faixa de horário</label>
|
||||
<ng5-slider
|
||||
[(value)]="HoraInicial"
|
||||
[(highValue)]="HoraFinal"
|
||||
[options]="sliderOptions">
|
||||
</ng5-slider>
|
||||
|
||||
<div *ngIf="false">
|
||||
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<!-- Colunas -->
|
||||
<ng-container matColumnDef="data">
|
||||
<mat-header-cell *matHeaderCellDef> Data </mat-header-cell>
|
||||
<mat-cell *matCellDef="let log"> {{log.logObj.data | date:'short'}} </mat-cell>
|
||||
</ng-container>
|
||||
<button mat-raised-button color="primary" class="full-button" (click)="btnFiltrarClick()">
|
||||
Aplicar período
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container matColumnDef="nome">
|
||||
<mat-header-cell *matHeaderCellDef> Nome </mat-header-cell>
|
||||
<mat-cell *matCellDef="let log"> {{log.nome}} </mat-cell>
|
||||
</ng-container>
|
||||
<div class="filter-block" *ngIf="NomesFiltro.length > 0">
|
||||
<label>Usuário</label>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-select [(ngModel)]="UsuarioSelecionado" (selectionChange)="txtNomeChange($event.value)">
|
||||
<mat-option *ngFor="let nome of NomesFiltro" [value]="nome">
|
||||
{{nome}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container matColumnDef="perfil">
|
||||
<mat-header-cell *matHeaderCellDef> Perfil </mat-header-cell>
|
||||
<mat-cell *matCellDef="let log"> {{log.perfil}} </mat-cell>
|
||||
</ng-container>
|
||||
<label *ngIf="PerfisFiltro.length > 0">Perfil</label>
|
||||
<mat-form-field appearance="outline" *ngIf="PerfisFiltro.length > 0">
|
||||
<mat-select [(ngModel)]="PerfilSelecionado" (selectionChange)="txtPerfilChange($event.value)">
|
||||
<mat-option *ngFor="let perfil of PerfisFiltro" [value]="perfil">
|
||||
{{perfil}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container matColumnDef="conectado">
|
||||
<mat-header-cell *matHeaderCellDef> Conectado </mat-header-cell>
|
||||
<mat-cell *matCellDef="let log"> {{log.logObj.conectado ? 'Sim' : 'Não'}} </mat-cell>
|
||||
</ng-container>
|
||||
<label *ngIf="trabalhos.length > 0">Operação</label>
|
||||
<mat-form-field appearance="outline" *ngIf="trabalhos.length > 0">
|
||||
<mat-select [value]="trabalhoSelecionadoOpcao" (selectionChange)="onTrabalhoSelecionado($event.value)">
|
||||
<mat-option *ngFor="let trabalho of trabalhos" [value]="trabalho">
|
||||
{{trabalhoLabel(trabalho)}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<ng-container matColumnDef="camera">
|
||||
<mat-header-cell *matHeaderCellDef> Câmera </mat-header-cell>
|
||||
<mat-cell *matCellDef="let log"> {{log.logObj.camera}} </mat-cell>
|
||||
</ng-container>
|
||||
<div class="side-summary">
|
||||
<div><strong>{{LogsDetalhes.length}}</strong><span>logs carregados</span></div>
|
||||
<div><strong>{{Datas.length}}</strong><span>dias com registros</span></div>
|
||||
<div><strong>{{LogsPeriodo.length}}</strong><span>no período</span></div>
|
||||
<div *ngIf="LogsInvalidos > 0" class="warning"><strong>{{LogsInvalidos}}</strong><span>JSON inválido</span></div>
|
||||
</div>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
<button mat-button class="full-button refresh-button" (click)="carregarLogs()">
|
||||
Recarregar dados
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<!-- Paginator -->
|
||||
<mat-paginator [pageSizeOptions]="[10, 25, 50]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
<main class="content-panel">
|
||||
<div class="state-card" *ngIf="Carregando">Carregando logs do Orion...</div>
|
||||
|
||||
<div class="state-card error" *ngIf="!Carregando && ErroCarregamento">
|
||||
<strong>Falha ao carregar os logs.</strong>
|
||||
<span>{{ErroCarregamento}}</span>
|
||||
</div>
|
||||
|
||||
<div class="state-card" *ngIf="!Carregando && !ErroCarregamento && LogsDetalhes.length === 0">
|
||||
Nenhum log encontrado no período disponibilizado pela API.
|
||||
</div>
|
||||
|
||||
<div class="state-card selection-hint" *ngIf="!Carregando && !ErroCarregamento && LogsDetalhes.length > 0 && FiltroSelecionado == null">
|
||||
<strong>Escolha uma operação para abrir a telemetria.</strong>
|
||||
<span>Selecione um dia destacado, aplique o período e escolha a sessão que deseja analisar.</span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="FiltroSelecionado != null">
|
||||
<section class="hero-card">
|
||||
<div>
|
||||
<div class="eyebrow">OPERAÇÃO SELECIONADA</div>
|
||||
<h1>
|
||||
{{FiltroSelecionado.modelo || 'Equipamento'}}
|
||||
<span class="separator">•</span>
|
||||
{{FiltroSelecionado.robotNs || 'NS'}}
|
||||
</h1>
|
||||
<p>
|
||||
{{FiltroSelecionado.usuario || UsuarioSelecionado}}
|
||||
<span class="separator">•</span>
|
||||
{{PerfilSelecionado}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<span class="event-badge">{{eventoAtual()}}</span>
|
||||
<div class="status-pill" [class.online]="FiltroSelecionado.conectado">
|
||||
{{FiltroSelecionado.conectado ? 'Conectado' : 'Desconectado'}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid headline-metrics">
|
||||
<div class="metric-card emphasis">
|
||||
<span>Início</span>
|
||||
<strong>{{resumoTrabalho.inicio | date:'HH:mm:ss'}}</strong>
|
||||
<small>{{resumoTrabalho.inicio | date:'dd/MM/yyyy'}}</small>
|
||||
</div>
|
||||
<div class="metric-card emphasis">
|
||||
<span>Fim</span>
|
||||
<strong>{{resumoTrabalho.fim | date:'HH:mm:ss'}}</strong>
|
||||
<small>{{resumoTrabalho.fim | date:'dd/MM/yyyy'}}</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>Duração</span>
|
||||
<strong>{{SegundosParaTempo(resumoTrabalho.duracaoSegundos)}}</strong>
|
||||
<small>janela real dos logs</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>Distância</span>
|
||||
<strong>{{resumoTrabalho.distanciaPercorrida | number:'1.1-2'}} m</strong>
|
||||
<small>movimento estimado</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>Velocidade média</span>
|
||||
<strong>{{formatarMedida(telemetria.velocidadeMedia, 2, 'm/s')}}</strong>
|
||||
<small>máx. {{formatarMedida(telemetria.velocidadeMax, 2, 'm/s')}}</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>Bateria mínima</span>
|
||||
<strong>{{formatarMedida(telemetria.bateriaMin, 1, '%')}}</strong>
|
||||
<small>final {{formatarMedida(telemetria.bateriaFinal, 1, '%')}}</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section" *ngIf="telemetria.temEnergia">
|
||||
<div class="story-heading">
|
||||
<div>
|
||||
<div class="eyebrow">ENERGIA</div>
|
||||
<h2>Como o sistema elétrico se comportou</h2>
|
||||
<p>Tensão, carga da bateria, corrente e potência no mesmo contexto operacional.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-kpis">
|
||||
<div class="story-kpi"><span>Tensão média</span><strong>{{formatarMedida(telemetria.tensaoMedia, 2, 'V')}}</strong><small>mín. {{formatarMedida(telemetria.tensaoMin, 2, 'V')}}</small></div>
|
||||
<div class="story-kpi"><span>Bateria média</span><strong>{{formatarMedida(telemetria.bateriaMedia, 1, '%')}}</strong><small>mín. {{formatarMedida(telemetria.bateriaMin, 1, '%')}}</small></div>
|
||||
<div class="story-kpi"><span>Corrente média</span><strong>{{formatarCorrente(telemetria.correnteMedia)}}</strong><small>pico {{formatarCorrente(telemetria.correnteMax)}}</small></div>
|
||||
<div class="story-kpi"><span>Potência média</span><strong>{{formatarMedida(telemetria.potenciaMediaW, 1, 'W')}}</strong><small>quando disponível no protocolo</small></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-grid two-columns">
|
||||
<article class="telemetry-card" *ngIf="telemetria.temTensao">
|
||||
<div class="card-heading"><div><span>Tensão</span><strong>Bateria sob carga</strong></div><small>V</small></div>
|
||||
<div class="chart-box"><canvas id="chart_tensao"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card" *ngIf="telemetria.temBateria">
|
||||
<div class="card-heading"><div><span>Carga</span><strong>Bateria disponível</strong></div><small>%</small></div>
|
||||
<div class="chart-box"><canvas id="chart_bateria"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card wide-card" *ngIf="telemetria.temCorrente">
|
||||
<div class="card-heading"><div><span>Consumo</span><strong>Corrente ao longo da operação</strong></div><small>escala convertida para A</small></div>
|
||||
<div class="chart-box"><canvas id="chart_corrente"></canvas></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section" *ngIf="telemetria.temMovimento">
|
||||
<div class="story-heading">
|
||||
<div>
|
||||
<div class="eyebrow">MOVIMENTO</div>
|
||||
<h2>Comando, velocidade e avanço</h2>
|
||||
<p>Relaciona o que foi comandado com a resposta de velocidade e a progressão de distância.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-kpis">
|
||||
<div class="story-kpi"><span>Velocidade média</span><strong>{{formatarMedida(telemetria.velocidadeMedia, 2, 'm/s')}}</strong><small>máx. {{formatarMedida(telemetria.velocidadeMax, 2, 'm/s')}}</small></div>
|
||||
<div class="story-kpi"><span>Comando médio</span><strong>{{formatarMedida(telemetria.comandoVelMedia, 1, '%')}}</strong><small>máx. {{formatarMedida(telemetria.comandoVelMax, 1, '%')}}</small></div>
|
||||
<div class="story-kpi"><span>Distância final</span><strong>{{formatarMedida(telemetria.distanciaFinal, 2, 'm')}}</strong><small>posição acumulada</small></div>
|
||||
<div class="story-kpi"><span>Deslocamento líquido</span><strong>{{formatarMedida(diagnosticoTrabalho.deslocamentoLiquido, 2, 'm')}}</strong><small>fim menos início</small></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-grid two-columns">
|
||||
<article class="telemetry-card wide-card" *ngIf="telemetria.temVelocidade || telemetria.temComandoVel">
|
||||
<div class="card-heading"><div><span>Resposta dinâmica</span><strong>Velocidade x comando</strong></div><small>m/s + %</small></div>
|
||||
<div class="chart-box large"><canvas id="chart_velocidade"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card wide-card" *ngIf="telemetria.temDistancia">
|
||||
<div class="card-heading"><div><span>Progressão</span><strong>Distância acumulada</strong></div><small>m</small></div>
|
||||
<div class="chart-box"><canvas id="chart_distancia"></canvas></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section" *ngIf="telemetria.temEstabilidade">
|
||||
<div class="story-heading">
|
||||
<div>
|
||||
<div class="eyebrow">ESTABILIDADE</div>
|
||||
<h2>Postura do robô durante o trabalho</h2>
|
||||
<p>Inclinação frontal e lateral agrupadas para revelar picos, terreno e momentos críticos.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-kpis">
|
||||
<div class="story-kpi"><span>Frontal média</span><strong>{{formatarMedida(telemetria.inclinacaoFrontalMedia, 2, '°')}}</strong><small>|máx.| {{formatarMedida(telemetria.inclinacaoFrontalMaxAbs, 2, '°')}}</small></div>
|
||||
<div class="story-kpi"><span>Lateral média</span><strong>{{formatarMedida(telemetria.inclinacaoLateralMedia, 2, '°')}}</strong><small>|máx.| {{formatarMedida(telemetria.inclinacaoLateralMaxAbs, 2, '°')}}</small></div>
|
||||
<div class="story-kpi"><span>|Roll| máximo</span><strong>{{formatarMedida(diagnosticoTrabalho.rollMaxAbs, 2, '°')}}</strong><small>schema novo</small></div>
|
||||
<div class="story-kpi"><span>|Pitch| máximo</span><strong>{{formatarMedida(diagnosticoTrabalho.pitchMaxAbs, 2, '°')}}</strong><small>schema novo</small></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-grid">
|
||||
<article class="telemetry-card wide-card" *ngIf="telemetria.temInclinacaoFrontal || telemetria.temInclinacaoLateral">
|
||||
<div class="card-heading"><div><span>Inclinação</span><strong>Frontal x lateral</strong></div><small>graus</small></div>
|
||||
<div class="chart-box large"><canvas id="chart_inclinacao"></canvas></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section" *ngIf="telemetria.temAmbiente">
|
||||
<div class="story-heading">
|
||||
<div>
|
||||
<div class="eyebrow">AMBIENTE</div>
|
||||
<h2>Condições ao redor do equipamento</h2>
|
||||
<p>Variáveis ambientais separadas por unidade para evitar escalas enganadoras.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="environment-grid">
|
||||
<article class="telemetry-card" *ngIf="telemetria.temTemperatura">
|
||||
<div class="card-heading"><div><span>Temperatura</span><strong>{{formatarMedida(telemetria.temperaturaMedia, 1, '°C')}}</strong></div><small>{{formatarMedida(telemetria.temperaturaMin, 1, '°C')}} → {{formatarMedida(telemetria.temperaturaMax, 1, '°C')}}</small></div>
|
||||
<div class="chart-box compact"><canvas id="chart_temperatura"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card" *ngIf="telemetria.temAltitude">
|
||||
<div class="card-heading"><div><span>Altitude</span><strong>{{formatarMedida(telemetria.altitudeMedia, 1, 'm')}}</strong></div><small>{{formatarMedida(telemetria.altitudeMin, 1, 'm')}} → {{formatarMedida(telemetria.altitudeMax, 1, 'm')}}</small></div>
|
||||
<div class="chart-box compact"><canvas id="chart_altitude"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card" *ngIf="telemetria.temUmidade">
|
||||
<div class="card-heading"><div><span>Umidade</span><strong>{{formatarMedida(telemetria.umidadeMedia, 1, '%')}}</strong></div><small>{{formatarMedida(telemetria.umidadeMin, 1, '%')}} → {{formatarMedida(telemetria.umidadeMax, 1, '%')}}</small></div>
|
||||
<div class="chart-box compact"><canvas id="chart_umidade"></canvas></div>
|
||||
</article>
|
||||
<article class="telemetry-card" *ngIf="telemetria.temGas">
|
||||
<div class="card-heading"><div><span>Gás</span><strong>{{formatarMedida(telemetria.gasMedia, 2, '')}}</strong></div><small>máx. {{formatarMedida(telemetria.gasMax, 2, '')}}</small></div>
|
||||
<div class="chart-box compact"><canvas id="chart_gas"></canvas></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section safety-section" *ngIf="telemetria.temSeguranca || diagnosticoTrabalho.mascaraSaudeUltima !== null">
|
||||
<div class="story-heading">
|
||||
<div>
|
||||
<div class="eyebrow">SEGURANÇA</div>
|
||||
<h2>Saúde e eventos críticos</h2>
|
||||
<p>Sinais que merecem leitura rápida, sem ocupar espaço com gráficos vazios.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="safety-grid">
|
||||
<div class="safety-card" [class.alarm]="diagnosticoTrabalho.infiltracaoDetectada">
|
||||
<span>Infiltração</span>
|
||||
<strong>{{diagnosticoTrabalho.infiltracaoDetectada ? 'ALARME DETECTADO' : 'Sem alarme'}}</strong>
|
||||
<small>raw máx. {{diagnosticoTrabalho.infiltracaoRawMax !== null ? diagnosticoTrabalho.infiltracaoRawMax : '-'}}</small>
|
||||
</div>
|
||||
<div class="safety-card">
|
||||
<span>Máscara de saúde</span>
|
||||
<strong>{{mascaraSaudeTexto()}}</strong>
|
||||
<small>estado final reportado</small>
|
||||
</div>
|
||||
<div class="safety-card" *ngIf="telemetria.temUltrassom">
|
||||
<span>Ultrassom</span>
|
||||
<strong>{{formatarMedida(telemetria.ultrassomMin, 2, '')}}</strong>
|
||||
<small>menor leitura da operação</small>
|
||||
</div>
|
||||
<div class="safety-card" *ngIf="!telemetria.temUltrassom">
|
||||
<span>Ultrassom</span>
|
||||
<strong>Sem dados</strong>
|
||||
<small>nenhuma amostra válida</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="telemetry-card safety-chart" *ngIf="telemetria.temUltrassom">
|
||||
<div class="card-heading"><div><span>Proximidade</span><strong>Ultrassom ao longo da operação</strong></div></div>
|
||||
<div class="chart-box compact"><canvas id="chart_ultrassom"></canvas></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="info-card compact-info-card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<div class="eyebrow">CONTEXTO</div>
|
||||
<h3>Equipamento e sessão</h3>
|
||||
</div>
|
||||
<span class="event-badge">schema {{resumoTrabalho.schemas}}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-grid equipment-grid">
|
||||
<div><span>NS</span><strong>{{FiltroSelecionado.robotNs || '-'}}</strong></div>
|
||||
<div><span>Software</span><strong>{{FiltroSelecionado.versao || '-'}}</strong></div>
|
||||
<div><span>Firmware</span><strong>{{FiltroSelecionado.firmwareVersaoStr || FiltroSelecionado.firmwareVersao || '-'}}</strong></div>
|
||||
<div><span>Protocolo</span><strong>{{FiltroSelecionado.protocoloVersao || '-'}}</strong></div>
|
||||
<div><span>Hardware</span><strong>{{FiltroSelecionado.revisaoHardware || '-'}}</strong></div>
|
||||
<div><span>Porta / Baud</span><strong>{{FiltroSelecionado.portaCOM || '-'}} / {{FiltroSelecionado.baudRate || '-'}}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="odometer-heading">
|
||||
<div>
|
||||
<span>ODÔMETROS DA SESSÃO</span>
|
||||
<strong>Inicial → final e variação capturada</strong>
|
||||
</div>
|
||||
<div class="odometer-duty" *ngIf="odometrosTrabalho.motores.delta !== null">
|
||||
<span>Motores ativos</span>
|
||||
<strong>{{SegundosParaTempo(odometrosTrabalho.motores.delta)}}</strong>
|
||||
<small *ngIf="odometrosTrabalho.movimentoPercentual !== null">{{formatarMedida(odometrosTrabalho.movimentoPercentual, 1, '%')}} do intervalo medido</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="odometer-grid">
|
||||
<article class="odometer-card">
|
||||
<div class="odometer-title">Total</div>
|
||||
<div class="odometer-values">
|
||||
<div><span>Inicial</span><strong>{{odometrosTrabalho.total.inicial !== null ? SegundosParaTempo(odometrosTrabalho.total.inicial) : '-'}}</strong></div>
|
||||
<div class="odometer-arrow">→</div>
|
||||
<div><span>Final</span><strong>{{odometrosTrabalho.total.final !== null ? SegundosParaTempo(odometrosTrabalho.total.final) : '-'}}</strong></div>
|
||||
</div>
|
||||
<div class="odometer-delta"><span>Δ sessão</span><strong>{{formatarDeltaOdometro(odometrosTrabalho.total)}}</strong></div>
|
||||
</article>
|
||||
|
||||
<article class="odometer-card">
|
||||
<div class="odometer-title">Conectado</div>
|
||||
<div class="odometer-values">
|
||||
<div><span>Inicial</span><strong>{{odometrosTrabalho.conectado.inicial !== null ? SegundosParaTempo(odometrosTrabalho.conectado.inicial) : '-'}}</strong></div>
|
||||
<div class="odometer-arrow">→</div>
|
||||
<div><span>Final</span><strong>{{odometrosTrabalho.conectado.final !== null ? SegundosParaTempo(odometrosTrabalho.conectado.final) : '-'}}</strong></div>
|
||||
</div>
|
||||
<div class="odometer-delta"><span>Δ sessão</span><strong>{{formatarDeltaOdometro(odometrosTrabalho.conectado)}}</strong></div>
|
||||
</article>
|
||||
|
||||
<article class="odometer-card emphasis">
|
||||
<div class="odometer-title">Motores / movimento</div>
|
||||
<div class="odometer-values">
|
||||
<div><span>Inicial</span><strong>{{odometrosTrabalho.motores.inicial !== null ? SegundosParaTempo(odometrosTrabalho.motores.inicial) : '-'}}</strong></div>
|
||||
<div class="odometer-arrow">→</div>
|
||||
<div><span>Final</span><strong>{{odometrosTrabalho.motores.final !== null ? SegundosParaTempo(odometrosTrabalho.motores.final) : '-'}}</strong></div>
|
||||
</div>
|
||||
<div class="odometer-delta"><span>Tempo em movimento</span><strong>{{formatarDeltaOdometro(odometrosTrabalho.motores)}}</strong></div>
|
||||
</article>
|
||||
|
||||
<article class="odometer-card">
|
||||
<div class="odometer-title">Parcial</div>
|
||||
<div class="odometer-values">
|
||||
<div><span>Inicial</span><strong>{{odometrosTrabalho.parcial.inicial !== null ? SegundosParaTempo(odometrosTrabalho.parcial.inicial) : '-'}}</strong></div>
|
||||
<div class="odometer-arrow">→</div>
|
||||
<div><span>Final</span><strong>{{odometrosTrabalho.parcial.final !== null ? SegundosParaTempo(odometrosTrabalho.parcial.final) : '-'}}</strong></div>
|
||||
</div>
|
||||
<div class="odometer-delta"><span>Δ sessão</span><strong>{{formatarDeltaOdometro(odometrosTrabalho.parcial)}}</strong></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card" *ngIf="FiltroSelecionado.servico">
|
||||
<div class="section-title"><div><div class="eyebrow">SERVIÇO</div><h3>Contexto do atendimento</h3></div></div>
|
||||
<div class="info-grid">
|
||||
<div><span>Nome</span><strong>{{FiltroSelecionado.servico.Nome || '-'}}</strong></div>
|
||||
<div><span>Cidade</span><strong>{{FiltroSelecionado.servico.Cidade || '-'}}</strong></div>
|
||||
<div><span>Responsável</span><strong>{{FiltroSelecionado.servico.Responsavel || '-'}}</strong></div>
|
||||
<div><span>Serviço</span><strong>{{FiltroSelecionado.servico.Servico || '-'}}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card location-card">
|
||||
<div class="section-title"><div><div class="eyebrow">LOCAL</div><h3>Posição registrada</h3></div></div>
|
||||
<div class="location-line">
|
||||
<strong>{{FiltroSelecionado.Coordenadas || 'Sem coordenadas'}}</strong>
|
||||
<span>{{FiltroSelecionado.Localizacao || 'Endereço não resolvido'}}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="advanced-panel" *ngIf="sensoresDiagnostico.length > 0 || diagnosticoTrabalho.logIdUltimo">
|
||||
<summary>
|
||||
<span>
|
||||
<span class="eyebrow">DIAGNÓSTICO AVANÇADO</span>
|
||||
<strong>Dados brutos e estatísticas dos sensores</strong>
|
||||
</span>
|
||||
<small>{{sensoresDiagnostico.length}} sinais com dados</small>
|
||||
</summary>
|
||||
|
||||
<div class="advanced-content">
|
||||
<div class="diagnostic-lines" *ngIf="diagnosticoTrabalho.capacidades || diagnosticoTrabalho.logIdUltimo || diagnosticoTrabalho.mensagensEventos.length > 0">
|
||||
<div *ngIf="diagnosticoTrabalho.capacidades"><span>Capacidades</span><strong>{{diagnosticoTrabalho.capacidades}}</strong></div>
|
||||
<div *ngIf="diagnosticoTrabalho.logIdUltimo"><span>Último log ID</span><strong class="mono">{{diagnosticoTrabalho.logIdUltimo}}</strong></div>
|
||||
<div *ngIf="diagnosticoTrabalho.mensagensEventos.length > 0">
|
||||
<span>Mensagens de evento</span>
|
||||
<strong *ngFor="let mensagem of diagnosticoTrabalho.mensagensEventos">{{mensagem}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="diagnostic-table-wrap" *ngIf="sensoresDiagnostico.length > 0">
|
||||
<table class="diagnostic-table">
|
||||
<thead>
|
||||
<tr><th>Sinal</th><th>Mín.</th><th>Média</th><th>Mediana</th><th>Máx.</th><th>Último</th><th>Amostras</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of sensoresDiagnostico">
|
||||
<td><strong>{{item.nome}}</strong><small>ID {{item.sensor.sensor}}</small></td>
|
||||
<td>{{item.sensor.minimo | number:'1.0-3'}}</td>
|
||||
<td>{{item.sensor.media | number:'1.0-3'}}</td>
|
||||
<td>{{item.sensor.mediana | number:'1.0-3'}}</td>
|
||||
<td>{{item.sensor.maximo | number:'1.0-3'}}</td>
|
||||
<td>{{item.sensor.ultimo | number:'1.0-3'}}</td>
|
||||
<td>{{item.sensor.quantidadeAmostras || 0}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<section class="state-card" *ngIf="!temDadosTelemetria()">
|
||||
Esta operação não possui séries de telemetria suficientes para montar os painéis.
|
||||
</section>
|
||||
</ng-container>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,877 @@
|
|||
#divChart{
|
||||
display: block;
|
||||
width: 90%;
|
||||
height: auto;
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background: #eef2f5;
|
||||
color: #1c2730;
|
||||
}
|
||||
|
||||
.divChart{
|
||||
display: block;
|
||||
width: 30%;
|
||||
position: relative;
|
||||
float: left;
|
||||
margin-right: 3%;
|
||||
height: 350px;
|
||||
.dashboard-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 304px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.TabelaDados {
|
||||
padding: 20px;
|
||||
.filters-panel {
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 28px 22px;
|
||||
background: linear-gradient(180deg, #172129 0%, #202d36 100%);
|
||||
color: #eef3f6;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.DivGraficoFora {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
.brand-block h2,
|
||||
.hero-card h1,
|
||||
.section-title h3,
|
||||
.story-heading h2 {
|
||||
margin: 4px 0 0;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.brand-block p,
|
||||
.hero-card p,
|
||||
.story-heading p {
|
||||
margin: 8px 0 0;
|
||||
color: #9eafb9;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.14em;
|
||||
font-weight: 800;
|
||||
color: #718896;
|
||||
}
|
||||
|
||||
.filter-block {
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.filter-block label {
|
||||
display: block;
|
||||
margin: 10px 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
color: #c7d1d8;
|
||||
}
|
||||
|
||||
.filters-panel mat-form-field,
|
||||
.full-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
margin-top: 16px;
|
||||
color: #d7e0e6;
|
||||
}
|
||||
|
||||
.side-summary {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.side-summary > div {
|
||||
padding: 12px;
|
||||
border-radius: 11px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.side-summary strong,
|
||||
.side-summary span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.side-summary strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.side-summary span {
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: #aebbc4;
|
||||
}
|
||||
|
||||
.side-summary .warning {
|
||||
grid-column: 1 / -1;
|
||||
background: rgba(255, 176, 32, 0.12);
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
box-sizing: border-box;
|
||||
padding: 28px;
|
||||
min-width: 0;
|
||||
max-width: 1600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.info-card,
|
||||
.state-card,
|
||||
.story-section,
|
||||
.advanced-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #dfe6ea;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 5px 18px rgba(20, 34, 46, 0.045);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 22px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-card h1 {
|
||||
font-size: 28px;
|
||||
color: #16242d;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-pill,
|
||||
.event-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 7px 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
background: #edf1f4;
|
||||
color: #5d6b75;
|
||||
}
|
||||
|
||||
.status-pill.online {
|
||||
background: #e5f6ec;
|
||||
color: #25764a;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
min-width: 0;
|
||||
padding: 15px 16px;
|
||||
border-radius: 13px;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e7eb;
|
||||
box-shadow: 0 3px 12px rgba(20, 34, 46, 0.03);
|
||||
}
|
||||
|
||||
.metric-card.emphasis {
|
||||
background: #1e2b34;
|
||||
border-color: #1e2b34;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.metric-card strong,
|
||||
.metric-card small,
|
||||
.story-kpi span,
|
||||
.story-kpi strong,
|
||||
.story-kpi small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.story-kpi span {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #75848f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-card.emphasis span,
|
||||
.metric-card.emphasis small {
|
||||
color: #b7c5cd;
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
margin-top: 7px;
|
||||
font-size: 19px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.metric-card small,
|
||||
.story-kpi small {
|
||||
margin-top: 4px;
|
||||
color: #8d9aa3;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.story-section {
|
||||
margin-top: 18px;
|
||||
padding: 22px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.story-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.story-heading h2 {
|
||||
font-size: 21px;
|
||||
color: #1d2a33;
|
||||
}
|
||||
|
||||
.story-heading p {
|
||||
color: #74838d;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.story-kpis {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(130px, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.story-kpi {
|
||||
min-width: 0;
|
||||
border-radius: 12px;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #edf1f3;
|
||||
padding: 13px 14px;
|
||||
}
|
||||
|
||||
.story-kpi strong {
|
||||
margin-top: 5px;
|
||||
font-size: 18px;
|
||||
color: #263640;
|
||||
}
|
||||
|
||||
.chart-grid {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chart-grid.two-columns {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.telemetry-card {
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
border-radius: 13px;
|
||||
background: #fbfcfd;
|
||||
border: 1px solid #e7ecef;
|
||||
}
|
||||
|
||||
.telemetry-card.wide-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
min-height: 42px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-heading span,
|
||||
.card-heading strong,
|
||||
.card-heading small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-heading span {
|
||||
color: #81909a;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.card-heading strong {
|
||||
margin-top: 3px;
|
||||
color: #263640;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.card-heading small {
|
||||
color: #8997a0;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 255px;
|
||||
}
|
||||
|
||||
.chart-box.large {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.chart-box.compact {
|
||||
height: 210px;
|
||||
}
|
||||
|
||||
.environment-grid {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.safety-section {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfcfd 100%);
|
||||
}
|
||||
|
||||
.safety-grid {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.safety-card {
|
||||
padding: 15px 16px;
|
||||
border-radius: 13px;
|
||||
background: #eef8f2;
|
||||
border: 1px solid #d9eee1;
|
||||
}
|
||||
|
||||
.safety-card.alarm {
|
||||
background: #fff0f0;
|
||||
border-color: #f1cece;
|
||||
}
|
||||
|
||||
.safety-card span,
|
||||
.safety-card strong,
|
||||
.safety-card small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.safety-card span {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #75848f;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.safety-card strong {
|
||||
margin-top: 6px;
|
||||
color: #24613f;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.safety-card.alarm strong {
|
||||
color: #a33b3b;
|
||||
}
|
||||
|
||||
.safety-card small {
|
||||
margin-top: 4px;
|
||||
color: #82908a;
|
||||
}
|
||||
|
||||
.safety-chart {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-top: 16px;
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.compact-info-card {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.info-grid > div {
|
||||
padding: 11px 13px;
|
||||
border-radius: 9px;
|
||||
background: #f7f9fa;
|
||||
}
|
||||
|
||||
.info-grid span,
|
||||
.info-grid strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info-grid span {
|
||||
font-size: 10px;
|
||||
color: #7d8b95;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.info-grid strong {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
::ng-deep .highlight-date .mat-calendar-body-cell-content {
|
||||
background-color: #ffcc80; /* Cor de destaque */
|
||||
border-radius: 50%;
|
||||
}
|
||||
.odometer-heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: 20px 0 10px;
|
||||
padding-top: 17px;
|
||||
border-top: 1px solid #e8edf0;
|
||||
}
|
||||
|
||||
.odometer-heading > div:first-child span,
|
||||
.odometer-duty span {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #7d8b95;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.odometer-heading > div:first-child strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.odometer-duty {
|
||||
min-width: 190px;
|
||||
padding: 10px 13px;
|
||||
border-radius: 10px;
|
||||
background: #eef8f3;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.odometer-duty strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
color: #177a4b;
|
||||
}
|
||||
|
||||
.odometer-duty small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #678377;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.odometer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(190px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.odometer-card {
|
||||
padding: 13px 14px;
|
||||
border: 1px solid #e6ecef;
|
||||
border-radius: 11px;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
|
||||
.odometer-card.emphasis {
|
||||
background: #f2faf6;
|
||||
border-color: #d5ebdf;
|
||||
}
|
||||
|
||||
.odometer-title {
|
||||
margin-bottom: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #44545e;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.odometer-values {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: end;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.odometer-values span,
|
||||
.odometer-delta span {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
color: #8a979f;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.odometer-values strong {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.odometer-arrow {
|
||||
padding-bottom: 1px;
|
||||
color: #9aa6ad;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.odometer-delta {
|
||||
margin-top: 10px;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid #e9edef;
|
||||
}
|
||||
|
||||
.odometer-delta strong {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 15px;
|
||||
color: #172b38;
|
||||
}
|
||||
|
||||
.odometer-card.emphasis .odometer-delta strong {
|
||||
color: #177a4b;
|
||||
}
|
||||
|
||||
.location-line {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.location-line span {
|
||||
color: #697781;
|
||||
}
|
||||
|
||||
.advanced-panel {
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.advanced-panel summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
padding: 18px 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.advanced-panel summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.advanced-panel summary > span,
|
||||
.advanced-panel summary strong,
|
||||
.advanced-panel summary small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.advanced-panel summary strong {
|
||||
margin-top: 3px;
|
||||
color: #2e3e48;
|
||||
}
|
||||
|
||||
.advanced-panel summary small {
|
||||
color: #84919a;
|
||||
}
|
||||
|
||||
.advanced-panel[open] summary {
|
||||
border-bottom: 1px solid #e5eaed;
|
||||
}
|
||||
|
||||
.advanced-content {
|
||||
padding: 18px 20px 22px;
|
||||
}
|
||||
|
||||
.diagnostic-lines {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.diagnostic-lines > div {
|
||||
padding: 11px 13px;
|
||||
border-radius: 9px;
|
||||
background: #f7f9fa;
|
||||
}
|
||||
|
||||
.diagnostic-lines span,
|
||||
.diagnostic-lines strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.diagnostic-lines span {
|
||||
font-size: 10px;
|
||||
color: #7d8b95;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.diagnostic-lines strong + strong {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.diagnostic-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.diagnostic-table {
|
||||
width: 100%;
|
||||
min-width: 760px;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.diagnostic-table th,
|
||||
.diagnostic-table td {
|
||||
padding: 10px 11px;
|
||||
border-bottom: 1px solid #e8edf0;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.diagnostic-table th {
|
||||
color: #75848f;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.diagnostic-table th:first-child,
|
||||
.diagnostic-table td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diagnostic-table td:first-child strong,
|
||||
.diagnostic-table td:first-child small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.diagnostic-table td:first-child small {
|
||||
margin-top: 2px;
|
||||
color: #9aa5ac;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
padding: 22px;
|
||||
color: #66757f;
|
||||
}
|
||||
|
||||
.state-card.error {
|
||||
border-color: #f0c9c9;
|
||||
background: #fff8f8;
|
||||
color: #9b3535;
|
||||
}
|
||||
|
||||
.state-card.error strong,
|
||||
.state-card.error span,
|
||||
.selection-hint strong,
|
||||
.selection-hint span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.state-card.error span,
|
||||
.selection-hint span {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.selection-hint span {
|
||||
color: #84919a;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin: -6px 0 14px;
|
||||
font-size: 11px;
|
||||
color: #aebbc4;
|
||||
}
|
||||
|
||||
.calendar-legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.legend-dot.v2 {
|
||||
background: #43a047;
|
||||
box-shadow: 0 0 0 2px rgba(67, 160, 71, 0.18);
|
||||
}
|
||||
|
||||
.legend-dot.legacy {
|
||||
background: #ffb74d;
|
||||
box-shadow: 0 0 0 2px rgba(255, 183, 77, 0.18);
|
||||
}
|
||||
|
||||
::ng-deep .filters-panel .mat-form-field-label,
|
||||
::ng-deep .filters-panel .mat-select-value,
|
||||
::ng-deep .filters-panel input.mat-input-element,
|
||||
::ng-deep .filters-panel .mat-datepicker-toggle {
|
||||
color: #e8eef2 !important;
|
||||
}
|
||||
|
||||
::ng-deep .filters-panel .mat-form-field-outline {
|
||||
color: rgba(255, 255, 255, 0.18) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-calendar-body-cell.orion-date-v2 .mat-calendar-body-cell-content,
|
||||
::ng-deep .orion-date-v2 .mat-calendar-body-cell-content {
|
||||
background: #43a047 !important;
|
||||
color: #fff !important;
|
||||
font-weight: 700 !important;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-calendar-body-cell.orion-date-legacy .mat-calendar-body-cell-content,
|
||||
::ng-deep .orion-date-legacy .mat-calendar-body-cell-content {
|
||||
background: #ffcc80 !important;
|
||||
color: #263238 !important;
|
||||
font-weight: 700 !important;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.story-kpis,
|
||||
.info-grid {
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.dashboard-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filters-panel {
|
||||
min-height: auto;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.chart-grid.two-columns,
|
||||
.environment-grid,
|
||||
.safety-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.telemetry-card.wide-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.metric-grid,
|
||||
.story-kpis,
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.story-section {
|
||||
padding: 17px;
|
||||
}
|
||||
|
||||
.chart-box,
|
||||
.chart-box.large {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.metric-grid,
|
||||
.story-kpis,
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.odometer-grid {
|
||||
grid-template-columns: repeat(2, minmax(190px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.odometer-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.odometer-duty {
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.odometer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,361 @@
|
|||
<div class="prontuario-page">
|
||||
|
||||
<div class="loading-main" *ngIf="carregandoProntuario && !prontuario">
|
||||
<div class="loading-bar"></div>
|
||||
<span>Montando prontuário do equipamento...</span>
|
||||
</div>
|
||||
|
||||
<div class="error-main" *ngIf="!carregandoProntuario && erroProntuario">
|
||||
<strong>Não foi possível abrir o prontuário.</strong>
|
||||
<p>{{ erroProntuario }}</p>
|
||||
<div class="error-actions">
|
||||
<button mat-stroked-button type="button" (click)="voltar()">Voltar</button>
|
||||
<button mat-raised-button color="primary" type="button" (click)="carregarProntuario()">
|
||||
Tentar novamente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="prontuario">
|
||||
<header class="hero-card">
|
||||
<div class="hero-main">
|
||||
<button mat-button type="button" class="back-button" (click)="voltar()">
|
||||
← Equipamentos
|
||||
</button>
|
||||
|
||||
<div class="identity-row">
|
||||
<div class="robot-mark">OT</div>
|
||||
<div>
|
||||
<span class="eyebrow">PRONTUÁRIO TÉCNICO</span>
|
||||
<h1>{{ equipamento.nome || equipamento.modelo_nome }}</h1>
|
||||
<div class="identity-meta">
|
||||
<span class="serial-chip">NS {{ equipamento.numero_serie }}</span>
|
||||
<span>{{ equipamento.modelo_nome || 'Modelo não informado' }}</span>
|
||||
<span *ngIf="equipamento.versao">Versão {{ equipamento.versao }}</span>
|
||||
<span *ngIf="equipamento.codigo_interno">{{ equipamento.codigo_interno }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-side">
|
||||
<span class="hero-side-label">Cliente atual</span>
|
||||
<strong>{{ equipamento.cliente_nome || 'Sem cliente vinculado' }}</strong>
|
||||
<small *ngIf="equipamento.cliente_razao_social">
|
||||
{{ equipamento.cliente_razao_social }}
|
||||
</small>
|
||||
<button mat-stroked-button type="button" (click)="atualizar()" [disabled]="carregandoProntuario || carregandoTimeline">
|
||||
Atualizar prontuário
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="metrics-grid usage-grid">
|
||||
<article class="metric-card metric-primary">
|
||||
<span>Movimento vitalício</span>
|
||||
<strong>{{ indicadores.horas_movimento | number:'1.1-1' }} h</strong>
|
||||
<small>Odômetro principal do equipamento</small>
|
||||
</article>
|
||||
|
||||
<article class="metric-card">
|
||||
<span>Tempo conectado</span>
|
||||
<strong>{{ indicadores.horas_conectado | number:'1.1-1' }} h</strong>
|
||||
<small>Total acumulado ligado</small>
|
||||
</article>
|
||||
|
||||
<article class="metric-card">
|
||||
<span>Odômetro parcial</span>
|
||||
<strong>{{ indicadores.horas_parcial | number:'1.1-1' }} h</strong>
|
||||
<small>Desde o último reset parcial</small>
|
||||
</article>
|
||||
|
||||
<article class="metric-card">
|
||||
<span>Desde a última saída</span>
|
||||
<strong *ngIf="indicadores.movimento_desde_ultima_saida_segundos !== null && indicadores.movimento_desde_ultima_saida_segundos !== undefined">
|
||||
{{ horasSegundos(indicadores.movimento_desde_ultima_saida_segundos) | number:'1.1-1' }} h
|
||||
</strong>
|
||||
<strong *ngIf="indicadores.movimento_desde_ultima_saida_segundos === null || indicadores.movimento_desde_ultima_saida_segundos === undefined">—</strong>
|
||||
<small>Movimento após a manutenção</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="overview-grid">
|
||||
<section class="overview-card maintenance-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="section-kicker">MANUTENÇÃO</span>
|
||||
<h2>Resumo de atendimentos</h2>
|
||||
</div>
|
||||
<span class="big-number">{{ indicadores.quantidade_os_efetivas }}</span>
|
||||
</div>
|
||||
|
||||
<div class="maintenance-stats">
|
||||
<div><strong>{{ indicadores.quantidade_os_abertas }}</strong><span>OS abertas</span></div>
|
||||
<div><strong>{{ indicadores.quantidade_corretivas }}</strong><span>Corretivas</span></div>
|
||||
<div><strong>{{ indicadores.quantidade_preventivas }}</strong><span>Preventivas</span></div>
|
||||
<div>
|
||||
<strong *ngIf="indicadores.media_movimento_entre_manutencoes_segundos !== null && indicadores.media_movimento_entre_manutencoes_segundos !== undefined">
|
||||
{{ horasSegundos(indicadores.media_movimento_entre_manutencoes_segundos) | number:'1.0-1' }} h
|
||||
</strong>
|
||||
<strong *ngIf="indicadores.media_movimento_entre_manutencoes_segundos === null || indicadores.media_movimento_entre_manutencoes_segundos === undefined">—</strong>
|
||||
<span>Média entre manutenções</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="last-maintenance" *ngIf="prontuario.ultima_os">
|
||||
<div>
|
||||
<span>Última ordem de serviço</span>
|
||||
<strong>OS #{{ prontuario.ultima_os.id }}</strong>
|
||||
<small>{{ prontuario.ultima_os.data_entrada | date:'dd/MM/yyyy HH:mm' }}</small>
|
||||
</div>
|
||||
<span class="status-badge" [ngClass]="classeStatus(prontuario.ultima_os.status)">
|
||||
{{ labelStatus(prontuario.ultima_os.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overview-card financial-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="section-kicker">CUSTO DE VIDA</span>
|
||||
<h2>Histórico financeiro</h2>
|
||||
</div>
|
||||
<strong class="total-cost">{{ indicadores.valor_total | currency:'BRL':'symbol':'1.2-2' }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="financial-rows">
|
||||
<div><span>Mão de obra</span><strong>{{ indicadores.valor_mao_obra | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div><span>Peças e insumos</span><strong>{{ indicadores.valor_insumos | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div><span>Acréscimos</span><strong>{{ indicadores.valor_acrescimo | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div><span>Descontos</span><strong>- {{ indicadores.valor_desconto | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div class="financial-divider"><span>Recebido</span><strong>{{ indicadores.valor_pago | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div [class.pending-value]="indicadores.valor_pendente > 0"><span>Pendente</span><strong>{{ indicadores.valor_pendente | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="financial-footer">
|
||||
<span>Média por OS: <strong>{{ indicadores.custo_medio_por_os | currency:'BRL':'symbol':'1.2-2' }}</strong></span>
|
||||
<span *ngIf="indicadores.custo_por_hora_movimento !== null && indicadores.custo_por_hora_movimento !== undefined">
|
||||
Por hora em movimento: <strong>{{ indicadores.custo_por_hora_movimento | currency:'BRL':'symbol':'1.2-2' }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="history-section">
|
||||
<div class="history-heading">
|
||||
<div>
|
||||
<span class="section-kicker">LINHA DO TEMPO</span>
|
||||
<h2>Histórico completo do equipamento</h2>
|
||||
<p>Manutenções, peças, mão de obra, odômetros e alterações de status.</p>
|
||||
</div>
|
||||
<span class="history-count">{{ totalRegistros }} OS</span>
|
||||
</div>
|
||||
|
||||
<div class="filters-card">
|
||||
<div class="filters-grid">
|
||||
<mat-form-field appearance="outline" class="field-search">
|
||||
<mat-label>Buscar no histórico</mat-label>
|
||||
<input matInput [(ngModel)]="busca" (keyup.enter)="aplicarFiltros()" placeholder="Motor, conversor RS485, diagnóstico, peça...">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tipo</mat-label>
|
||||
<mat-select [(ngModel)]="tipoSelecionado">
|
||||
<mat-option *ngFor="let opcao of tipoOpcoes" [value]="opcao.valor">{{ opcao.texto }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Status</mat-label>
|
||||
<mat-select [(ngModel)]="statusSelecionado">
|
||||
<mat-option *ngFor="let opcao of statusOpcoes" [value]="opcao.valor">{{ opcao.texto }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Data inicial</mat-label>
|
||||
<input matInput [matDatepicker]="pickerInicio" [(ngModel)]="dataInicio">
|
||||
<mat-datepicker-toggle matSuffix [for]="pickerInicio"></mat-datepicker-toggle>
|
||||
<mat-datepicker #pickerInicio></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Data final</mat-label>
|
||||
<input matInput [matDatepicker]="pickerFim" [(ngModel)]="dataFim">
|
||||
<mat-datepicker-toggle matSuffix [for]="pickerFim"></mat-datepicker-toggle>
|
||||
<mat-datepicker #pickerFim></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Peças e insumos</mat-label>
|
||||
<mat-select [(ngModel)]="somenteComMateriais">
|
||||
<mat-option *ngFor="let opcao of materiaisOpcoes" [value]="opcao.valor">{{ opcao.texto }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="filters-actions">
|
||||
<button mat-button type="button" (click)="limparFiltros()" [disabled]="carregandoTimeline">Limpar</button>
|
||||
<button mat-raised-button color="primary" type="button" (click)="aplicarFiltros()" [disabled]="carregandoTimeline">Pesquisar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-loading" *ngIf="carregandoTimeline">
|
||||
<div class="loading-bar"></div>
|
||||
<span>Consultando histórico...</span>
|
||||
</div>
|
||||
|
||||
<div class="timeline-error" *ngIf="!carregandoTimeline && erroTimeline">
|
||||
<strong>Não foi possível carregar a linha do tempo.</strong>
|
||||
<p>{{ erroTimeline }}</p>
|
||||
<button mat-stroked-button type="button" (click)="carregarTimeline()">Tentar novamente</button>
|
||||
</div>
|
||||
|
||||
<div class="timeline-empty" *ngIf="!carregandoTimeline && !erroTimeline && timeline.length === 0">
|
||||
<strong>Nenhum evento encontrado.</strong>
|
||||
<p>Altere os filtros ou confira se já existem OSs para o equipamento.</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline" *ngIf="!carregandoTimeline && !erroTimeline && timeline.length > 0">
|
||||
<article class="timeline-event" *ngFor="let ordem of timeline; trackBy: trackById">
|
||||
<div class="timeline-marker">
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div class="os-card" [class.expanded]="ordemExpandida(ordem.id)">
|
||||
<button class="os-card-header" type="button" (click)="alternarOrdem(ordem.id)">
|
||||
<div class="os-date">
|
||||
<strong>{{ ordem.data_entrada | date:'dd' }}</strong>
|
||||
<span>{{ ordem.data_entrada | date:'MMM yyyy' }}</span>
|
||||
<small>{{ ordem.data_entrada | date:'HH:mm' }}</small>
|
||||
</div>
|
||||
|
||||
<div class="os-title">
|
||||
<div class="os-title-line">
|
||||
<strong>OS #{{ ordem.id }}</strong>
|
||||
<span class="type-chip">{{ labelTipo(ordem.tipo) }}</span>
|
||||
<span class="status-badge" [ngClass]="classeStatus(ordem.status)">{{ labelStatus(ordem.status) }}</span>
|
||||
</div>
|
||||
<p>{{ ordem.problema_relatado || 'Sem problema relatado.' }}</p>
|
||||
<div class="os-resume">
|
||||
<span>{{ ordem.atividades.length }} atividade(s)</span>
|
||||
<span>{{ quantidadeMateriais(ordem) }} peça(s)/insumo(s)</span>
|
||||
<span>{{ totalHorasOrdem(ordem) | number:'1.2-2' }} h de trabalho</span>
|
||||
<span>{{ ordem.valor_total | currency:'BRL':'symbol':'1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="expand-icon">{{ ordemExpandida(ordem.id) ? '−' : '+' }}</span>
|
||||
</button>
|
||||
|
||||
<div class="os-card-body" *ngIf="ordemExpandida(ordem.id)">
|
||||
<div class="os-actions">
|
||||
<button mat-stroked-button color="primary" type="button" (click)="abrirOrdem(ordem)">Abrir OS completa</button>
|
||||
</div>
|
||||
|
||||
<div class="narrative-grid">
|
||||
<div class="narrative-item">
|
||||
<span>Problema relatado</span>
|
||||
<p>{{ ordem.problema_relatado || 'Não informado.' }}</p>
|
||||
</div>
|
||||
<div class="narrative-item">
|
||||
<span>Diagnóstico</span>
|
||||
<p>{{ ordem.diagnostico || 'Não informado.' }}</p>
|
||||
</div>
|
||||
<div class="narrative-item">
|
||||
<span>Solução</span>
|
||||
<p>{{ ordem.solucao_resumo || 'Não informada.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="odometer-pair" *ngIf="leituraEntrada(ordem) || leituraSaida(ordem)">
|
||||
<div *ngIf="leituraEntrada(ordem)">
|
||||
<span>Entrada na manutenção</span>
|
||||
<strong>{{ horasSegundos(leituraEntrada(ordem).odometro_movimento_segundos) | number:'1.1-1' }} h</strong>
|
||||
<small>Movimento · parcial {{ horasSegundos(leituraEntrada(ordem).odometro_parcial_segundos) | number:'1.1-1' }} h</small>
|
||||
</div>
|
||||
<div *ngIf="leituraSaida(ordem)">
|
||||
<span>Saída da manutenção</span>
|
||||
<strong>{{ horasSegundos(leituraSaida(ordem).odometro_movimento_segundos) | number:'1.1-1' }} h</strong>
|
||||
<small>Movimento · parcial {{ horasSegundos(leituraSaida(ordem).odometro_parcial_segundos) | number:'1.1-1' }} h</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="detail-section" *ngIf="ordem.atividades.length">
|
||||
<h3>Atividades executadas</h3>
|
||||
<div class="activity-list">
|
||||
<article class="activity-card" *ngFor="let atividade of ordem.atividades; let i = index">
|
||||
<div class="activity-number">{{ i + 1 }}</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-header">
|
||||
<strong>{{ atividade.descricao }}</strong>
|
||||
<span>{{ atividade.data_atividade | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="activity-meta">
|
||||
<span>{{ atividade.horas_expediente | number:'1.2-2' }} h expediente</span>
|
||||
<span *ngIf="atividade.horas_extras > 0">{{ atividade.horas_extras | number:'1.2-2' }} h extras</span>
|
||||
<span>{{ atividade.valor_mao_obra | currency:'BRL':'symbol':'1.2-2' }} de mão de obra</span>
|
||||
</div>
|
||||
|
||||
<div class="technicians" *ngIf="atividade.tecnicos.length">
|
||||
<span *ngFor="let tecnico of atividade.tecnicos">
|
||||
{{ tecnico.tecnico_nome }} · {{ tecnico.papel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="activity-notes" *ngIf="atividade.observacoes_cliente">{{ atividade.observacoes_cliente }}</p>
|
||||
|
||||
<div class="materials-table" *ngIf="atividade.materiais.length">
|
||||
<div class="material-row material-header">
|
||||
<span>Peça / insumo</span><span>Qtd.</span><span>Unitário</span><span>Total</span>
|
||||
</div>
|
||||
<div class="material-row" *ngFor="let material of atividade.materiais">
|
||||
<span><strong>{{ material.descricao_snapshot }}</strong><small *ngIf="material.observacoes">{{ material.observacoes }}</small></span>
|
||||
<span>{{ material.quantidade | number:'1.0-2' }} {{ material.unidade_snapshot }}</span>
|
||||
<span>{{ material.valor_cobrado_unitario | currency:'BRL':'symbol':'1.2-2' }}</span>
|
||||
<span>{{ valorMaterial(material) | currency:'BRL':'symbol':'1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="bottom-details">
|
||||
<section class="detail-section status-history" *ngIf="ordem.historico_status.length">
|
||||
<h3>Evolução do status</h3>
|
||||
<div class="status-step" *ngFor="let item of ordem.historico_status">
|
||||
<span class="status-dot"></span>
|
||||
<div>
|
||||
<strong>{{ labelStatus(item.status_novo) }}</strong>
|
||||
<small>{{ item.created_at | date:'dd/MM/yyyy HH:mm' }}</small>
|
||||
<p *ngIf="item.observacoes">{{ item.observacoes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section order-financial">
|
||||
<h3>Fechamento da OS</h3>
|
||||
<div><span>Mão de obra</span><strong>{{ ordem.valor_mao_obra | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div><span>Peças e insumos</span><strong>{{ ordem.valor_insumos | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
<div class="order-total"><span>Total</span><strong>{{ ordem.valor_total | currency:'BRL':'symbol':'1.2-2' }}</strong></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<mat-paginator
|
||||
*ngIf="!erroTimeline && totalRegistros > 0"
|
||||
[length]="totalRegistros"
|
||||
[pageIndex]="paginaAtual - 1"
|
||||
[pageSize]="tamanhoPagina"
|
||||
[pageSizeOptions]="opcoesTamanhoPagina"
|
||||
[showFirstLastButtons]="true"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</section>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,733 @@
|
|||
:host {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: #f4f6f8;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.prontuario-page {
|
||||
max-width: 1680px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.metric-card,
|
||||
.overview-card,
|
||||
.history-section,
|
||||
.filters-card,
|
||||
.os-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e4e7ec;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 14px rgba(16, 24, 40, 0.045);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 28px;
|
||||
padding: 22px 24px;
|
||||
margin-bottom: 18px;
|
||||
background: linear-gradient(125deg, #ffffff 0%, #f8fbff 72%, #eef6ff 100%);
|
||||
}
|
||||
|
||||
.hero-main { min-width: 0; }
|
||||
|
||||
.back-button {
|
||||
margin: -7px 0 10px -12px;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.identity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 3px 0 8px;
|
||||
color: #101828;
|
||||
font-size: 30px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.robot-mark {
|
||||
flex: 0 0 58px;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(145deg, #1769aa, #0d4e83);
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 8px 18px rgba(23, 105, 170, 0.22);
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.section-kicker {
|
||||
color: #1769aa;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
.identity-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 14px;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.serial-chip,
|
||||
.type-chip {
|
||||
display: inline-flex;
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
background: #eff8ff;
|
||||
color: #175cd3;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.hero-side {
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
text-align: right;
|
||||
|
||||
strong { color: #101828; font-size: 17px; }
|
||||
small { color: #98a2b3; }
|
||||
button { margin-top: 12px; }
|
||||
}
|
||||
|
||||
.hero-side-label {
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 17px 19px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 4px;
|
||||
background: #98a2b3;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin: 5px 0 3px;
|
||||
color: #101828;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
small { color: #98a2b3; font-size: 11px; }
|
||||
}
|
||||
|
||||
.metric-primary::before { background: #1769aa; }
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
gap: 18px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.overview-card { padding: 20px; }
|
||||
|
||||
.section-heading,
|
||||
.history-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 4px 0 0;
|
||||
color: #101828;
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.big-number {
|
||||
color: #1769aa;
|
||||
font-size: 34px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.maintenance-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
|
||||
div {
|
||||
padding: 13px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
color: #101828;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
span { color: #667085; font-size: 11px; }
|
||||
}
|
||||
|
||||
.last-maintenance {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eaecf0;
|
||||
|
||||
> div { display: flex; flex-direction: column; gap: 2px; }
|
||||
span:first-child, small { color: #98a2b3; font-size: 11px; }
|
||||
strong { color: #344054; }
|
||||
}
|
||||
|
||||
.total-cost {
|
||||
color: #027a48;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.financial-rows {
|
||||
margin-top: 16px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 8px 0;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
|
||||
strong { color: #344054; }
|
||||
}
|
||||
}
|
||||
|
||||
.financial-divider {
|
||||
margin-top: 5px;
|
||||
padding-top: 13px !important;
|
||||
border-top: 1px solid #eaecf0;
|
||||
}
|
||||
|
||||
.pending-value strong { color: #b42318 !important; }
|
||||
|
||||
.financial-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 20px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 9px;
|
||||
background: #f8fafc;
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
|
||||
strong { color: #344054; }
|
||||
}
|
||||
|
||||
.history-section { padding: 22px; }
|
||||
|
||||
.history-heading {
|
||||
margin-bottom: 18px;
|
||||
|
||||
p { margin: 5px 0 0; color: #667085; }
|
||||
}
|
||||
|
||||
.history-count {
|
||||
padding: 6px 11px;
|
||||
border-radius: 999px;
|
||||
background: #f2f4f7;
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
padding: 17px 17px 12px;
|
||||
margin-bottom: 22px;
|
||||
background: #f8fafc;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(270px, 1.8fr) repeat(5, minmax(145px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.filters-grid .mat-form-field { width: 100%; }
|
||||
|
||||
.filters-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 25px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 7px;
|
||||
width: 2px;
|
||||
background: #dbe7f1;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-event {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
top: 27px;
|
||||
left: -25px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2px solid #1769aa;
|
||||
z-index: 1;
|
||||
|
||||
span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1769aa;
|
||||
}
|
||||
}
|
||||
|
||||
.os-card { overflow: hidden; box-shadow: 0 2px 8px rgba(16, 24, 40, 0.035); }
|
||||
.os-card.expanded { border-color: #b7d6ef; box-shadow: 0 7px 18px rgba(23, 105, 170, 0.08); }
|
||||
|
||||
.os-card-header {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 78px 1fr 32px;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 15px 17px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.os-date {
|
||||
padding-right: 14px;
|
||||
border-right: 1px solid #eaecf0;
|
||||
text-align: center;
|
||||
|
||||
strong { display: block; color: #101828; font-size: 24px; line-height: 1; }
|
||||
span { display: block; margin-top: 3px; color: #475467; font-size: 11px; text-transform: uppercase; }
|
||||
small { color: #98a2b3; }
|
||||
}
|
||||
|
||||
.os-title {
|
||||
min-width: 0;
|
||||
|
||||
> p {
|
||||
margin: 7px 0 5px;
|
||||
color: #475467;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.os-title-line,
|
||||
.os-resume {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 7px 12px;
|
||||
}
|
||||
|
||||
.os-title-line > strong { color: #101828; font-size: 16px; }
|
||||
.os-resume { color: #98a2b3; font-size: 11px; }
|
||||
.expand-icon { color: #1769aa; font-size: 25px; font-weight: 300; text-align: center; }
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 23px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-sucesso { background: #ecfdf3; color: #027a48; }
|
||||
.badge-processando { background: #eff8ff; color: #175cd3; }
|
||||
.badge-alerta { background: #fffaeb; color: #b54708; }
|
||||
.badge-erro { background: #fef3f2; color: #b42318; }
|
||||
.badge-info { background: #f4f3ff; color: #5925dc; }
|
||||
.badge-neutro { background: #f2f4f7; color: #475467; }
|
||||
|
||||
.os-card-body {
|
||||
padding: 0 18px 20px;
|
||||
border-top: 1px solid #eaecf0;
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.os-actions { display: flex; justify-content: flex-end; padding: 12px 0 5px; }
|
||||
|
||||
.narrative-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin: 8px 0 14px;
|
||||
}
|
||||
|
||||
.narrative-item {
|
||||
padding: 13px;
|
||||
border: 1px solid #eaecf0;
|
||||
border-radius: 9px;
|
||||
background: #ffffff;
|
||||
|
||||
> span { color: #667085; font-size: 10px; font-weight: 800; text-transform: uppercase; }
|
||||
p { margin: 6px 0 0; color: #344054; line-height: 1.5; white-space: pre-wrap; }
|
||||
}
|
||||
|
||||
.odometer-pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
padding: 11px 13px;
|
||||
border-radius: 9px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
span { color: #475467; font-size: 11px; font-weight: 700; }
|
||||
strong { color: #175cd3; }
|
||||
small { margin-left: auto; color: #667085; }
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 11px;
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
.activity-card {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr;
|
||||
gap: 11px;
|
||||
padding: 13px;
|
||||
border: 1px solid #eaecf0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.activity-number {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #eff8ff;
|
||||
color: #175cd3;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
strong { color: #344054; }
|
||||
span { color: #98a2b3; font-size: 11px; white-space: nowrap; }
|
||||
}
|
||||
|
||||
.activity-meta,
|
||||
.technicians {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
margin-top: 7px;
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.technicians span {
|
||||
padding: 3px 7px;
|
||||
border-radius: 5px;
|
||||
background: #f2f4f7;
|
||||
}
|
||||
|
||||
.activity-notes { margin: 9px 0 0; color: #475467; white-space: pre-wrap; }
|
||||
|
||||
.materials-table {
|
||||
margin-top: 11px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #eaecf0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.material-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 1fr) 100px 125px 125px;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
|
||||
&:first-child { border-top: 0; }
|
||||
> span { display: flex; flex-direction: column; }
|
||||
strong { color: #344054; }
|
||||
small { color: #98a2b3; }
|
||||
}
|
||||
|
||||
.material-header {
|
||||
background: #f8fafc;
|
||||
color: #667085;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bottom-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 330px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.status-history { position: relative; }
|
||||
|
||||
.status-step {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 12px 1fr;
|
||||
gap: 8px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: -2px;
|
||||
left: 4px;
|
||||
width: 1px;
|
||||
background: #d0d5dd;
|
||||
}
|
||||
|
||||
&:last-child::before { display: none; }
|
||||
strong { display: block; color: #344054; font-size: 12px; }
|
||||
small { color: #98a2b3; }
|
||||
p { margin: 3px 0 0; color: #667085; }
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-top: 3px;
|
||||
border-radius: 50%;
|
||||
background: #1769aa;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.order-financial {
|
||||
padding: 13px;
|
||||
border-radius: 9px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eaecf0;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
strong { color: #344054; }
|
||||
}
|
||||
|
||||
.order-total {
|
||||
margin-top: 5px;
|
||||
padding-top: 10px !important;
|
||||
border-top: 1px solid #eaecf0;
|
||||
|
||||
strong { color: #027a48; font-size: 15px; }
|
||||
}
|
||||
|
||||
.loading-main,
|
||||
.error-main,
|
||||
.timeline-loading,
|
||||
.timeline-error,
|
||||
.timeline-empty {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
|
||||
p { margin: 0 0 7px; color: #667085; }
|
||||
}
|
||||
|
||||
.loading-main,
|
||||
.error-main { min-height: 420px; }
|
||||
.error-actions { display: flex; gap: 9px; }
|
||||
|
||||
.loading-bar {
|
||||
width: 180px;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #dbeafe;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 45%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #1769aa;
|
||||
animation: loading 1s ease-in-out infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(125%); }
|
||||
}
|
||||
|
||||
/* Blindagem contra o tema global antigo */
|
||||
:host ::ng-deep .filters-card .mat-form-field,
|
||||
:host ::ng-deep .filters-card .mat-input-element,
|
||||
:host ::ng-deep .filters-card .mat-select-value,
|
||||
:host ::ng-deep .filters-card .mat-select-value-text {
|
||||
color: #101828 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-input-element { caret-color: #1769aa !important; }
|
||||
:host ::ng-deep .filters-card .mat-form-field-label,
|
||||
:host ::ng-deep .filters-card .mat-select-arrow,
|
||||
:host ::ng-deep .filters-card .mat-datepicker-toggle { color: #667085 !important; }
|
||||
:host ::ng-deep .filters-card .mat-form-field.mat-focused .mat-form-field-label { color: #1769aa !important; }
|
||||
:host ::ng-deep .filters-card .mat-form-field-outline { color: #d0d5dd !important; opacity: 1 !important; }
|
||||
:host ::ng-deep .filters-card .mat-form-field-outline-thick { color: #1769aa !important; }
|
||||
:host ::ng-deep .filters-card .mat-form-field-flex { background: transparent !important; }
|
||||
:host ::ng-deep .filters-card input::placeholder { color: #98a2b3 !important; opacity: 1 !important; }
|
||||
:host ::ng-deep .filters-actions .mat-button { color: #344054 !important; }
|
||||
:host ::ng-deep .filters-actions .mat-raised-button { color: #ffffff !important; }
|
||||
:host ::ng-deep .hero-card .mat-stroked-button,
|
||||
:host ::ng-deep .os-card .mat-stroked-button,
|
||||
:host ::ng-deep .error-main .mat-stroked-button,
|
||||
:host ::ng-deep .timeline-error .mat-stroked-button {
|
||||
color: #344054 !important;
|
||||
border-color: #d0d5dd !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
:host ::ng-deep .back-button.mat-button { color: #475467 !important; }
|
||||
:host ::ng-deep .mat-paginator { background: #ffffff !important; color: #475467 !important; border-top: 1px solid #eaecf0; }
|
||||
:host ::ng-deep .mat-paginator .mat-select-value,
|
||||
:host ::ng-deep .mat-paginator .mat-select-value-text,
|
||||
:host ::ng-deep .mat-paginator .mat-select-arrow,
|
||||
:host ::ng-deep .mat-paginator .mat-icon-button { color: #475467 !important; }
|
||||
:host ::ng-deep .mat-paginator .mat-icon-button[disabled] { color: #d0d5dd !important; }
|
||||
:host ::ng-deep .status-badge.badge-sucesso { background: #ecfdf3 !important; color: #027a48 !important; }
|
||||
:host ::ng-deep .status-badge.badge-processando { background: #eff8ff !important; color: #175cd3 !important; }
|
||||
:host ::ng-deep .status-badge.badge-alerta { background: #fffaeb !important; color: #b54708 !important; }
|
||||
:host ::ng-deep .status-badge.badge-erro { background: #fef3f2 !important; color: #b42318 !important; }
|
||||
:host ::ng-deep .status-badge.badge-info { background: #f4f3ff !important; color: #5925dc !important; }
|
||||
:host ::ng-deep .status-badge.badge-neutro { background: #f2f4f7 !important; color: #475467 !important; }
|
||||
|
||||
@media (max-width: 1350px) {
|
||||
.filters-grid { grid-template-columns: repeat(3, minmax(180px, 1fr)); }
|
||||
.field-search { grid-column: span 2; }
|
||||
.maintenance-stats { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.metrics-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.overview-grid { grid-template-columns: 1fr; }
|
||||
.narrative-grid { grid-template-columns: 1fr; }
|
||||
.bottom-details { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.prontuario-page { padding: 14px; }
|
||||
.hero-card { flex-direction: column; }
|
||||
.hero-side { align-items: flex-start; text-align: left; }
|
||||
.filters-grid { grid-template-columns: 1fr; }
|
||||
.field-search { grid-column: auto; }
|
||||
.os-card-header { grid-template-columns: 58px 1fr 24px; gap: 9px; padding: 13px 10px; }
|
||||
.os-resume { display: none; }
|
||||
.odometer-pair { grid-template-columns: 1fr; }
|
||||
.material-row { grid-template-columns: 1fr 70px 100px; }
|
||||
.material-row > span:nth-child(3) { display: none; }
|
||||
.activity-header { flex-direction: column; gap: 3px; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.metrics-grid { grid-template-columns: 1fr; }
|
||||
.identity-row { align-items: flex-start; }
|
||||
.robot-mark { display: none; }
|
||||
.maintenance-stats { grid-template-columns: repeat(2, 1fr); }
|
||||
.history-section { padding: 14px; }
|
||||
.timeline { padding-left: 18px; }
|
||||
.timeline-marker { left: -18px; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OrionEquipamentoProntuarioComponent } from './orion-equipamento-prontuario.component';
|
||||
|
||||
describe('OrionEquipamentoProntuarioComponent', () => {
|
||||
let component: OrionEquipamentoProntuarioComponent;
|
||||
let fixture: ComponentFixture<OrionEquipamentoProntuarioComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OrionEquipamentoProntuarioComponent]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OrionEquipamentoProntuarioComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('deve criar o componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import {
|
||||
EquipamentoProntuarioIndicadoresModel,
|
||||
EquipamentoProntuarioResponseModel,
|
||||
EquipamentoTimelineFiltrosModel,
|
||||
EquipamentoTimelineItemModel,
|
||||
OrdemServicoAtividadeMaterialModel,
|
||||
OrdemServicoOdometroModel,
|
||||
OrdemServicoStatus,
|
||||
OrdemServicoTipo
|
||||
} from '../../models/orionModel';
|
||||
import { OrionService } from '../../services/orionService';
|
||||
|
||||
@Component({
|
||||
selector: 'app-orion-equipamento-prontuario',
|
||||
templateUrl: './orion-equipamento-prontuario.component.html',
|
||||
styleUrls: ['./orion-equipamento-prontuario.component.scss']
|
||||
})
|
||||
export class OrionEquipamentoProntuarioComponent implements OnInit {
|
||||
|
||||
equipamentoId: number = null;
|
||||
prontuario: EquipamentoProntuarioResponseModel = null;
|
||||
timeline: EquipamentoTimelineItemModel[] = [];
|
||||
osExpandidas = new Set<number>();
|
||||
|
||||
carregandoProntuario: boolean = false;
|
||||
carregandoTimeline: boolean = false;
|
||||
erroProntuario: string = '';
|
||||
erroTimeline: string = '';
|
||||
|
||||
paginaAtual: number = 1;
|
||||
tamanhoPagina: number = 10;
|
||||
totalRegistros: number = 0;
|
||||
totalPaginas: number = 0;
|
||||
opcoesTamanhoPagina: number[] = [5, 10, 20, 50];
|
||||
|
||||
busca: string = '';
|
||||
tipoSelecionado: OrdemServicoTipo | '' = '';
|
||||
statusSelecionado: OrdemServicoStatus | '' = '';
|
||||
dataInicio: Date = null;
|
||||
dataFim: Date = null;
|
||||
somenteComMateriais: boolean | '' = '';
|
||||
|
||||
tipoOpcoes = [
|
||||
{ valor: '', texto: 'Todos os tipos' },
|
||||
{ valor: 'CORRETIVA', texto: 'Corretiva' },
|
||||
{ valor: 'PREVENTIVA', texto: 'Preventiva' },
|
||||
{ valor: 'REFORMA', texto: 'Reforma' },
|
||||
{ valor: 'DESENVOLVIMENTO', texto: 'Desenvolvimento' },
|
||||
{ valor: 'FABRICACAO', texto: 'Fabricação' },
|
||||
{ valor: 'GARANTIA', texto: 'Garantia' },
|
||||
{ valor: 'CORTESIA', texto: 'Cortesia' },
|
||||
{ valor: 'INTERNA', texto: 'Interna' },
|
||||
{ valor: 'OUTRO', texto: 'Outro' }
|
||||
];
|
||||
|
||||
statusOpcoes = [
|
||||
{ valor: '', texto: 'Todos os status' },
|
||||
{ valor: 'RASCUNHO', texto: 'Rascunho' },
|
||||
{ valor: 'ABERTA', texto: 'Aberta' },
|
||||
{ valor: 'EM_DIAGNOSTICO', texto: 'Em diagnóstico' },
|
||||
{ valor: 'AGUARDANDO_APROVACAO', texto: 'Aguardando aprovação' },
|
||||
{ valor: 'AGUARDANDO_PECA', texto: 'Aguardando peça' },
|
||||
{ valor: 'EM_EXECUCAO', texto: 'Em execução' },
|
||||
{ valor: 'EM_TESTES', texto: 'Em testes' },
|
||||
{ valor: 'FINALIZADA', texto: 'Finalizada' },
|
||||
{ valor: 'AGUARDANDO_RETIRADA', texto: 'Aguardando retirada' },
|
||||
{ valor: 'RETIRADA', texto: 'Retirada' },
|
||||
{ valor: 'CANCELADA', texto: 'Cancelada' }
|
||||
];
|
||||
|
||||
materiaisOpcoes = [
|
||||
{ valor: '', texto: 'Todas as OS' },
|
||||
{ valor: true, texto: 'Somente com peças/insumos' }
|
||||
];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private orionService: OrionService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.equipamentoId = Number(this.route.snapshot.paramMap.get('id'));
|
||||
|
||||
if (!this.equipamentoId || this.equipamentoId <= 0) {
|
||||
this.erroProntuario = 'Equipamento inválido.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.carregarProntuario();
|
||||
this.carregarTimeline();
|
||||
}
|
||||
|
||||
carregarProntuario() {
|
||||
this.carregandoProntuario = true;
|
||||
this.erroProntuario = '';
|
||||
|
||||
this.orionService
|
||||
.getEquipamentoProntuario(this.equipamentoId)
|
||||
.then(resultado => {
|
||||
this.prontuario = resultado;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[PRONTUARIO] Erro ao carregar resumo:', error);
|
||||
this.prontuario = null;
|
||||
this.erroProntuario = error && error.message
|
||||
? error.message
|
||||
: 'Não foi possível carregar o prontuário do equipamento.';
|
||||
})
|
||||
.then(() => {
|
||||
this.carregandoProntuario = false;
|
||||
});
|
||||
}
|
||||
|
||||
carregarTimeline() {
|
||||
this.carregandoTimeline = true;
|
||||
this.erroTimeline = '';
|
||||
|
||||
const filtros: EquipamentoTimelineFiltrosModel = {
|
||||
page: this.paginaAtual,
|
||||
pageSize: this.tamanhoPagina,
|
||||
busca: this.normalizarTexto(this.busca),
|
||||
tipo: this.tipoSelecionado,
|
||||
status: this.statusSelecionado,
|
||||
data_inicio: this.formatarDataApi(this.dataInicio),
|
||||
data_fim: this.formatarDataApi(this.dataFim),
|
||||
somente_com_materiais: this.somenteComMateriais === true
|
||||
? true
|
||||
: undefined
|
||||
};
|
||||
|
||||
this.orionService
|
||||
.getEquipamentoTimeline(this.equipamentoId, filtros)
|
||||
.then(resultado => {
|
||||
this.timeline = resultado.items || [];
|
||||
this.paginaAtual = resultado.pagination.page || 1;
|
||||
this.tamanhoPagina = resultado.pagination.pageSize || 10;
|
||||
this.totalRegistros = resultado.pagination.total || 0;
|
||||
this.totalPaginas = resultado.pagination.totalPages || 0;
|
||||
|
||||
if (this.timeline.length && this.osExpandidas.size === 0) {
|
||||
this.osExpandidas.add(this.timeline[0].id);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[PRONTUARIO] Erro ao carregar timeline:', error);
|
||||
this.timeline = [];
|
||||
this.totalRegistros = 0;
|
||||
this.totalPaginas = 0;
|
||||
this.erroTimeline = error && error.message
|
||||
? error.message
|
||||
: 'Não foi possível carregar o histórico do equipamento.';
|
||||
})
|
||||
.then(() => {
|
||||
this.carregandoTimeline = false;
|
||||
});
|
||||
}
|
||||
|
||||
atualizar() {
|
||||
this.carregarProntuario();
|
||||
this.carregarTimeline();
|
||||
}
|
||||
|
||||
voltar() {
|
||||
this.router.navigate(['/orion-equipamentos']);
|
||||
}
|
||||
|
||||
abrirOrdem(ordem: EquipamentoTimelineItemModel) {
|
||||
this.router.navigate([
|
||||
'/orion-ordens-servico',
|
||||
ordem.id,
|
||||
'editar'
|
||||
]);
|
||||
}
|
||||
|
||||
alternarOrdem(ordemId: number) {
|
||||
if (this.osExpandidas.has(ordemId)) {
|
||||
this.osExpandidas.delete(ordemId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.osExpandidas.add(ordemId);
|
||||
}
|
||||
|
||||
ordemExpandida(ordemId: number): boolean {
|
||||
return this.osExpandidas.has(ordemId);
|
||||
}
|
||||
|
||||
aplicarFiltros() {
|
||||
this.paginaAtual = 1;
|
||||
this.osExpandidas.clear();
|
||||
this.carregarTimeline();
|
||||
}
|
||||
|
||||
limparFiltros() {
|
||||
this.busca = '';
|
||||
this.tipoSelecionado = '';
|
||||
this.statusSelecionado = '';
|
||||
this.dataInicio = null;
|
||||
this.dataFim = null;
|
||||
this.somenteComMateriais = '';
|
||||
this.paginaAtual = 1;
|
||||
this.osExpandidas.clear();
|
||||
this.carregarTimeline();
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
this.paginaAtual = event.pageIndex + 1;
|
||||
this.tamanhoPagina = event.pageSize;
|
||||
this.osExpandidas.clear();
|
||||
this.carregarTimeline();
|
||||
}
|
||||
|
||||
get equipamento() {
|
||||
return this.prontuario ? this.prontuario.equipamento : null;
|
||||
}
|
||||
|
||||
get indicadores(): EquipamentoProntuarioIndicadoresModel {
|
||||
return this.prontuario ? this.prontuario.indicadores : null;
|
||||
}
|
||||
|
||||
horasSegundos(segundos: number): number {
|
||||
return this.numero(segundos) / 3600;
|
||||
}
|
||||
|
||||
totalHorasOrdem(ordem: EquipamentoTimelineItemModel): number {
|
||||
return this.numero(ordem.total_horas_expediente) +
|
||||
this.numero(ordem.total_horas_extras);
|
||||
}
|
||||
|
||||
quantidadeMateriais(ordem: EquipamentoTimelineItemModel): number {
|
||||
return (ordem.atividades || []).reduce(
|
||||
(total, atividade) => total + (atividade.materiais || []).length,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
leituraEntrada(ordem: EquipamentoTimelineItemModel): OrdemServicoOdometroModel {
|
||||
return (ordem.odometros || []).find(
|
||||
leitura => leitura.tipo_leitura === 'ENTRADA_MANUTENCAO'
|
||||
) || null;
|
||||
}
|
||||
|
||||
leituraSaida(ordem: EquipamentoTimelineItemModel): OrdemServicoOdometroModel {
|
||||
return (ordem.odometros || []).find(
|
||||
leitura => leitura.tipo_leitura === 'SAIDA_MANUTENCAO'
|
||||
) || null;
|
||||
}
|
||||
|
||||
valorMaterial(material: OrdemServicoAtividadeMaterialModel): number {
|
||||
if (material.valor_cobrado_total !== null && material.valor_cobrado_total !== undefined) {
|
||||
return this.numero(material.valor_cobrado_total);
|
||||
}
|
||||
|
||||
return this.numero(material.quantidade) *
|
||||
this.numero(material.valor_cobrado_unitario);
|
||||
}
|
||||
|
||||
labelStatus(status: OrdemServicoStatus): string {
|
||||
return this.buscarTexto(this.statusOpcoes, status, status || 'Sem status');
|
||||
}
|
||||
|
||||
labelTipo(tipo: OrdemServicoTipo): string {
|
||||
return this.buscarTexto(this.tipoOpcoes, tipo, tipo || 'Não informado');
|
||||
}
|
||||
|
||||
labelLeitura(tipo: string): string {
|
||||
const labels: any = {
|
||||
ENTRADA_MANUTENCAO: 'Entrada na manutenção',
|
||||
SAIDA_MANUTENCAO: 'Saída da manutenção',
|
||||
CADASTRO_INICIAL: 'Cadastro inicial',
|
||||
ATUALIZACAO_MANUAL: 'Atualização manual',
|
||||
TELEMETRIA: 'Telemetria'
|
||||
};
|
||||
|
||||
return labels[tipo] || tipo;
|
||||
}
|
||||
|
||||
classeStatus(status: OrdemServicoStatus): string {
|
||||
switch (status) {
|
||||
case 'RETIRADA':
|
||||
case 'FINALIZADA':
|
||||
return 'badge-sucesso';
|
||||
case 'EM_EXECUCAO':
|
||||
case 'EM_DIAGNOSTICO':
|
||||
case 'EM_TESTES':
|
||||
return 'badge-processando';
|
||||
case 'AGUARDANDO_APROVACAO':
|
||||
case 'AGUARDANDO_PECA':
|
||||
case 'AGUARDANDO_RETIRADA':
|
||||
return 'badge-alerta';
|
||||
case 'CANCELADA':
|
||||
return 'badge-erro';
|
||||
case 'RASCUNHO':
|
||||
return 'badge-neutro';
|
||||
default:
|
||||
return 'badge-info';
|
||||
}
|
||||
}
|
||||
|
||||
trackById(index: number, ordem: EquipamentoTimelineItemModel): number {
|
||||
return ordem.id;
|
||||
}
|
||||
|
||||
private buscarTexto(opcoes: any[], valor: string, padrao: string): string {
|
||||
for (let index = 0; index < opcoes.length; index += 1) {
|
||||
if (opcoes[index].valor === valor) {
|
||||
return opcoes[index].texto;
|
||||
}
|
||||
}
|
||||
|
||||
return padrao;
|
||||
}
|
||||
|
||||
private normalizarTexto(valor: string): string {
|
||||
return valor ? valor.trim() : '';
|
||||
}
|
||||
|
||||
private formatarDataApi(data: Date): string {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const ano = data.getFullYear();
|
||||
const mes = data.getMonth() + 1;
|
||||
const dia = data.getDate();
|
||||
|
||||
return ano + '-' +
|
||||
(mes < 10 ? '0' : '') + mes + '-' +
|
||||
(dia < 10 ? '0' : '') + dia;
|
||||
}
|
||||
|
||||
private numero(valor: any): number {
|
||||
const convertido = Number(valor);
|
||||
return isNaN(convertido) ? 0 : convertido;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<div class="equipamentos-page">
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<span class="eyebrow">ORIONTARD</span>
|
||||
<h1>Equipamentos</h1>
|
||||
<p>Visão individual dos robôs, histórico de manutenção, utilização e custos.</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
(click)="atualizar()"
|
||||
[disabled]="carregando">
|
||||
Atualizar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="filters-card">
|
||||
<div class="filters-grid">
|
||||
<mat-form-field appearance="outline" class="field-search">
|
||||
<mat-label>Pesquisar</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="busca"
|
||||
(keyup.enter)="aplicarFiltros()"
|
||||
placeholder="Número de série, nome, modelo ou cliente">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Modelo</mat-label>
|
||||
<mat-select
|
||||
[(ngModel)]="modeloSelecionado"
|
||||
[disabled]="carregandoFiltros">
|
||||
<mat-option value="">Todos os modelos</mat-option>
|
||||
<mat-option *ngFor="let modelo of modelos" [value]="modelo.nome">
|
||||
{{ modelo.nome }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Cliente</mat-label>
|
||||
<mat-select
|
||||
[(ngModel)]="clienteSelecionado"
|
||||
[disabled]="carregandoFiltros">
|
||||
<mat-option [value]="null">Todos os clientes</mat-option>
|
||||
<mat-option *ngFor="let cliente of clientes" [value]="cliente.id">
|
||||
{{ cliente.nome_fantasia }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Versão</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="versao"
|
||||
(keyup.enter)="aplicarFiltros()"
|
||||
placeholder="Ex.: V3">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Situação da manutenção</mat-label>
|
||||
<mat-select [(ngModel)]="possuiOsAberta">
|
||||
<mat-option *ngFor="let opcao of osAbertaOpcoes" [value]="opcao.valor">
|
||||
{{ opcao.texto }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Status da última OS</mat-label>
|
||||
<mat-select [(ngModel)]="statusUltimaOs">
|
||||
<mat-option *ngFor="let opcao of statusOpcoes" [value]="opcao.valor">
|
||||
{{ opcao.texto }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="filters-actions">
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
(click)="limparFiltros()"
|
||||
[disabled]="carregando">
|
||||
Limpar
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="aplicarFiltros()"
|
||||
[disabled]="carregando">
|
||||
Filtrar
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="summary-grid">
|
||||
<article class="summary-card">
|
||||
<span class="summary-label">Equipamentos encontrados</span>
|
||||
<strong>{{ totalRegistros }}</strong>
|
||||
<small>{{ dataSource.data.length }} nesta página</small>
|
||||
</article>
|
||||
|
||||
<article class="summary-card summary-card-alerta">
|
||||
<span class="summary-label">OS abertas na página</span>
|
||||
<strong>{{ osAbertasNaPagina }}</strong>
|
||||
<small>Equipamentos em atendimento</small>
|
||||
</article>
|
||||
|
||||
<article class="summary-card">
|
||||
<span class="summary-label">Movimento acumulado</span>
|
||||
<strong>{{ horas(movimentoNaPagina) | number:'1.1-1' }} h</strong>
|
||||
<small>Soma dos equipamentos exibidos</small>
|
||||
</article>
|
||||
|
||||
<article class="summary-card">
|
||||
<span class="summary-label">Custo histórico na página</span>
|
||||
<strong>{{ custoNaPagina | currency:'BRL':'symbol':'1.2-2' }}</strong>
|
||||
<small>OS efetivas exibidas</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="loading-state" *ngIf="carregando">
|
||||
<div class="loading-bar"></div>
|
||||
<span>Carregando equipamentos...</span>
|
||||
</div>
|
||||
|
||||
<div class="error-state" *ngIf="!carregando && erro">
|
||||
<strong>Não foi possível carregar os equipamentos.</strong>
|
||||
<p>{{ erro }}</p>
|
||||
<button mat-stroked-button type="button" (click)="atualizar()">
|
||||
Tentar novamente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="empty-state"
|
||||
*ngIf="!carregando && !erro && dataSource.data.length === 0">
|
||||
<strong>Nenhum equipamento encontrado.</strong>
|
||||
<p>Altere os filtros ou confira o cadastro dos equipamentos.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-scroll"
|
||||
*ngIf="!carregando && !erro && dataSource.data.length > 0">
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="equipamentos-table">
|
||||
<ng-container matColumnDef="equipamento">
|
||||
<th mat-header-cell *matHeaderCellDef>Equipamento</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<div class="equipment-cell">
|
||||
<strong>{{ nomeEquipamento(equipamento) }}</strong>
|
||||
<span class="serial-number">NS {{ equipamento.numero_serie }}</span>
|
||||
<small *ngIf="equipamento.codigo_interno">
|
||||
{{ equipamento.codigo_interno }}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="modelo">
|
||||
<th mat-header-cell *matHeaderCellDef>Modelo / versão</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<div class="stacked-cell">
|
||||
<strong>{{ equipamento.modelo_nome || 'Não informado' }}</strong>
|
||||
<span>{{ equipamento.versao || 'Sem versão' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="cliente">
|
||||
<th mat-header-cell *matHeaderCellDef>Cliente</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
{{ equipamento.cliente_nome || 'Sem cliente vinculado' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="movimento">
|
||||
<th mat-header-cell *matHeaderCellDef>Movimento</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<div class="metric-cell">
|
||||
<strong>{{ horas(equipamento.odometro_movimento_segundos) | number:'1.1-1' }} h</strong>
|
||||
<span>
|
||||
Parcial: {{ horas(equipamento.odometro_parcial_segundos) | number:'1.1-1' }} h
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="ordens">
|
||||
<th mat-header-cell *matHeaderCellDef>Ordens</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<div class="orders-cell">
|
||||
<strong>{{ equipamento.quantidade_os }}</strong>
|
||||
<span
|
||||
*ngIf="equipamento.quantidade_os_abertas > 0"
|
||||
class="open-order">
|
||||
{{ equipamento.quantidade_os_abertas }} aberta(s)
|
||||
</span>
|
||||
<span *ngIf="equipamento.quantidade_os_abertas === 0">
|
||||
Nenhuma aberta
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="ultima_manutencao">
|
||||
<th mat-header-cell *matHeaderCellDef>Última manutenção</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<button
|
||||
*ngIf="equipamento.ultima_os_id"
|
||||
mat-button
|
||||
type="button"
|
||||
class="os-link"
|
||||
(click)="abrirUltimaOs($event, equipamento)">
|
||||
<strong>OS #{{ equipamento.ultima_os_id }}</strong>
|
||||
<span>{{ equipamento.ultima_os_data_entrada | date:'dd/MM/yyyy' }}</span>
|
||||
</button>
|
||||
|
||||
<span *ngIf="!equipamento.ultima_os_id" class="muted-text">
|
||||
Sem histórico
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>Último status</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<span
|
||||
class="status-badge"
|
||||
[ngClass]="classeStatus(equipamento.ultima_os_status)">
|
||||
{{ labelStatus(equipamento.ultima_os_status) }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="custo">
|
||||
<th mat-header-cell *matHeaderCellDef>Custo histórico</th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<div class="metric-cell cost-cell">
|
||||
<strong>
|
||||
{{ equipamento.valor_total_historico | currency:'BRL':'symbol':'1.2-2' }}
|
||||
</strong>
|
||||
<span>
|
||||
M.O. {{ equipamento.valor_mao_obra_historico | currency:'BRL':'symbol':'1.0-0' }}
|
||||
· Peças {{ equipamento.valor_insumos_historico | currency:'BRL':'symbol':'1.0-0' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="acoes">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let equipamento">
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="abrirProntuario(equipamento)">
|
||||
Ver histórico
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let equipamento; columns: displayedColumns"
|
||||
[class.selected-row]="equipamentoSelecionado?.id === equipamento.id"
|
||||
(click)="selecionarEquipamento(equipamento)"
|
||||
(dblclick)="abrirProntuario(equipamento)">
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator
|
||||
*ngIf="!erro && totalRegistros > 0"
|
||||
[length]="totalRegistros"
|
||||
[pageIndex]="paginaAtual - 1"
|
||||
[pageSize]="tamanhoPagina"
|
||||
[pageSizeOptions]="opcoesTamanhoPagina"
|
||||
[showFirstLastButtons]="true"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
:host {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.equipamentos-page {
|
||||
max-width: 1680px;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h1 {
|
||||
margin: 3px 0 5px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: #2563eb;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.header-actions,
|
||||
.filters-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filters-card,
|
||||
.table-card,
|
||||
.summary-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 7px 22px rgba(15, 23, 42, 0.045);
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
padding: 20px 20px 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(270px, 1.7fr) repeat(5, minmax(155px, 1fr));
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.filters-grid .mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 18px 20px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin: 5px 0 2px;
|
||||
color: #0f172a;
|
||||
font-size: 23px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-card-alerta::before {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.equipamentos-table {
|
||||
width: 100%;
|
||||
min-width: 1400px;
|
||||
|
||||
th.mat-header-cell {
|
||||
padding: 14px 13px;
|
||||
border-bottom-color: #dbe2ea;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.mat-cell {
|
||||
padding: 14px 13px;
|
||||
border-bottom-color: #edf1f5;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr.mat-row {
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background: #eff6ff !important;
|
||||
}
|
||||
|
||||
.equipment-cell,
|
||||
.stacked-cell,
|
||||
.metric-cell,
|
||||
.orders-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
span,
|
||||
small {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.serial-number {
|
||||
width: fit-content;
|
||||
padding: 2px 7px;
|
||||
border-radius: 5px;
|
||||
background: #eaf2ff;
|
||||
color: #1d4ed8 !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.open-order {
|
||||
color: #b45309 !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cost-cell {
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-sucesso { background: #dcfce7; color: #166534; }
|
||||
.badge-processando { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-alerta { background: #fef3c7; color: #92400e; }
|
||||
.badge-erro { background: #fee2e2; color: #b91c1c; }
|
||||
.badge-info { background: #e0f2fe; color: #0369a1; }
|
||||
.badge-neutro { background: #e2e8f0; color: #475569; }
|
||||
|
||||
.os-link {
|
||||
min-width: 0;
|
||||
padding: 2px 7px;
|
||||
line-height: 1.25;
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.muted-text {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
min-height: 190px;
|
||||
padding: 35px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
max-width: 560px;
|
||||
margin: 0 0 8px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
width: 180px;
|
||||
height: 4px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #dbeafe;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 45%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #2563eb;
|
||||
animation: loading 1s ease-in-out infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(125%); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
BLINDAGEM CONTRA O TEMA GLOBAL ANTIGO
|
||||
------------------------------------------------------------ */
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field {
|
||||
color: #101828 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-input-element {
|
||||
color: #101828 !important;
|
||||
caret-color: #1769aa !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-select-value,
|
||||
:host ::ng-deep .filters-card .mat-select-value-text {
|
||||
color: #101828 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field-label {
|
||||
color: #667085 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field.mat-focused .mat-form-field-label {
|
||||
color: #1769aa !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field-outline {
|
||||
color: #d0d5dd !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field-outline-thick {
|
||||
color: #1769aa !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-select-arrow {
|
||||
color: #667085 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card input::placeholder {
|
||||
color: #98a2b3 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-card .mat-form-field-flex {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table,
|
||||
:host ::ng-deep table.equipamentos-table {
|
||||
background: #ffffff !important;
|
||||
color: #344054 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table tr.mat-header-row {
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table tr.mat-row {
|
||||
background: #ffffff !important;
|
||||
color: #344054 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table tr.mat-row:nth-child(even) {
|
||||
background: #fbfcfd !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table tr.mat-row:hover {
|
||||
background: #f2f7fc !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table tr.mat-row.selected-row {
|
||||
background: #eaf4ff !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table th.mat-header-cell {
|
||||
background: #f8fafc !important;
|
||||
color: #475467 !important;
|
||||
border-color: #eaecf0 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table td.mat-cell {
|
||||
background: transparent !important;
|
||||
color: #344054 !important;
|
||||
border-color: #eaecf0 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .equipamentos-table td,
|
||||
:host ::ng-deep .equipamentos-table th {
|
||||
color: #344054 !important;
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .header-actions .mat-stroked-button,
|
||||
:host ::ng-deep .equipamentos-table .mat-stroked-button {
|
||||
color: #344054 !important;
|
||||
border-color: #d0d5dd !important;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-actions .mat-button {
|
||||
color: #344054 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .filters-actions .mat-raised-button {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-paginator {
|
||||
background: #ffffff !important;
|
||||
color: #475467 !important;
|
||||
border-top: 1px solid #eaecf0;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-paginator .mat-select-value,
|
||||
:host ::ng-deep .mat-paginator .mat-select-value-text {
|
||||
color: #344054 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-paginator .mat-select-arrow,
|
||||
:host ::ng-deep .mat-paginator .mat-icon-button {
|
||||
color: #667085 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .mat-paginator .mat-icon-button[disabled] {
|
||||
color: #d0d5dd !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1350px) {
|
||||
.filters-grid {
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.field-search {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.equipamentos-page {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filters-grid,
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.field-search {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.filters-grid,
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-search {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OrionEquipamentosComponent } from './orion-equipamentos.component';
|
||||
|
||||
describe('OrionEquipamentosComponent', () => {
|
||||
let component: OrionEquipamentosComponent;
|
||||
let fixture: ComponentFixture<OrionEquipamentosComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OrionEquipamentosComponent]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OrionEquipamentosComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('deve criar o componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import {
|
||||
ClienteManutencaoModel,
|
||||
EquipamentoHistoricoFiltrosModel,
|
||||
EquipamentoHistoricoListaModel,
|
||||
EquipamentoManutencaoModel,
|
||||
OrdemServicoStatus
|
||||
} from '../../models/orionModel';
|
||||
import { OrionService } from '../../services/orionService';
|
||||
|
||||
interface OpcaoModelo {
|
||||
id: number;
|
||||
nome: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-orion-equipamentos',
|
||||
templateUrl: './orion-equipamentos.component.html',
|
||||
styleUrls: ['./orion-equipamentos.component.scss']
|
||||
})
|
||||
export class OrionEquipamentosComponent implements OnInit {
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'equipamento',
|
||||
'modelo',
|
||||
'cliente',
|
||||
'movimento',
|
||||
'ordens',
|
||||
'ultima_manutencao',
|
||||
'status',
|
||||
'custo',
|
||||
'acoes'
|
||||
];
|
||||
|
||||
dataSource = new MatTableDataSource<EquipamentoHistoricoListaModel>([]);
|
||||
equipamentoSelecionado: EquipamentoHistoricoListaModel = null;
|
||||
|
||||
carregando: boolean = false;
|
||||
carregandoFiltros: boolean = false;
|
||||
erro: string = '';
|
||||
|
||||
paginaAtual: number = 1;
|
||||
tamanhoPagina: number = 25;
|
||||
totalRegistros: number = 0;
|
||||
totalPaginas: number = 0;
|
||||
opcoesTamanhoPagina: number[] = [10, 25, 50, 100];
|
||||
|
||||
busca: string = '';
|
||||
modeloSelecionado: string = '';
|
||||
clienteSelecionado: number = null;
|
||||
versao: string = '';
|
||||
possuiOsAberta: boolean | '' = '';
|
||||
statusUltimaOs: OrdemServicoStatus | '' = '';
|
||||
|
||||
clientes: ClienteManutencaoModel[] = [];
|
||||
modelos: OpcaoModelo[] = [];
|
||||
|
||||
statusOpcoes = [
|
||||
{ valor: '', texto: 'Todos os status' },
|
||||
{ valor: 'RASCUNHO', texto: 'Rascunho' },
|
||||
{ valor: 'ABERTA', texto: 'Aberta' },
|
||||
{ valor: 'EM_DIAGNOSTICO', texto: 'Em diagnóstico' },
|
||||
{ valor: 'AGUARDANDO_APROVACAO', texto: 'Aguardando aprovação' },
|
||||
{ valor: 'AGUARDANDO_PECA', texto: 'Aguardando peça' },
|
||||
{ valor: 'EM_EXECUCAO', texto: 'Em execução' },
|
||||
{ valor: 'EM_TESTES', texto: 'Em testes' },
|
||||
{ valor: 'FINALIZADA', texto: 'Finalizada' },
|
||||
{ valor: 'AGUARDANDO_RETIRADA', texto: 'Aguardando retirada' },
|
||||
{ valor: 'RETIRADA', texto: 'Retirada' },
|
||||
{ valor: 'CANCELADA', texto: 'Cancelada' }
|
||||
];
|
||||
|
||||
osAbertaOpcoes = [
|
||||
{ valor: '', texto: 'Todos' },
|
||||
{ valor: true, texto: 'Com OS aberta' },
|
||||
{ valor: false, texto: 'Sem OS aberta' }
|
||||
];
|
||||
|
||||
constructor(
|
||||
private orionService: OrionService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.carregarOpcoesFiltros();
|
||||
this.carregarEquipamentos();
|
||||
}
|
||||
|
||||
carregarEquipamentos() {
|
||||
this.carregando = true;
|
||||
this.erro = '';
|
||||
|
||||
const filtros: EquipamentoHistoricoFiltrosModel = {
|
||||
page: this.paginaAtual,
|
||||
pageSize: this.tamanhoPagina,
|
||||
busca: this.normalizarTexto(this.busca),
|
||||
modelo: this.modeloSelecionado,
|
||||
cliente_id: this.clienteSelecionado,
|
||||
versao: this.normalizarTexto(this.versao),
|
||||
possui_os_aberta: this.possuiOsAberta,
|
||||
status_ultima_os: this.statusUltimaOs
|
||||
};
|
||||
|
||||
this.orionService
|
||||
.getEquipamentosHistorico(filtros)
|
||||
.then(resultado => {
|
||||
this.dataSource.data = resultado.items || [];
|
||||
this.paginaAtual = resultado.pagination.page || 1;
|
||||
this.tamanhoPagina = resultado.pagination.pageSize || 25;
|
||||
this.totalRegistros = resultado.pagination.total || 0;
|
||||
this.totalPaginas = resultado.pagination.totalPages || 0;
|
||||
|
||||
if (
|
||||
this.equipamentoSelecionado &&
|
||||
!this.dataSource.data.some(
|
||||
equipamento => equipamento.id === this.equipamentoSelecionado.id
|
||||
)
|
||||
) {
|
||||
this.equipamentoSelecionado = null;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[EQUIPAMENTOS] Erro ao carregar histórico:', error);
|
||||
this.dataSource.data = [];
|
||||
this.totalRegistros = 0;
|
||||
this.totalPaginas = 0;
|
||||
this.erro = error && error.message
|
||||
? error.message
|
||||
: 'Não foi possível carregar os equipamentos.';
|
||||
})
|
||||
.then(() => {
|
||||
this.carregando = false;
|
||||
});
|
||||
}
|
||||
|
||||
carregarOpcoesFiltros() {
|
||||
this.carregandoFiltros = true;
|
||||
|
||||
Promise.all([
|
||||
this.orionService.getClientesManutencao(),
|
||||
this.orionService.getEquipamentosManutencao()
|
||||
])
|
||||
.then(resultados => {
|
||||
this.clientes = resultados[0] || [];
|
||||
this.modelos = this.extrairModelos(resultados[1] || []);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[EQUIPAMENTOS] Erro ao carregar filtros:', error);
|
||||
})
|
||||
.then(() => {
|
||||
this.carregandoFiltros = false;
|
||||
});
|
||||
}
|
||||
|
||||
aplicarFiltros() {
|
||||
this.paginaAtual = 1;
|
||||
this.equipamentoSelecionado = null;
|
||||
this.carregarEquipamentos();
|
||||
}
|
||||
|
||||
limparFiltros() {
|
||||
this.busca = '';
|
||||
this.modeloSelecionado = '';
|
||||
this.clienteSelecionado = null;
|
||||
this.versao = '';
|
||||
this.possuiOsAberta = '';
|
||||
this.statusUltimaOs = '';
|
||||
this.paginaAtual = 1;
|
||||
this.equipamentoSelecionado = null;
|
||||
this.carregarEquipamentos();
|
||||
}
|
||||
|
||||
atualizar() {
|
||||
this.carregarEquipamentos();
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
this.paginaAtual = event.pageIndex + 1;
|
||||
this.tamanhoPagina = event.pageSize;
|
||||
this.equipamentoSelecionado = null;
|
||||
this.carregarEquipamentos();
|
||||
}
|
||||
|
||||
selecionarEquipamento(equipamento: EquipamentoHistoricoListaModel) {
|
||||
this.equipamentoSelecionado = equipamento;
|
||||
}
|
||||
|
||||
abrirProntuario(equipamento: EquipamentoHistoricoListaModel) {
|
||||
this.router.navigate([
|
||||
'/orion-equipamentos',
|
||||
equipamento.id,
|
||||
'historico'
|
||||
]);
|
||||
}
|
||||
|
||||
abrirUltimaOs(
|
||||
event: MouseEvent,
|
||||
equipamento: EquipamentoHistoricoListaModel
|
||||
) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!equipamento.ultima_os_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate([
|
||||
'/orion-ordens-servico',
|
||||
equipamento.ultima_os_id,
|
||||
'editar'
|
||||
]);
|
||||
}
|
||||
|
||||
trackById(index: number, equipamento: EquipamentoHistoricoListaModel): number {
|
||||
return equipamento.id;
|
||||
}
|
||||
|
||||
get osAbertasNaPagina(): number {
|
||||
return this.dataSource.data.reduce(
|
||||
(total, equipamento) => total + this.numero(equipamento.quantidade_os_abertas),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
get movimentoNaPagina(): number {
|
||||
return this.dataSource.data.reduce(
|
||||
(total, equipamento) => total + this.numero(equipamento.odometro_movimento_segundos),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
get custoNaPagina(): number {
|
||||
return this.dataSource.data.reduce(
|
||||
(total, equipamento) => total + this.numero(equipamento.valor_total_historico),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
horas(segundos: number): number {
|
||||
return this.numero(segundos) / 3600;
|
||||
}
|
||||
|
||||
nomeEquipamento(equipamento: EquipamentoHistoricoListaModel): string {
|
||||
return equipamento.nome || equipamento.modelo_nome || 'Equipamento sem nome';
|
||||
}
|
||||
|
||||
labelStatus(status: OrdemServicoStatus): string {
|
||||
if (!status) {
|
||||
return 'Sem OS';
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.statusOpcoes.length; i += 1) {
|
||||
if (this.statusOpcoes[i].valor === status) {
|
||||
return this.statusOpcoes[i].texto;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
classeStatus(status: OrdemServicoStatus): string {
|
||||
switch (status) {
|
||||
case 'RETIRADA':
|
||||
case 'FINALIZADA':
|
||||
return 'badge-sucesso';
|
||||
|
||||
case 'EM_EXECUCAO':
|
||||
case 'EM_DIAGNOSTICO':
|
||||
case 'EM_TESTES':
|
||||
return 'badge-processando';
|
||||
|
||||
case 'AGUARDANDO_APROVACAO':
|
||||
case 'AGUARDANDO_PECA':
|
||||
case 'AGUARDANDO_RETIRADA':
|
||||
return 'badge-alerta';
|
||||
|
||||
case 'CANCELADA':
|
||||
return 'badge-erro';
|
||||
|
||||
case 'RASCUNHO':
|
||||
return 'badge-neutro';
|
||||
|
||||
default:
|
||||
return 'badge-info';
|
||||
}
|
||||
}
|
||||
|
||||
private extrairModelos(equipamentos: EquipamentoManutencaoModel[]): OpcaoModelo[] {
|
||||
const unicos = new Map<string, OpcaoModelo>();
|
||||
|
||||
equipamentos.forEach(equipamento => {
|
||||
const nome = this.normalizarTexto(equipamento.modelo_nome);
|
||||
if (!nome || unicos.has(nome.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
unicos.set(nome.toLowerCase(), {
|
||||
id: equipamento.modelo_id || 0,
|
||||
nome
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(unicos.values()).sort((a, b) =>
|
||||
a.nome.localeCompare(b.nome)
|
||||
);
|
||||
}
|
||||
|
||||
private normalizarTexto(valor: string): string {
|
||||
return valor ? valor.trim() : '';
|
||||
}
|
||||
|
||||
private numero(valor: any): number {
|
||||
const convertido = Number(valor);
|
||||
return isNaN(convertido) ? 0 : convertido;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@ export class OrionOrdensServicoComponent implements OnInit {
|
|||
{ valor: 'EM_EXECUCAO', texto: 'Em execução' },
|
||||
{ valor: 'EM_TESTES', texto: 'Em testes' },
|
||||
{ valor: 'FINALIZADA', texto: 'Finalizada' },
|
||||
{ valor: 'AGUARDANDO_RETIRADA', texto: 'Aguardando retirada' },
|
||||
{ valor: 'RETIRADA', texto: 'Retirada' },
|
||||
{ valor: 'CANCELADA', texto: 'Cancelada' }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { environment } from 'src/environments/environment';
|
|||
|
||||
import {
|
||||
ClienteManutencaoModel,
|
||||
EquipamentoHistoricoFiltrosModel,
|
||||
EquipamentoHistoricoListaModel,
|
||||
EquipamentoHistoricoListaResponseModel,
|
||||
EquipamentoManutencaoModel,
|
||||
EquipamentoProntuarioIndicadoresModel,
|
||||
EquipamentoProntuarioResponseModel,
|
||||
EquipamentoTimelineFiltrosModel,
|
||||
EquipamentoTimelineItemModel,
|
||||
EquipamentoTimelineResponseModel,
|
||||
GeoCoordenadasResponseModel,
|
||||
MaterialManutencaoModel,
|
||||
OrionApiResponseModel,
|
||||
|
|
@ -105,11 +113,14 @@ export class OrionService {
|
|||
// LOGS
|
||||
// ==========================================================
|
||||
|
||||
getAllLogs(): Promise<OrionLogsModel[]> {
|
||||
getAllLogs(meses: number = 3): Promise<OrionLogsModel[]> {
|
||||
const params = new HttpParams().set('meses', String(meses));
|
||||
|
||||
return this.executar(
|
||||
this.http
|
||||
.get<OrionApiResponseModel<OrionLogsModel[]>>(
|
||||
`${this.Rota}/getAllLogs`
|
||||
`${this.Rota}/getAllLogs`,
|
||||
{ params }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
|
@ -131,7 +142,7 @@ export class OrionService {
|
|||
): Promise<GeoCoordenadasResponseModel> {
|
||||
return this.http
|
||||
.get<GeoCoordenadasResponseModel>(
|
||||
`http://api.positionstack.com/v1/reverse` +
|
||||
`https://api.positionstack.com/v1/reverse` +
|
||||
`?access_key=${environment.GeoAPI}` +
|
||||
`&query=${encodeURIComponent(coordenadas)}`
|
||||
)
|
||||
|
|
@ -185,6 +196,243 @@ export class OrionService {
|
|||
);
|
||||
}
|
||||
|
||||
getEquipamentosHistorico(
|
||||
filtros: EquipamentoHistoricoFiltrosModel = {}
|
||||
): Promise<EquipamentoHistoricoListaResponseModel> {
|
||||
const params = this.montarParametros(filtros);
|
||||
|
||||
return this.executar(
|
||||
this.http
|
||||
.get<OrionApiResponseModel<EquipamentoHistoricoListaResponseModel>>(
|
||||
`${this.Rota}/getEquipamentosHistorico`,
|
||||
{ params }
|
||||
)
|
||||
.toPromise()
|
||||
).then(resultado => ({
|
||||
items: (resultado.items || []).map(item =>
|
||||
this.normalizarEquipamentoHistorico(item)
|
||||
),
|
||||
pagination: {
|
||||
page: this.numero(resultado.pagination.page),
|
||||
pageSize: this.numero(resultado.pagination.pageSize),
|
||||
total: this.numero(resultado.pagination.total),
|
||||
totalPages: this.numero(resultado.pagination.totalPages)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private normalizarEquipamentoHistorico(
|
||||
equipamento: EquipamentoHistoricoListaModel
|
||||
): EquipamentoHistoricoListaModel {
|
||||
return {
|
||||
...equipamento,
|
||||
id: this.numero(equipamento.id),
|
||||
cliente_id: equipamento.cliente_id
|
||||
? this.numero(equipamento.cliente_id)
|
||||
: null,
|
||||
modelo_id: equipamento.modelo_id
|
||||
? this.numero(equipamento.modelo_id)
|
||||
: null,
|
||||
odometro_total_segundos: this.numero(
|
||||
equipamento.odometro_total_segundos
|
||||
),
|
||||
odometro_conectado_segundos: this.numero(
|
||||
equipamento.odometro_conectado_segundos
|
||||
),
|
||||
odometro_movimento_segundos: this.numero(
|
||||
equipamento.odometro_movimento_segundos
|
||||
),
|
||||
odometro_parcial_segundos: this.numero(
|
||||
equipamento.odometro_parcial_segundos
|
||||
),
|
||||
quantidade_os: this.numero(equipamento.quantidade_os),
|
||||
quantidade_os_abertas: this.numero(
|
||||
equipamento.quantidade_os_abertas
|
||||
),
|
||||
valor_mao_obra_historico: this.numero(
|
||||
equipamento.valor_mao_obra_historico
|
||||
),
|
||||
valor_insumos_historico: this.numero(
|
||||
equipamento.valor_insumos_historico
|
||||
),
|
||||
valor_total_historico: this.numero(
|
||||
equipamento.valor_total_historico
|
||||
),
|
||||
valor_pago_historico: this.numero(
|
||||
equipamento.valor_pago_historico
|
||||
),
|
||||
valor_pendente_historico: this.numero(
|
||||
equipamento.valor_pendente_historico
|
||||
),
|
||||
ultima_os_id: equipamento.ultima_os_id
|
||||
? this.numero(equipamento.ultima_os_id)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
getEquipamentoProntuario(
|
||||
equipamentoId: number
|
||||
): Promise<EquipamentoProntuarioResponseModel> {
|
||||
return this.executar(
|
||||
this.http
|
||||
.get<OrionApiResponseModel<EquipamentoProntuarioResponseModel>>(
|
||||
`${this.Rota}/getEquipamentoProntuario/${equipamentoId}`
|
||||
)
|
||||
.toPromise()
|
||||
).then(resultado => ({
|
||||
...resultado,
|
||||
equipamento: {
|
||||
...resultado.equipamento,
|
||||
id: this.numero(resultado.equipamento.id),
|
||||
cliente_id: resultado.equipamento.cliente_id
|
||||
? this.numero(resultado.equipamento.cliente_id)
|
||||
: null,
|
||||
modelo_id: resultado.equipamento.modelo_id
|
||||
? this.numero(resultado.equipamento.modelo_id)
|
||||
: null,
|
||||
odometro_total_segundos: this.numero(
|
||||
resultado.equipamento.odometro_total_segundos
|
||||
),
|
||||
odometro_conectado_segundos: this.numero(
|
||||
resultado.equipamento.odometro_conectado_segundos
|
||||
),
|
||||
odometro_movimento_segundos: this.numero(
|
||||
resultado.equipamento.odometro_movimento_segundos
|
||||
),
|
||||
odometro_parcial_segundos: this.numero(
|
||||
resultado.equipamento.odometro_parcial_segundos
|
||||
)
|
||||
},
|
||||
indicadores: this.normalizarIndicadoresProntuario(
|
||||
resultado.indicadores
|
||||
),
|
||||
ultima_os: resultado.ultima_os
|
||||
? this.normalizarOrdemLista(resultado.ultima_os)
|
||||
: null,
|
||||
ultima_saida_manutencao: resultado.ultima_saida_manutencao || null,
|
||||
leituras_manutencao: (resultado.leituras_manutencao || []).map(
|
||||
leitura => this.normalizarOdometro(leitura)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
getEquipamentoTimeline(
|
||||
equipamentoId: number,
|
||||
filtros: EquipamentoTimelineFiltrosModel = {}
|
||||
): Promise<EquipamentoTimelineResponseModel> {
|
||||
const params = this.montarParametros(filtros);
|
||||
|
||||
return this.executar(
|
||||
this.http
|
||||
.get<OrionApiResponseModel<EquipamentoTimelineResponseModel>>(
|
||||
`${this.Rota}/getEquipamentoTimeline/${equipamentoId}`,
|
||||
{ params }
|
||||
)
|
||||
.toPromise()
|
||||
).then(resultado => ({
|
||||
items: (resultado.items || []).map(item =>
|
||||
this.normalizarTimelineItem(item)
|
||||
),
|
||||
pagination: {
|
||||
page: this.numero(resultado.pagination.page),
|
||||
pageSize: this.numero(resultado.pagination.pageSize),
|
||||
total: this.numero(resultado.pagination.total),
|
||||
totalPages: this.numero(resultado.pagination.totalPages)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private normalizarIndicadoresProntuario(
|
||||
indicadores: EquipamentoProntuarioIndicadoresModel
|
||||
): EquipamentoProntuarioIndicadoresModel {
|
||||
const normalizado: any = { ...indicadores };
|
||||
const campos = [
|
||||
'quantidade_os', 'quantidade_os_abertas', 'quantidade_os_efetivas',
|
||||
'quantidade_corretivas', 'quantidade_preventivas',
|
||||
'total_horas_expediente', 'total_horas_extras',
|
||||
'horas_movimento', 'horas_conectado', 'horas_total', 'horas_parcial',
|
||||
'valor_mao_obra', 'custo_insumos', 'valor_insumos',
|
||||
'valor_desconto', 'valor_acrescimo', 'valor_total',
|
||||
'valor_pago', 'valor_pendente',
|
||||
'movimento_desde_ultima_saida_segundos',
|
||||
'media_movimento_entre_manutencoes_segundos',
|
||||
'custo_medio_por_os', 'custo_por_hora_movimento'
|
||||
];
|
||||
|
||||
campos.forEach(campo => {
|
||||
if (normalizado[campo] !== null && normalizado[campo] !== undefined) {
|
||||
normalizado[campo] = this.numero(normalizado[campo]);
|
||||
}
|
||||
});
|
||||
|
||||
return normalizado as EquipamentoProntuarioIndicadoresModel;
|
||||
}
|
||||
|
||||
private normalizarTimelineItem(
|
||||
item: EquipamentoTimelineItemModel
|
||||
): EquipamentoTimelineItemModel {
|
||||
const ordem = this.normalizarOrdemLista(item);
|
||||
|
||||
return {
|
||||
...item,
|
||||
...ordem,
|
||||
custo_insumos: this.numero(item.custo_insumos),
|
||||
atividades: (item.atividades || []).map(atividade => ({
|
||||
...atividade,
|
||||
id: this.numero(atividade.id),
|
||||
ordem_servico_id: this.numero(atividade.ordem_servico_id),
|
||||
ordem_exibicao: this.numero(atividade.ordem_exibicao),
|
||||
horas_expediente: this.numero(atividade.horas_expediente),
|
||||
horas_extras: this.numero(atividade.horas_extras),
|
||||
valor_hora_expediente: this.numero(atividade.valor_hora_expediente),
|
||||
valor_hora_extra: this.numero(atividade.valor_hora_extra),
|
||||
valor_mao_obra: this.numero(atividade.valor_mao_obra),
|
||||
tecnicos: atividade.tecnicos || [],
|
||||
materiais: (atividade.materiais || []).map(material => ({
|
||||
...material,
|
||||
quantidade: this.numero(material.quantidade),
|
||||
valor_custo_unitario: this.numero(material.valor_custo_unitario),
|
||||
valor_cobrado_unitario: this.numero(material.valor_cobrado_unitario),
|
||||
valor_custo_total: this.numero(material.valor_custo_total),
|
||||
valor_cobrado_total: this.numero(material.valor_cobrado_total)
|
||||
}))
|
||||
})),
|
||||
odometros: (item.odometros || []).map(
|
||||
leitura => this.normalizarOdometro(leitura)
|
||||
),
|
||||
odometros_da_os: (item.odometros || []).map(
|
||||
leitura => this.normalizarOdometro(leitura)
|
||||
),
|
||||
pagamentos: item.pagamentos || [],
|
||||
historico_status: item.historico_status || []
|
||||
};
|
||||
}
|
||||
|
||||
private normalizarOdometro(
|
||||
leitura: OrdemServicoOdometroModel
|
||||
): OrdemServicoOdometroModel {
|
||||
return {
|
||||
...leitura,
|
||||
id: leitura.id ? this.numero(leitura.id) : null,
|
||||
equipamento_id: leitura.equipamento_id
|
||||
? this.numero(leitura.equipamento_id)
|
||||
: null,
|
||||
ordem_servico_id: leitura.ordem_servico_id
|
||||
? this.numero(leitura.ordem_servico_id)
|
||||
: null,
|
||||
odometro_total_segundos: this.numero(leitura.odometro_total_segundos),
|
||||
odometro_conectado_segundos: this.numero(
|
||||
leitura.odometro_conectado_segundos
|
||||
),
|
||||
odometro_movimento_segundos: this.numero(
|
||||
leitura.odometro_movimento_segundos
|
||||
),
|
||||
odometro_parcial_segundos: this.numero(
|
||||
leitura.odometro_parcial_segundos
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
getTecnicosManutencao(): Promise<TecnicoManutencaoModel[]> {
|
||||
return this.executar(
|
||||
this.http
|
||||
|
|
@ -486,4 +734,4 @@ export class OrionService {
|
|||
.then(response => response as Blob);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue