adicionado criacao de OSs Oriontard

This commit is contained in:
Diego Freitas 2026-07-12 15:14:26 -03:00
parent 56941e2978
commit 445b7b5812
26 changed files with 8805 additions and 122 deletions

View File

@ -124,3 +124,31 @@ global.ExecuteQueryAgro = function ExecuteQueryAgro(sqlQuery, req, res, params =
//#endregion
//#region ORIONTARD
var ConfigMySQLOriontard = {
connectionLimit : 10,
host : 'zendioninc.com.br',
port : '3306',
user : 'zendion',
password : 'Z210203!',
database : 'oriontard',
debug : false,
multipleStatements : true
};
global.AbreConexaoOriontardMySQL = function AbreConexaoOriontardMySQL() {
global.ConexaoMySQL_Oriontard = mysql.createPool(ConfigMySQLOriontard);
console.log("MySQL Conectado! Oriontard");
}
AbreConexaoOriontardMySQL();
global.ExecuteQueryOriontard = function ExecuteQueryOriontard(sqlQuery, req, res, params = [], callback = null) {
ConexaoMySQL_Oriontard.query(sqlQuery, params, function (error, results, fields) {
console.log(`${new Date().toUTCString()} - ${req.originalUrl} ${(error) ? ' 500 error' : ' 200 success'}`);
res.send(JSON.stringify({ "status": ((error) ? 500 : 200), "error": error, "response": results }))
});
}
//#endregion

View File

@ -37,7 +37,7 @@ var routes = function () {
const produto = results[0];
if (produto['idtiposlicenca'] == 6) {
const sqlGerarChave = `CALL GerarInserirChave(?, ?, ?, @SerialKey, @idChave);`;
mysqlConnection.query(sqlGerarChave, [produto['cpf'], 30, produto['idsistemas']], function (chaveError, resChave) {
mysqlConnection.query(sqlGerarChave, [produto['cpf'], 90, produto['idsistemas']], function (chaveError, resChave) {
if (chaveError) {
return res.status(500).send({ error: 'Erro ao gerar chave.' });
}

View File

@ -0,0 +1,424 @@
'use strict';
require('../connector');
const express = require('express');
const router = express.Router();
const {
gerarOrdemServicoPdf,
gerarRelatorioOrdensServicoPdf
} = require('../services/orionPdfService');
function getPool() {
if (!global.ConexaoMySQL_Oriontard) {
throw new Error('Pool do banco Oriontard não foi inicializado.');
}
return global.ConexaoMySQL_Oriontard;
}
function dbQuery(sql, params = []) {
return new Promise((resolve, reject) => {
getPool().query(sql, params, (error, results, fields) => {
if (error) {
reject(error);
return;
}
resolve({ results, fields });
});
});
}
function positiveInteger(value, fieldName) {
const number = Number(value);
if (!Number.isInteger(number) || number <= 0) {
const error = new Error(`${fieldName} inválido.`);
error.httpStatus = 400;
throw error;
}
return number;
}
function sendError(req, res, error) {
const status = error.httpStatus || 500;
const message = status >= 500
? 'Erro interno ao gerar o relatório.'
: error.message;
console.error(
`${new Date().toUTCString()} - ${req.originalUrl} ${status} error`,
error
);
if (res.headersSent) {
res.destroy(error);
return;
}
res.status(status).json({
status,
error: {
code: error.code || null,
message
},
response: null
});
}
function dateToSql(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function parseDateOrDefault(value, fallback) {
if (!value) {
return fallback;
}
const date = new Date(`${value}T00:00:00`);
if (Number.isNaN(date.getTime())) {
const error = new Error('Data de filtro inválida.');
error.httpStatus = 400;
throw error;
}
return date;
}
async function loadFullOrder(id) {
const [
{ results: orders },
{ results: activities },
{ results: technicians },
{ results: materials },
{ results: odometers },
{ results: payments }
] = await Promise.all([
dbQuery(
`
SELECT
os.*,
t.nome AS tecnico_responsavel_nome,
GREATEST(os.valor_total - os.valor_pago, 0) AS valor_pendente
FROM manut_ordens_servico os
LEFT JOIN manut_tecnicos t
ON t.id = os.tecnico_responsavel_id
WHERE os.id = ?
AND os.excluido_em IS NULL
LIMIT 1;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_ordem_servico_atividades
WHERE ordem_servico_id = ?
ORDER BY data_atividade, ordem_exibicao, id;
`,
[id]
),
dbQuery(
`
SELECT
at.atividade_id,
at.tecnico_id,
at.papel,
at.horas_trabalhadas_expediente,
at.horas_trabalhadas_extras,
t.nome AS tecnico_nome
FROM manut_ordem_servico_atividade_tecnicos at
INNER JOIN manut_tecnicos t
ON t.id = at.tecnico_id
INNER JOIN manut_ordem_servico_atividades a
ON a.id = at.atividade_id
WHERE a.ordem_servico_id = ?
ORDER BY t.nome;
`,
[id]
),
dbQuery(
`
SELECT mat.*
FROM manut_ordem_servico_atividade_materiais mat
INNER JOIN manut_ordem_servico_atividades a
ON a.id = mat.atividade_id
WHERE a.ordem_servico_id = ?
ORDER BY mat.id;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_equipamento_odometro_leituras
WHERE ordem_servico_id = ?
ORDER BY leitura_em, id;
`,
[id]
),
dbQuery(
`
SELECT *
FROM manut_ordem_servico_pagamentos
WHERE ordem_servico_id = ?
AND cancelado_em IS NULL
ORDER BY data_pagamento, id;
`,
[id]
)
]);
if (!orders.length) {
const error = new Error('Ordem de serviço não encontrada.');
error.httpStatus = 404;
throw error;
}
const techniciansByActivity = new Map();
technicians.forEach(item => {
if (!techniciansByActivity.has(item.atividade_id)) {
techniciansByActivity.set(item.atividade_id, []);
}
techniciansByActivity.get(item.atividade_id).push(item);
});
const materialsByActivity = new Map();
materials.forEach(item => {
if (!materialsByActivity.has(item.atividade_id)) {
materialsByActivity.set(item.atividade_id, []);
}
materialsByActivity.get(item.atividade_id).push(item);
});
const nestedActivities = activities.map(activity => ({
...activity,
tecnicos: techniciansByActivity.get(activity.id) || [],
materiais: materialsByActivity.get(activity.id) || []
}));
return {
...orders[0],
atividades: nestedActivities,
odometros_da_os: odometers,
pagamentos: payments
};
}
function buildReportFilters(query) {
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const dataInicio = parseDateOrDefault(query.data_inicio, firstDay);
const dataFim = parseDateOrDefault(query.data_fim, lastDay);
if (dataFim < dataInicio) {
const error = new Error('A data final não pode ser anterior à data inicial.');
error.httpStatus = 400;
throw error;
}
const criterios = {
data_entrada: {
coluna: 'os.data_entrada',
label: 'data de entrada'
},
data_finalizacao: {
coluna: 'os.data_finalizacao',
label: 'data de finalização'
},
data_retirada: {
coluna: 'os.data_retirada',
label: 'data de retirada'
}
};
const criterio = criterios[query.criterio] || criterios.data_finalizacao;
return {
dataInicio,
dataFim,
dataInicioSql: dateToSql(dataInicio),
dataFimSql: dateToSql(dataFim),
criterio,
clienteId: query.cliente_id
? positiveInteger(query.cliente_id, 'Cliente')
: null,
equipamentoId: query.equipamento_id
? positiveInteger(query.equipamento_id, 'Equipamento')
: null,
status: query.status || null,
statusPagamento: query.status_pagamento || null,
tipo: query.tipo || null,
busca: query.busca ? String(query.busca).trim() : null,
cobrancaAberta:
query.cobranca_aberta === '1' ||
query.cobranca_aberta === 'true'
};
}
async function loadReportOrders(query) {
const filtros = buildReportFilters(query);
const where = [
'os.excluido_em IS NULL',
"os.status NOT IN ('RASCUNHO', 'CANCELADA')",
`${filtros.criterio.coluna} >= ?`,
`${filtros.criterio.coluna} < DATE_ADD(?, INTERVAL 1 DAY)`
];
const params = [
filtros.dataInicioSql,
filtros.dataFimSql
];
if (filtros.clienteId) {
where.push('os.cliente_id = ?');
params.push(filtros.clienteId);
}
if (filtros.equipamentoId) {
where.push('os.equipamento_id = ?');
params.push(filtros.equipamentoId);
}
if (filtros.status) {
where.push('os.status = ?');
params.push(filtros.status);
}
if (filtros.statusPagamento) {
where.push('os.status_pagamento = ?');
params.push(filtros.statusPagamento);
}
if (filtros.tipo) {
where.push('os.tipo = ?');
params.push(filtros.tipo);
}
if (filtros.cobrancaAberta) {
where.push("os.status_pagamento IN ('PENDENTE', 'PARCIAL')");
}
if (filtros.busca) {
const search = `%${filtros.busca}%`;
where.push(`(
CAST(os.id AS CHAR) LIKE ? OR
os.equipamento_nome_snapshot LIKE ? OR
os.numero_serie_snapshot LIKE ? OR
os.problema_relatado LIKE ? OR
os.cliente_nome_snapshot LIKE ?
)`);
params.push(search, search, search, search, search);
}
const { results } = await dbQuery(
`
SELECT
os.id,
os.cliente_id,
os.cliente_nome_snapshot,
os.equipamento_nome_snapshot,
os.numero_serie_snapshot,
os.tipo,
os.status,
os.status_pagamento,
os.data_entrada,
os.data_finalizacao,
os.data_retirada,
os.total_horas_expediente,
os.total_horas_extras,
os.valor_mao_obra,
os.valor_insumos,
os.valor_desconto,
os.valor_acrescimo,
os.valor_total,
os.valor_pago,
GREATEST(os.valor_total - os.valor_pago, 0) AS valor_pendente
FROM manut_ordens_servico os
WHERE ${where.join(' AND ')}
ORDER BY
os.cliente_nome_snapshot,
${filtros.criterio.coluna},
os.id;
`,
params
);
let clienteNome = null;
if (filtros.clienteId) {
const { results: clientes } = await dbQuery(
`
SELECT nome_fantasia
FROM manut_clientes
WHERE id = ?
LIMIT 1;
`,
[filtros.clienteId]
);
clienteNome = clientes.length
? clientes[0].nome_fantasia
: null;
}
return {
ordens: results,
filtros: {
dataInicio: filtros.dataInicio,
dataFim: filtros.dataFim,
criterioLabel: filtros.criterio.label,
clienteNome
}
};
}
var routes = function () {
router.get('/downloadOrdemServicoPdf/:id', async function (req, res) {
try {
const id = positiveInteger(req.params.id, 'Ordem de serviço');
const interno =
req.query.interno === '1' ||
req.query.interno === 'true';
const ordem = await loadFullOrder(id);
gerarOrdemServicoPdf(res, ordem, {
interno
});
} catch (error) {
sendError(req, res, error);
}
});
router.get('/downloadRelatorioOrdensServicoPdf', async function (req, res) {
try {
const resultado = await loadReportOrders(req.query || {});
gerarRelatorioOrdensServicoPdf(
res,
resultado.ordens,
resultado.filtros
);
} catch (error) {
sendError(req, res, error);
}
});
return router;
};
module.exports = routes;

File diff suppressed because it is too large Load Diff

155
server/package-lock.json generated
View File

@ -203,6 +203,16 @@
"yargs": "^16.2.0"
}
},
"@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="
},
"@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="
},
"@panva/asn1.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
@ -298,6 +308,21 @@
"@sendgrid/helpers": "^8.0.0"
}
},
"@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"requires": {
"tslib": "^2.8.0"
},
"dependencies": {
"tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
}
}
},
"@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -510,8 +535,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"optional": true
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bignumber.js": {
"version": "4.0.4",
@ -569,6 +593,29 @@
"fill-range": "^7.1.1"
}
},
"brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"requires": {
"base64-js": "^1.1.2"
}
},
"browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"requires": {
"pako": "~1.0.5"
},
"dependencies": {
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
}
}
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@ -605,6 +652,11 @@
"wrap-ansi": "^7.0.0"
}
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -738,6 +790,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"dicer": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz",
@ -997,8 +1054,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"optional": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-text-encoding": {
"version": "1.0.3",
@ -1113,6 +1169,22 @@
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"requires": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -1571,6 +1643,11 @@
"@panva/asn1.js": "^1.0.0"
}
},
"js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="
},
"json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
@ -1671,6 +1748,22 @@
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"requires": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
},
"dependencies": {
"base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="
}
}
},
"lodash": {
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
@ -1976,6 +2069,11 @@
"yocto-queue": "^0.1.0"
}
},
"pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@ -1996,11 +2094,32 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"pdfkit": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz",
"integrity": "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==",
"requires": {
"@noble/ciphers": "^1.0.0",
"@noble/hashes": "^1.6.0",
"fontkit": "^2.0.4",
"js-md5": "^0.8.3",
"linebreak": "^1.1.0",
"png-js": "^1.1.0"
}
},
"picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="
},
"png-js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
"integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
"requires": {
"browserify-zlib": "^0.2.0"
}
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
@ -2147,6 +2266,11 @@
"path-parse": "^1.0.6"
}
},
"restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
},
"retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -2354,6 +2478,11 @@
"uuid": "^8.0.0"
}
},
"tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -2400,6 +2529,24 @@
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
},
"unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"requires": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"requires": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"unique-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",

View File

@ -24,6 +24,7 @@
"mysql": "^2.15.0",
"nodemailer": "^6.10.1",
"nodemon": "^3.1.14",
"pdfkit": "^0.19.1",
"shelljs": "^0.8.4",
"unzipper": "^0.12.3"
}

View File

@ -29,6 +29,8 @@ app.use('/email', require('./controllers/emailController').router);
app.use('/checkout', require('./controllers/checkoutController')());
app.use('/otp', require('./controllers/oriontardproController')());
app.use('/otp', require('./controllers/oriontard.pdf.controller')());
// Allteeth
app.use('/api_5/sincronia', require('./controllers/allteeth/sincroniaController')());

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ import { BingoSortComponent } from './bingo-sort/bingo-sort.component';
import { BingoComponent } from './bingo/bingo.component';
import { BingoDashboardComponent } from './bingo-dashboard/bingo-dashboard.component';
import { BingoService } from './services/bingoService';
import { OrionDashboardComponent } from './orion-dashboard/orion-dashboard.component';
import { OrionDashboardComponent } from './oriontard/orion-dashboard/orion-dashboard.component';
import { OrionService } from './services/orionService';
import { ProdutosComponent } from './produtos/produtos.component';
import { ProdutosService } from './services/produtosService';
@ -36,6 +36,8 @@ import { AgrobaseService } from './services/agrobase.service';
import { TermosUsoComponent } from './termos-uso/termos-uso.component';
import { PoliticaPrivacidadeComponent } from './politica-privacidade/politica-privacidade.component';
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';
const routes: Routes = [
@ -149,10 +151,24 @@ const routes: Routes = [
component: BingoSortComponent,
},
]
}, {
},
{
path: 'orion-dashboard',
component: OrionDashboardComponent
}, {
},
{
path: 'orion-ordens-servico/nova',
component: OrionOrdemServicoFormComponent
},
{
path: 'orion-ordens-servico/:id/editar',
component: OrionOrdemServicoFormComponent
},
{
path: 'orion-ordens-servico',
component: OrionOrdensServicoComponent
},
{
path: 'agrobase',
children: [
{

View File

@ -26,7 +26,7 @@ import { EasyAVAComponent } from './easy-ava/easy-ava.component';
import { BingoSortComponent } from './bingo-sort/bingo-sort.component';
import { BingoComponent } from './bingo/bingo.component';
import { BingoDashboardComponent } from './bingo-dashboard/bingo-dashboard.component';
import { OrionDashboardComponent } from './orion-dashboard/orion-dashboard.component';
import { OrionDashboardComponent } from './oriontard/orion-dashboard/orion-dashboard.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDatepickerModule } from '@angular/material/datepicker';
@ -49,6 +49,8 @@ import { TipoArquivoNomePipe } from './pipes/tipo-arquivo-nome.pipe';
import { SobreComponent } from './sobre/sobre.component';
import { PoliticaPrivacidadeComponent } from './politica-privacidade/politica-privacidade.component';
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';
// Registrar a localidade pt-BR
@ -79,7 +81,7 @@ registerLocaleData(localePt);
ProdutosComponent,
AgrobaseLoginComponent,
AgrobaseVersionamentoComponent,
TipoArquivoNomePipe, SobreComponent, PoliticaPrivacidadeComponent, TermosUsoComponent
TipoArquivoNomePipe, SobreComponent, PoliticaPrivacidadeComponent, TermosUsoComponent, OrionOrdensServicoComponent, OrionOrdemServicoFormComponent
],
imports: [
BrowserModule,

View File

@ -71,7 +71,7 @@ export class ChavesComponent implements OnInit {
return;
}
if (confirm(Mensagem)) {
this.GerarChave(90);
this.GerarChave(30);
}
else {
return;

View File

@ -149,13 +149,34 @@ export class DashboardComponent implements OnInit {
{
index: 5,
texto: "OrionTard Pro",
link: "/orion-dashboard",
havesub: false,
link: "",
havesub: true,
dropicon: "arrow_drop_down",
icon: "home",
issub: false,
sub: [
{
index: 0,
texto: "Logs",
link: "/orion-dashboard",
icon: "update",
havesub: false,
dropicon: "",
issub: true,
sub: null,
},
{
index: 1,
texto: "Ordens de Serviço",
link: "/orion-ordens-servico",
icon: "update",
havesub: false,
dropicon: "",
issub: true,
sub: null,
},
],
},
{
index: 6,
texto: "AgroBase",

View File

@ -90,3 +90,424 @@ export class LogAgrupado {
key: string;
value: OrionLogsDetalhesModel[];
}
// ============================================================
// RESPOSTA PADRÃO DA API
// ============================================================
export interface OrionApiErrorModel {
code?: string;
message: string;
}
export interface OrionApiResponseModel<T> {
status: number;
error: OrionApiErrorModel | null;
response: T;
}
// ============================================================
// CADASTROS AUXILIARES
// ============================================================
export interface ClienteManutencaoModel {
id: number;
nome_fantasia: string;
razao_social?: string;
documento?: string;
}
export interface EquipamentoManutencaoModel {
id: number;
cliente_id?: number;
modelo_id?: number;
codigo_interno?: string;
nome: string;
numero_serie: string;
versao?: string;
odometro_total_segundos: number;
odometro_conectado_segundos: number;
odometro_movimento_segundos: number;
odometro_parcial_segundos: number;
odometro_atualizado_em?: string;
modelo_nome?: string;
cliente_nome?: string;
}
export interface TecnicoManutencaoModel {
id: number;
usuario_sistema_id?: number;
nome: string;
especialidade?: string;
valor_hora_expediente_padrao: number;
valor_hora_extra_padrao: number;
}
export interface MaterialManutencaoModel {
id: number;
codigo?: string;
descricao: string;
tipo: MaterialManutencaoTipo;
unidade: string;
custo_padrao: number;
preco_venda_padrao: number;
}
export type MaterialManutencaoTipo =
| 'PECA'
| 'INSUMO'
| 'SERVICO_TERCEIRO'
| 'FRETE'
| 'FERRAMENTA'
| 'OUTRO';
// ============================================================
// ORDENS DE SERVIÇO
// ============================================================
export interface OrdemServicoListaModel {
id: number;
cliente_id?: number;
equipamento_id?: number;
tecnico_responsavel_id?: number;
cliente_nome_snapshot?: string;
equipamento_nome_snapshot?: string;
numero_serie_snapshot?: string;
modelo_snapshot?: string;
versao_snapshot?: string;
tipo: OrdemServicoTipo;
status: OrdemServicoStatus;
status_pagamento: OrdemServicoStatusPagamento;
prioridade: OrdemServicoPrioridade;
data_entrada: string;
data_previsao?: string;
data_finalizacao?: string;
data_retirada?: string;
problema_relatado?: string;
total_horas_expediente: number;
total_horas_extras: number;
valor_mao_obra: number;
valor_insumos: number;
valor_desconto: number;
valor_acrescimo: number;
valor_total: number;
valor_pago: number;
valor_pendente: number;
tecnico_responsavel_nome?: string;
created_at: string;
updated_at: string;
}
export interface OrdemServicoPaginacaoModel {
page: number;
pageSize: number;
total: number;
totalPages: number;
}
export interface OrdemServicoListaResponseModel {
items: OrdemServicoListaModel[];
pagination: OrdemServicoPaginacaoModel;
}
export interface OrdemServicoFiltrosModel {
page?: number;
pageSize?: number;
busca?: string;
status?: OrdemServicoStatus | '';
status_pagamento?: OrdemServicoStatusPagamento | '';
tipo?: OrdemServicoTipo | '';
equipamento_id?: number;
cliente_id?: number;
data_inicio?: string;
data_fim?: string;
}
export interface OrdemServicoDetalhesModel extends OrdemServicoListaModel {
diagnostico?: string;
solucao_resumo?: string;
responsavel_entrega?: string;
responsavel_retirada?: string;
observacoes_internas?: string;
observacoes_cliente?: string;
garantia_dias?: number;
garantia_ate?: string;
custo_insumos: number;
justificativa_ajuste_valor?: string;
origem?: string;
atividades: OrdemServicoAtividadeModel[];
odometros_da_os: OrdemServicoOdometroModel[];
intervalo_desde_leitura_anterior?: OrdemServicoIntervaloOdometroModel;
pagamentos: OrdemServicoPagamentoModel[];
historico_status: OrdemServicoHistoricoStatusModel[];
}
export interface OrdemServicoAtividadeModel {
id: number;
ordem_servico_id: number;
data_atividade: string;
descricao: string;
ordem_exibicao: number;
horas_expediente: number;
horas_extras: number;
valor_hora_expediente: number;
valor_hora_extra: number;
valor_mao_obra: number;
cobravel: boolean | number;
observacoes_internas?: string;
observacoes_cliente?: string;
tecnicos: OrdemServicoAtividadeTecnicoModel[];
materiais: OrdemServicoAtividadeMaterialModel[];
created_at?: string;
updated_at?: string;
}
export interface OrdemServicoAtividadeTecnicoModel {
atividade_id?: number;
tecnico_id: number;
tecnico_nome?: string;
papel: OrdemServicoTecnicoPapel;
horas_trabalhadas_expediente?: number;
horas_trabalhadas_extras?: number;
}
export interface OrdemServicoAtividadeMaterialModel {
id?: number;
atividade_id?: number;
material_id?: number;
descricao_snapshot: string;
unidade_snapshot: string;
quantidade: number;
valor_custo_unitario: number;
valor_cobrado_unitario: number;
valor_custo_total?: number;
valor_cobrado_total?: number;
cobravel: boolean | number;
observacoes?: string;
}
export interface OrdemServicoOdometroModel {
id?: number;
equipamento_id?: number;
ordem_servico_id?: number;
tipo_leitura: OrdemServicoTipoLeituraOdometro;
origem?: string;
leitura_em?: string;
odometro_total_segundos: number;
odometro_conectado_segundos: number;
odometro_movimento_segundos: number;
odometro_parcial_segundos: number;
odometro_total_anterior?: number;
odometro_conectado_anterior?: number;
odometro_movimento_anterior?: number;
observacoes?: string;
usuario_id?: number;
}
export interface OrdemServicoIntervaloOdometroModel {
total_segundos?: number;
conectado_segundos?: number;
movimento_segundos?: number;
}
export interface OrdemServicoPagamentoModel {
id?: number;
ordem_servico_id?: number;
data_pagamento?: string;
valor: number;
forma_pagamento: OrdemServicoFormaPagamento;
referencia?: string;
observacoes?: string;
usuario_id?: number;
cancelado_em?: string;
}
export interface OrdemServicoHistoricoStatusModel {
id: number;
ordem_servico_id: number;
status_anterior?: OrdemServicoStatus;
status_novo: OrdemServicoStatus;
observacoes?: string;
alterado_por_usuario_id?: number;
created_at: string;
}
export interface OrdemServicoSalvarModel {
cliente_id?: number;
equipamento_id?: number;
tecnico_responsavel_id?: number;
equipamento_nome?: string;
numero_serie?: string;
modelo?: string;
versao?: string;
tipo?: OrdemServicoTipo;
status?: OrdemServicoStatus;
status_pagamento?: OrdemServicoStatusPagamento;
prioridade?: OrdemServicoPrioridade;
data_entrada?: string;
data_previsao?: string;
data_finalizacao?: string;
data_retirada?: string;
problema_relatado?: string;
diagnostico?: string;
solucao_resumo?: string;
responsavel_entrega?: string;
responsavel_retirada?: string;
observacoes_internas?: string;
observacoes_cliente?: string;
garantia_dias?: number;
garantia_ate?: string;
valor_desconto?: number;
valor_acrescimo?: number;
justificativa_ajuste_valor?: string;
observacao_status?: string;
usuario_id?: number;
odometro_entrada?: OrdemServicoOdometroModel;
}
export interface OrdemServicoAtividadeSalvarModel {
data_atividade?: string;
descricao: string;
ordem_exibicao?: number;
horas_expediente?: number;
horas_extras?: number;
valor_hora_expediente?: number;
valor_hora_extra?: number;
cobravel?: boolean;
observacoes_internas?: string;
observacoes_cliente?: string;
usuario_id?: number;
tecnicos?: OrdemServicoAtividadeTecnicoModel[];
materiais?: OrdemServicoAtividadeMaterialModel[];
}
export interface OrdemServicoTotaisModel {
total_horas_expediente: number;
total_horas_extras: 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;
status_pagamento: OrdemServicoStatusPagamento;
}
export interface OrdemServicoOperacaoResultModel {
id: number;
ordem_servico_id?: number;
equipamento_id?: number;
excluida?: boolean;
cancelado?: boolean;
tipo_leitura?: OrdemServicoTipoLeituraOdometro;
totais_os?: OrdemServicoTotaisModel;
}
export interface OrionLogInsertModel {
cpf?: string;
perfil?: string;
latitude?: number;
longitude?: number;
log: string;
}
export type OrdemServicoTipo =
| 'CORRETIVA'
| 'PREVENTIVA'
| 'REFORMA'
| 'DESENVOLVIMENTO'
| 'FABRICACAO'
| 'GARANTIA'
| 'CORTESIA'
| 'INTERNA'
| 'OUTRO';
export type OrdemServicoStatus =
| 'RASCUNHO'
| 'ABERTA'
| 'EM_DIAGNOSTICO'
| 'AGUARDANDO_APROVACAO'
| 'AGUARDANDO_PECA'
| 'EM_EXECUCAO'
| 'EM_TESTES'
| 'FINALIZADA'
| 'AGUARDANDO_RETIRADA'
| 'RETIRADA'
| 'CANCELADA';
export type OrdemServicoStatusPagamento =
| 'NAO_APLICAVEL'
| 'PENDENTE'
| 'PARCIAL'
| 'RECEBIDO'
| 'CORTESIA';
export type OrdemServicoPrioridade =
| 'BAIXA'
| 'NORMAL'
| 'ALTA'
| 'URGENTE';
export type OrdemServicoTecnicoPapel =
| 'RESPONSAVEL'
| 'EXECUTOR'
| 'APOIO';
export type OrdemServicoFormaPagamento =
| 'DINHEIRO'
| 'PIX'
| 'TRANSFERENCIA'
| 'BOLETO'
| 'CARTAO'
| 'OUTRO';
export type OrdemServicoTipoLeituraOdometro =
| 'CADASTRO_INICIAL'
| 'ENTRADA_MANUTENCAO'
| 'SAIDA_MANUTENCAO'
| 'ATUALIZACAO_MANUAL'
| 'TELEMETRIA';

View File

@ -1,8 +1,8 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { Chart } from 'chart.js';
import { LogAgrupado, OrionLogsDetalhesModel, OrionLogSensorModel, OrionLogsModel } from '../models/orionModel';
import { OrionService } from '../services/orionService';
import { LogAgrupado, OrionLogsDetalhesModel, OrionLogSensorModel, OrionLogsModel } from '../../models/orionModel';
import { OrionService } from '../../services/orionService';
import { MatTableDataSource } from '@angular/material/table';
import { MatTabChangeEvent } from '@angular/material';

View File

@ -0,0 +1,720 @@
<div class="os-form-page">
<header class="page-header">
<div class="header-main">
<button mat-stroked-button type="button" (click)="voltar()">Voltar</button>
<div>
<span class="eyebrow">ORIONTARD · MANUTENÇÃO</span>
<h1 *ngIf="!modoEdicao">Nova ordem de serviço</h1>
<h1 *ngIf="modoEdicao">OS #{{ ordemServicoId }}</h1>
<p *ngIf="!modoEdicao">Registre a entrada do equipamento e os odômetros recebidos.</p>
<p *ngIf="modoEdicao && ordem">
{{ ordem.equipamento_nome_snapshot || ordem.modelo_snapshot }}
<span *ngIf="ordem.numero_serie_snapshot">· NS {{ ordem.numero_serie_snapshot }}</span>
</p>
</div>
</div>
<div class="header-actions">
<ng-container *ngIf="modoEdicao && ordem">
<span class="status-badge" [ngClass]="classeStatus(ordem.status)">
{{ labelStatus(ordem.status) }}
</span>
<span class="status-badge" [ngClass]="classePagamento(ordem.status_pagamento)">
{{ labelPagamento(ordem.status_pagamento) }}
</span>
</ng-container>
<button mat-raised-button color="primary" type="button"
(click)="salvarOrdem()" [disabled]="carregando || salvando">
{{ salvando ? 'Salvando...' : (modoEdicao ? 'Salvar alterações' : 'Criar OS') }}
</button>
<button
mat-stroked-button
type="button"
*ngIf="modoEdicao"
(click)="baixarPdfCliente()"
[disabled]="baixandoPdf || salvando">
{{ baixandoPdf ? 'Gerando PDF...' : 'Baixar OS' }}
</button>
</div>
</header>
<div class="message message-error" *ngIf="erro">{{ erro }}</div>
<div class="message message-success" *ngIf="sucesso">{{ sucesso }}</div>
<div class="loading-card" *ngIf="carregando">
<div class="loading-bar"></div>
<span>Carregando dados da ordem de serviço...</span>
</div>
<ng-container *ngIf="!carregando">
<form [formGroup]="formOrdem" class="section-card">
<div class="section-heading">
<div>
<span class="section-kicker">DADOS GERAIS</span>
<h2>Entrada e identificação</h2>
</div>
</div>
<div class="form-grid form-grid-4">
<mat-form-field appearance="outline" class="span-2">
<mat-label>Equipamento</mat-label>
<mat-select formControlName="equipamento_id"
(selectionChange)="equipamentoSelecionado($event.value)">
<mat-option *ngFor="let equipamento of equipamentos" [value]="equipamento.id">
{{ equipamento.nome }} · NS {{ equipamento.numero_serie }}
<span *ngIf="equipamento.modelo_nome">· {{ equipamento.modelo_nome }}</span>
</mat-option>
</mat-select>
<mat-error>Selecione o equipamento.</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cliente</mat-label>
<mat-select formControlName="cliente_id">
<mat-option [value]="null">Não informado</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>Técnico responsável</mat-label>
<mat-select formControlName="tecnico_responsavel_id">
<mat-option [value]="null">Não definido</mat-option>
<mat-option *ngFor="let tecnico of tecnicos" [value]="tecnico.id">
{{ tecnico.nome }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Tipo</mat-label>
<mat-select
formControlName="tipo"
(selectionChange)="tipoOrdemAlterado($event.value)">
<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>Situação financeira</mat-label>
<mat-select formControlName="status_pagamento" disabled>
<mat-option value="NAO_APLICAVEL">
Não aplicável
</mat-option>
<mat-option value="PENDENTE">
Pendente
</mat-option>
<mat-option value="PARCIAL">
Parcial
</mat-option>
<mat-option value="RECEBIDO">
Recebido
</mat-option>
<mat-option value="CORTESIA">
Cortesia
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Status</mat-label>
<mat-select formControlName="status">
<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>Prioridade</mat-label>
<mat-select formControlName="prioridade">
<mat-option *ngFor="let opcao of prioridadeOpcoes" [value]="opcao.valor">
{{ opcao.texto }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Data e hora de entrada</mat-label>
<input matInput type="datetime-local" formControlName="data_entrada">
<mat-error>Informe a data de entrada.</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Previsão</mat-label>
<input matInput type="datetime-local" formControlName="data_previsao">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Finalização</mat-label>
<input matInput type="datetime-local" formControlName="data_finalizacao">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Retirada</mat-label>
<input matInput type="datetime-local" formControlName="data_retirada">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Responsável pela entrega</mat-label>
<input matInput formControlName="responsavel_entrega">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Responsável pela retirada</mat-label>
<input matInput formControlName="responsavel_retirada">
</mat-form-field>
<mat-form-field appearance="outline" class="span-4">
<mat-label>Problema relatado pelo cliente</mat-label>
<textarea matInput rows="3" formControlName="problema_relatado"></textarea>
<mat-error>Descreva o problema relatado.</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Diagnóstico</mat-label>
<textarea matInput rows="4" formControlName="diagnostico"></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Solução resumida</mat-label>
<textarea matInput rows="4" formControlName="solucao_resumo"></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observações internas</mat-label>
<textarea matInput rows="3" formControlName="observacoes_internas"></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observações para o cliente</mat-label>
<textarea matInput rows="3" formControlName="observacoes_cliente"></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Garantia em dias</mat-label>
<input matInput type="number" min="0" formControlName="garantia_dias">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Garantia até</mat-label>
<input matInput type="date" formControlName="garantia_ate">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Desconto</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_desconto">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Acréscimo</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_acrescimo">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Justificativa do ajuste financeiro</mat-label>
<input matInput formControlName="justificativa_ajuste_valor">
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observação da alteração de status</mat-label>
<input matInput formControlName="observacao_status">
</mat-form-field>
</div>
</form>
<section class="section-card" *ngIf="!modoEdicao" [formGroup]="formOdometroEntrada">
<div class="section-heading">
<div>
<span class="section-kicker">LEITURA DE ENTRADA</span>
<h2>Odômetros recebidos</h2>
<p>Confirme os valores exibidos pelo robô quando ele chegou ao laboratório.</p>
</div>
</div>
<div class="form-grid form-grid-4">
<mat-form-field appearance="outline">
<mat-label>Total em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_total_segundos">
<mat-hint>{{ segundosParaTempo(formOdometroEntrada.get('odometro_total_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Conectado em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_conectado_segundos">
<mat-hint>{{ segundosParaTempo(formOdometroEntrada.get('odometro_conectado_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Movimento em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_movimento_segundos">
<mat-hint>{{ segundosParaTempo(formOdometroEntrada.get('odometro_movimento_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Parcial em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_parcial_segundos">
<mat-hint>{{ segundosParaTempo(formOdometroEntrada.get('odometro_parcial_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Momento da leitura</mat-label>
<input matInput type="datetime-local" formControlName="leitura_em">
</mat-form-field>
<mat-form-field appearance="outline" class="span-3">
<mat-label>Observações da leitura</mat-label>
<input matInput formControlName="observacoes">
</mat-form-field>
</div>
</section>
<mat-tab-group *ngIf="modoEdicao && ordem" class="os-tabs">
<mat-tab label="Atividades">
<div class="tab-content">
<div class="tab-toolbar">
<div>
<h2>Atividades realizadas</h2>
<p>Adicione um registro para cada dia ou etapa da manutenção.</p>
</div>
<button mat-raised-button color="primary" type="button"
(click)="abrirNovaAtividade()" *ngIf="!exibirFormAtividade">
Adicionar atividade
</button>
</div>
<form class="inline-form-card" [formGroup]="formAtividade" *ngIf="exibirFormAtividade">
<div class="inline-form-header">
<div>
<span class="section-kicker">ATIVIDADE</span>
<h3>{{ atividadeEditandoId ? 'Editar atividade' : 'Nova atividade' }}</h3>
</div>
<button mat-button type="button" (click)="cancelarEdicaoAtividade()">Cancelar</button>
</div>
<div class="form-grid form-grid-4">
<mat-form-field appearance="outline">
<mat-label>Data</mat-label>
<input matInput type="date" formControlName="data_atividade">
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Técnicos</mat-label>
<mat-select multiple formControlName="tecnico_ids">
<mat-option *ngFor="let tecnico of tecnicos" [value]="tecnico.id">
{{ tecnico.nome }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Ordem de exibição</mat-label>
<input matInput type="number" min="0" formControlName="ordem_exibicao">
</mat-form-field>
<mat-form-field appearance="outline" class="span-4">
<mat-label>Descrição do serviço realizado</mat-label>
<textarea matInput rows="4" formControlName="descricao"></textarea>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Horas de expediente</mat-label>
<input matInput type="number" min="0" step="0.25" formControlName="horas_expediente">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Horas extras</mat-label>
<input matInput type="number" min="0" step="0.25" formControlName="horas_extras">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Valor hora expediente</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_hora_expediente">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Valor hora extra</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_hora_extra">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<label class="checkbox-field span-4">
<input type="checkbox" formControlName="cobravel">
<span>Atividade cobrável</span>
</label>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observações internas</mat-label>
<textarea matInput rows="2" formControlName="observacoes_internas"></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observações para o cliente</mat-label>
<textarea matInput rows="2" formControlName="observacoes_cliente"></textarea>
</mat-form-field>
</div>
<div class="materials-section" formArrayName="materiais">
<div class="subsection-heading">
<div>
<h4>Materiais e insumos</h4>
<p>Use o catálogo ou registre um item avulso.</p>
</div>
<button mat-stroked-button type="button" (click)="adicionarMaterialAtividade()">
Adicionar material
</button>
</div>
<div class="material-row"
*ngFor="let materialControl of materiaisAtividade.controls; let i = index"
[formGroupName]="i">
<mat-form-field appearance="outline">
<mat-label>Catálogo</mat-label>
<mat-select formControlName="material_id"
(selectionChange)="materialCatalogoSelecionado(i, $event.value)">
<mat-option [value]="null">Item avulso</mat-option>
<mat-option *ngFor="let material of materiais" [value]="material.id">
{{ material.descricao }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="material-description">
<mat-label>Descrição</mat-label>
<input matInput formControlName="descricao_snapshot">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Unidade</mat-label>
<input matInput formControlName="unidade_snapshot">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Quantidade</mat-label>
<input matInput type="number" min="0.001" step="0.001" formControlName="quantidade">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Custo unitário</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_custo_unitario">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Valor cobrado</mat-label>
<input matInput type="number" min="0" step="0.01" formControlName="valor_cobrado_unitario">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<label class="checkbox-field compact-checkbox">
<input type="checkbox" formControlName="cobravel">
<span>Cobrável</span>
</label>
<button mat-button type="button" class="danger-button"
(click)="removerMaterialAtividade(i)">
Remover
</button>
</div>
<div class="empty-subsection" *ngIf="materiaisAtividade.length === 0">
Nenhum material adicionado nesta atividade.
</div>
</div>
<div class="inline-form-actions">
<button mat-button type="button" (click)="cancelarEdicaoAtividade()">Cancelar</button>
<button mat-raised-button color="primary" type="button"
(click)="salvarAtividade()" [disabled]="salvandoAtividade">
{{ salvandoAtividade ? 'Salvando...' : 'Salvar atividade' }}
</button>
</div>
</form>
<div class="activity-card" *ngFor="let atividade of atividadesOrdenadas">
<div class="activity-header">
<div>
<span class="activity-date">{{ atividade.data_atividade | date:'dd/MM/yyyy' }}</span>
<h3>{{ atividade.descricao }}</h3>
</div>
<div class="activity-actions">
<button mat-button type="button" (click)="editarAtividade(atividade)">Editar</button>
<button mat-button type="button" class="danger-button"
(click)="excluirAtividade(atividade)">Excluir</button>
</div>
</div>
<div class="activity-meta-grid">
<div>
<span>Técnicos</span>
<strong>{{ nomeTecnicosAtividade(atividade) }}</strong>
</div>
<div>
<span>Horas</span>
<strong>{{ totalHorasAtividade(atividade) | number:'1.2-2' }}</strong>
<small>{{ atividade.horas_expediente | number:'1.2-2' }} exp. · {{ atividade.horas_extras | number:'1.2-2' }} extra</small>
</div>
<div>
<span>Mão de obra</span>
<strong>{{ atividade.valor_mao_obra | currency:'BRL':'symbol':'1.2-2' }}</strong>
</div>
<div>
<span>Materiais</span>
<strong>{{ valorMateriaisAtividade(atividade) | currency:'BRL':'symbol':'1.2-2' }}</strong>
</div>
</div>
<div class="materials-list" *ngIf="atividade.materiais && atividade.materiais.length > 0">
<div class="material-chip" *ngFor="let material of atividade.materiais">
{{ material.quantidade | number:'1.0-3' }} {{ material.unidade_snapshot }} · {{ material.descricao_snapshot }}
</div>
</div>
</div>
<div class="empty-state" *ngIf="atividadesOrdenadas.length === 0 && !exibirFormAtividade">
<strong>Nenhuma atividade registrada.</strong>
<p>Adicione o primeiro serviço realizado nesta manutenção.</p>
</div>
</div>
</mat-tab>
<mat-tab label="Financeiro">
<div class="tab-content">
<div class="financial-grid">
<article><span>Mão de obra</span><strong>{{ ordem.valor_mao_obra | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article><span>Insumos</span><strong>{{ ordem.valor_insumos | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article><span>Desconto</span><strong>{{ ordem.valor_desconto | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article><span>Acréscimo</span><strong>{{ ordem.valor_acrescimo | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article class="financial-total"><span>Valor total</span><strong>{{ ordem.valor_total | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article><span>Pago</span><strong>{{ ordem.valor_pago | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
<article><span>Pendente</span><strong>{{ valorPendente | currency:'BRL':'symbol':'1.2-2' }}</strong></article>
</div>
<div class="tab-toolbar">
<div>
<h2>Pagamentos</h2>
<p>O status financeiro é calculado pelos pagamentos registrados.</p>
</div>
<button mat-raised-button color="primary" type="button" (click)="exibirFormPagamento = true" *ngIf="!exibirFormPagamento || (formOrdem.get('tipo').value !== 'CORTESIA' && formOrdem.get('tipo').value !== 'GARANTIA')">
Registrar pagamento
</button>
</div>
<form class="inline-form-card" [formGroup]="formPagamento" *ngIf="exibirFormPagamento">
<div class="form-grid form-grid-4">
<mat-form-field appearance="outline">
<mat-label>Data e hora</mat-label>
<input matInput type="datetime-local" formControlName="data_pagamento">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Valor</mat-label>
<input matInput type="number" min="0.01" step="0.01" formControlName="valor">
<span matPrefix>R$&nbsp;</span>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Forma</mat-label>
<mat-select formControlName="forma_pagamento">
<mat-option *ngFor="let opcao of formaPagamentoOpcoes" [value]="opcao.valor">
{{ opcao.texto }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Referência</mat-label>
<input matInput formControlName="referencia">
</mat-form-field>
<mat-form-field appearance="outline" class="span-4">
<mat-label>Observações</mat-label>
<textarea matInput rows="2" formControlName="observacoes"></textarea>
</mat-form-field>
</div>
<div class="inline-form-actions">
<button mat-button type="button" (click)="exibirFormPagamento = false">Cancelar</button>
<button mat-raised-button color="primary" type="button" (click)="registrarPagamento()" [disabled]="salvandoPagamento">
{{ salvandoPagamento ? 'Registrando...' : 'Registrar pagamento' }}
</button>
</div>
</form>
<div class="simple-list-item" *ngFor="let pagamento of ordem.pagamentos">
<div>
<strong>{{ pagamento.valor | currency:'BRL':'symbol':'1.2-2' }}</strong>
<span>{{ pagamento.data_pagamento | date:'dd/MM/yyyy HH:mm' }} · {{ pagamento.forma_pagamento }}</span>
<small *ngIf="pagamento.referencia">{{ pagamento.referencia }}</small>
</div>
<button mat-button type="button" class="danger-button" *ngIf="!pagamento.cancelado_em" (click)="cancelarPagamento(pagamento)">
Cancelar
</button>
</div>
<div class="empty-state" *ngIf="(!ordem.pagamentos || ordem.pagamentos.length === 0) && formOrdem.get('tipo').value != 'CORTESIA' && formOrdem.get('tipo').value != 'GARANTIA'">
<strong>Nenhum pagamento registrado.</strong>
</div>
<div
class="billing-notice"
*ngIf="formOrdem.get('tipo').value === 'CORTESIA'">
Esta OS é uma cortesia. Não é necessário registrar pagamento.
</div>
<div
class="billing-notice"
*ngIf="formOrdem.get('tipo').value === 'GARANTIA'">
Esta OS está em garantia. As horas e os materiais continuam
registrados para controle interno, mas não são cobrados.
</div>
</div>
</mat-tab>
<mat-tab label="Odômetros">
<div class="tab-content">
<div class="tab-toolbar">
<div>
<h2>Histórico de odômetros</h2>
<p>Leituras registradas na entrada, durante os testes e na saída.</p>
</div>
<button mat-raised-button color="primary" type="button"
(click)="exibirFormOdometro = true" *ngIf="!exibirFormOdometro">
Nova leitura
</button>
</div>
<form class="inline-form-card" [formGroup]="formOdometro" *ngIf="exibirFormOdometro">
<div class="form-grid form-grid-4">
<mat-form-field appearance="outline">
<mat-label>Tipo de leitura</mat-label>
<mat-select formControlName="tipo_leitura">
<mat-option *ngFor="let opcao of tipoLeituraOpcoes" [value]="opcao.valor">
{{ opcao.texto }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Data e hora</mat-label>
<input matInput type="datetime-local" formControlName="leitura_em">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Total em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_total_segundos">
<mat-hint>{{ segundosParaTempo(formOdometro.get('odometro_total_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Conectado em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_conectado_segundos">
<mat-hint>{{ segundosParaTempo(formOdometro.get('odometro_conectado_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Movimento em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_movimento_segundos">
<mat-hint>{{ segundosParaTempo(formOdometro.get('odometro_movimento_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Parcial em segundos</mat-label>
<input matInput type="number" min="0" formControlName="odometro_parcial_segundos">
<mat-hint>{{ segundosParaTempo(formOdometro.get('odometro_parcial_segundos').value) }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="span-2">
<mat-label>Observações</mat-label>
<input matInput formControlName="observacoes">
</mat-form-field>
</div>
<div class="inline-form-actions">
<button mat-button type="button" (click)="exibirFormOdometro = false">Cancelar</button>
<button mat-raised-button color="primary" type="button"
(click)="registrarOdometro()" [disabled]="salvandoOdometro">
{{ salvandoOdometro ? 'Registrando...' : 'Registrar leitura' }}
</button>
</div>
</form>
<div class="odometer-card" *ngFor="let leitura of ordem.odometros_da_os">
<div class="odometer-header">
<div>
<strong>{{ leitura.tipo_leitura }}</strong>
<span>{{ leitura.leitura_em | date:'dd/MM/yyyy HH:mm' }}</span>
</div>
</div>
<div class="odometer-grid">
<div><span>Total</span><strong>{{ segundosParaTempo(leitura.odometro_total_segundos) }}</strong></div>
<div><span>Conectado</span><strong>{{ segundosParaTempo(leitura.odometro_conectado_segundos) }}</strong></div>
<div><span>Movimento</span><strong>{{ segundosParaTempo(leitura.odometro_movimento_segundos) }}</strong></div>
<div><span>Parcial</span><strong>{{ segundosParaTempo(leitura.odometro_parcial_segundos) }}</strong></div>
</div>
<p *ngIf="leitura.observacoes">{{ leitura.observacoes }}</p>
</div>
<div class="empty-state" *ngIf="!ordem.odometros_da_os || ordem.odometros_da_os.length === 0">
<strong>Nenhuma leitura vinculada a esta OS.</strong>
</div>
</div>
</mat-tab>
<mat-tab label="Histórico">
<div class="tab-content">
<div class="tab-toolbar">
<div>
<h2>Histórico de status</h2>
<p>Alterações registradas durante a vida da OS.</p>
</div>
</div>
<div class="timeline-item" *ngFor="let item of ordem.historico_status">
<div class="timeline-dot"></div>
<div>
<strong>{{ labelStatus(item.status_novo) }}</strong>
<span>{{ item.created_at | date:'dd/MM/yyyy HH:mm' }}</span>
<p *ngIf="item.observacoes">{{ item.observacoes }}</p>
</div>
</div>
<div class="empty-state" *ngIf="!ordem.historico_status || ordem.historico_status.length === 0">
<strong>Nenhuma alteração de status registrada.</strong>
</div>
</div>
</mat-tab>
</mat-tab-group>
</ng-container>
</div>

View File

@ -0,0 +1,589 @@
:host {
display: block;
min-height: 100vh;
background: #f4f6f8;
color: #1f2933;
}
.os-form-page {
max-width: 1680px;
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
}
.page-header,
.header-main,
.header-actions,
.activity-actions,
.inline-form-actions,
.section-heading,
.inline-form-header,
.subsection-heading,
.tab-toolbar,
.activity-header,
.odometer-header {
display: flex;
}
.page-header,
.section-heading,
.inline-form-header,
.subsection-heading,
.tab-toolbar,
.activity-header,
.odometer-header {
justify-content: space-between;
align-items: flex-start;
gap: 18px;
}
.page-header {
margin-bottom: 18px;
}
.header-main,
.header-actions,
.activity-actions,
.inline-form-actions {
align-items: center;
gap: 10px;
}
.header-main {
align-items: flex-start;
h1 {
margin: 4px 0 5px;
color: #101828;
font-size: 30px;
line-height: 1.15;
}
p {
margin: 0;
color: #667085;
font-size: 14px;
}
}
.header-actions {
flex-wrap: wrap;
justify-content: flex-end;
}
.eyebrow,
.section-kicker {
color: #1769aa;
font-size: 11px;
font-weight: 700;
letter-spacing: 1.1px;
}
.section-card,
.inline-form-card,
.activity-card,
.loading-card,
.odometer-card,
.simple-list-item,
.os-tabs {
background: #fff;
border: 1px solid #e4e7ec;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(16, 24, 40, 0.04);
}
.section-card {
margin-bottom: 18px;
padding: 20px;
}
.section-heading,
.inline-form-header,
.subsection-heading {
margin-bottom: 16px;
}
.section-heading h2,
.tab-toolbar h2,
.inline-form-header h3,
.subsection-heading h4 {
margin: 4px 0 0;
color: #101828;
}
.section-heading p,
.tab-toolbar p,
.subsection-heading p {
margin: 5px 0 0;
color: #667085;
font-size: 13px;
}
.form-grid {
display: grid;
gap: 12px;
align-items: start;
}
.form-grid-4 {
grid-template-columns: repeat(4, minmax(160px, 1fr));
}
.span-2 { grid-column: span 2; }
.span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; }
.form-grid .mat-form-field {
width: 100%;
}
.os-tabs {
display: block;
margin-top: 6px;
overflow: hidden;
}
.tab-content {
padding: 20px;
}
.tab-toolbar {
align-items: center;
margin-bottom: 18px;
}
.inline-form-card {
margin-bottom: 18px;
padding: 18px;
background: #fbfcfe;
}
.inline-form-actions {
justify-content: flex-end;
margin-top: 16px;
}
.checkbox-field {
display: flex;
align-items: center;
gap: 8px;
min-height: 38px;
color: #344054;
font-size: 13px;
input {
width: 16px;
height: 16px;
}
}
.compact-checkbox {
align-self: center;
}
.materials-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eaecf0;
}
.material-row {
display: grid;
grid-template-columns:
minmax(170px, 1.1fr)
minmax(220px, 1.8fr)
90px
110px
140px
140px
90px
auto;
gap: 10px;
align-items: start;
margin-bottom: 10px;
padding: 12px;
background: #fff;
border: 1px solid #eaecf0;
border-radius: 9px;
}
.empty-subsection,
.empty-state {
padding: 28px;
color: #667085;
text-align: center;
background: #f8fafc;
border: 1px dashed #d0d5dd;
border-radius: 9px;
}
.empty-state {
strong {
display: block;
color: #344054;
font-size: 15px;
}
p {
margin: 5px 0 0;
}
}
.activity-card {
margin-bottom: 14px;
padding: 18px;
}
.activity-date {
color: #1769aa;
font-size: 12px;
font-weight: 700;
}
.activity-header h3 {
max-width: 1000px;
margin: 6px 0 0;
color: #344054;
font-size: 15px;
line-height: 1.55;
font-weight: 600;
}
.activity-meta-grid,
.financial-grid,
.odometer-grid {
display: grid;
gap: 12px;
margin-top: 16px;
}
.activity-meta-grid {
grid-template-columns: 2fr repeat(3, minmax(140px, 1fr));
}
.activity-meta-grid > div,
.odometer-grid > div {
padding: 12px;
background: #f8fafc;
border-radius: 8px;
}
.activity-meta-grid span,
.odometer-grid span,
.financial-grid span {
display: block;
color: #667085;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.35px;
}
.activity-meta-grid strong,
.odometer-grid strong {
display: block;
margin-top: 5px;
color: #344054;
font-size: 13px;
}
.activity-meta-grid small {
display: block;
margin-top: 4px;
color: #98a2b3;
font-size: 11px;
}
.materials-list {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 14px;
}
.material-chip {
padding: 5px 9px;
background: #eff8ff;
color: #175cd3;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.financial-grid {
grid-template-columns: repeat(7, minmax(140px, 1fr));
margin-bottom: 24px;
}
.financial-grid article {
padding: 16px;
background: #f8fafc;
border-radius: 9px;
strong {
display: block;
margin-top: 7px;
color: #101828;
font-size: 17px;
}
}
.financial-grid .financial-total {
background: #eef6ff;
strong {
color: #175cd3;
}
}
.simple-list-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-bottom: 10px;
padding: 14px 16px;
strong,
span,
small {
display: block;
}
strong { color: #344054; }
span,
small {
margin-top: 3px;
color: #667085;
font-size: 12px;
}
}
.odometer-card {
margin-bottom: 12px;
padding: 16px;
}
.odometer-header {
strong,
span { display: block; }
strong { color: #344054; }
span {
margin-top: 3px;
color: #667085;
font-size: 12px;
}
}
.odometer-grid {
grid-template-columns: repeat(4, minmax(140px, 1fr));
}
.odometer-card p {
margin: 12px 0 0;
color: #667085;
font-size: 13px;
}
.timeline-item {
position: relative;
display: grid;
grid-template-columns: 18px 1fr;
gap: 12px;
padding-bottom: 20px;
&::before {
content: '';
position: absolute;
top: 14px;
bottom: -2px;
left: 6px;
width: 2px;
background: #e4e7ec;
}
&:last-child::before { display: none; }
strong,
span { display: block; }
strong { color: #344054; }
span {
margin-top: 3px;
color: #667085;
font-size: 12px;
}
p {
margin: 7px 0 0;
color: #475467;
font-size: 13px;
}
}
.timeline-dot {
position: relative;
z-index: 1;
width: 12px;
height: 12px;
margin-top: 3px;
background: #1769aa;
border: 3px solid #eaf4ff;
border-radius: 50%;
box-sizing: border-box;
}
.status-badge {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.badge-sucesso { color: #027a48; background: #ecfdf3; }
.badge-alerta { color: #b54708; background: #fffaeb; }
.badge-erro { color: #b42318; background: #fef3f2; }
.badge-info { color: #175cd3; background: #eff8ff; }
.badge-neutro { color: #475467; background: #f2f4f7; }
.message {
margin-bottom: 14px;
padding: 12px 15px;
border-radius: 9px;
font-size: 13px;
font-weight: 600;
}
.message-error {
color: #b42318;
background: #fef3f2;
border: 1px solid #fecdca;
}
.message-success {
color: #027a48;
background: #ecfdf3;
border: 1px solid #abefc6;
}
.loading-card {
min-height: 220px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #667085;
}
.loading-bar {
width: 170px;
height: 4px;
margin-bottom: 15px;
border-radius: 999px;
background: linear-gradient(90deg, #d0d5dd 0%, #1769aa 45%, #d0d5dd 100%);
background-size: 200% 100%;
animation: loading 1.2s linear infinite;
}
.danger-button { color: #b42318 !important; }
@keyframes loading {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
/* Blindagem contra o tema global antigo */
:host ::ng-deep .os-form-page .mat-form-field,
:host ::ng-deep .os-form-page .mat-input-element,
:host ::ng-deep .os-form-page .mat-select-value,
:host ::ng-deep .os-form-page .mat-select-value-text {
color: #101828 !important;
}
:host ::ng-deep .os-form-page .mat-form-field-label,
:host ::ng-deep .os-form-page .mat-select-arrow,
:host ::ng-deep .os-form-page .mat-hint {
color: #667085 !important;
}
:host ::ng-deep .os-form-page .mat-form-field-outline {
color: #d0d5dd !important;
}
:host ::ng-deep .os-form-page .mat-form-field-outline-thick {
color: #1769aa !important;
}
:host ::ng-deep .os-form-page .mat-form-field-flex {
background: transparent !important;
}
:host ::ng-deep .os-form-page .mat-tab-label {
color: #344054 !important;
opacity: 1 !important;
}
:host ::ng-deep .os-form-page .mat-tab-label-active {
color: #1769aa !important;
}
:host ::ng-deep .os-form-page .mat-tab-header {
background: #f8fafc !important;
}
@media (max-width: 1350px) {
.form-grid-4 { grid-template-columns: repeat(2, minmax(180px, 1fr)); }
.span-3,
.span-4 { grid-column: span 2; }
.material-row { grid-template-columns: repeat(3, minmax(160px, 1fr)); }
.financial-grid { grid-template-columns: repeat(4, minmax(150px, 1fr)); }
.activity-meta-grid { grid-template-columns: repeat(2, minmax(180px, 1fr)); }
}
@media (max-width: 760px) {
.os-form-page { padding: 14px; }
.page-header,
.header-main,
.header-actions,
.section-heading,
.tab-toolbar,
.activity-header,
.subsection-heading {
flex-direction: column;
}
.header-actions {
align-items: stretch;
width: 100%;
button { width: 100%; }
}
.form-grid-4,
.material-row,
.financial-grid,
.activity-meta-grid,
.odometer-grid {
grid-template-columns: 1fr;
}
.span-2,
.span-3,
.span-4 {
grid-column: span 1;
}
}
.billing-notice {
grid-column: span 4;
padding: 12px 14px;
color: #175cd3;
background: #eff8ff;
border: 1px solid #b2ddff;
border-radius: 8px;
font-size: 13px;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrionOrdemServicoFormComponent } from './orion-ordem-servico-form.component';
describe('OrionOrdemServicoFormComponent', () => {
let component: OrionOrdemServicoFormComponent;
let fixture: ComponentFixture<OrionOrdemServicoFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ OrionOrdemServicoFormComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrionOrdemServicoFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,911 @@
import { Component, OnInit } from '@angular/core';
import {
AbstractControl,
FormArray,
FormBuilder,
FormGroup,
Validators
} from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
ClienteManutencaoModel,
EquipamentoManutencaoModel,
MaterialManutencaoModel,
OrdemServicoAtividadeMaterialModel,
OrdemServicoAtividadeModel,
OrdemServicoAtividadeSalvarModel,
OrdemServicoAtividadeTecnicoModel,
OrdemServicoDetalhesModel,
OrdemServicoOdometroModel,
OrdemServicoPagamentoModel,
OrdemServicoSalvarModel,
OrdemServicoStatus,
OrdemServicoStatusPagamento,
OrdemServicoTipo,
TecnicoManutencaoModel
} from '../../models/orionModel';
import { OrionService } from '../../services/orionService';
@Component({
selector: 'app-orion-ordem-servico-form',
templateUrl: './orion-ordem-servico-form.component.html',
styleUrls: ['./orion-ordem-servico-form.component.scss']
})
export class OrionOrdemServicoFormComponent implements OnInit {
modoEdicao = false;
ordemServicoId: number = null;
ordem: OrdemServicoDetalhesModel = null;
carregando = true;
salvando = false;
salvandoAtividade = false;
salvandoPagamento = false;
salvandoOdometro = false;
erro = '';
sucesso = '';
formOrdem: FormGroup;
formOdometroEntrada: FormGroup;
formAtividade: FormGroup;
formPagamento: FormGroup;
formOdometro: FormGroup;
clientes: ClienteManutencaoModel[] = [];
equipamentos: EquipamentoManutencaoModel[] = [];
tecnicos: TecnicoManutencaoModel[] = [];
materiais: MaterialManutencaoModel[] = [];
exibirFormAtividade = false;
atividadeEditandoId: number = null;
exibirFormPagamento = false;
exibirFormOdometro = false;
statusOpcoes = [
{ 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' }
];
tipoOpcoes = [
{ 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' }
];
prioridadeOpcoes = [
{ valor: 'BAIXA', texto: 'Baixa' },
{ valor: 'NORMAL', texto: 'Normal' },
{ valor: 'ALTA', texto: 'Alta' },
{ valor: 'URGENTE', texto: 'Urgente' }
];
formaPagamentoOpcoes = [
{ valor: 'DINHEIRO', texto: 'Dinheiro' },
{ valor: 'PIX', texto: 'PIX' },
{ valor: 'TRANSFERENCIA', texto: 'Transferência' },
{ valor: 'BOLETO', texto: 'Boleto' },
{ valor: 'CARTAO', texto: 'Cartão' },
{ valor: 'OUTRO', texto: 'Outro' }
];
tipoLeituraOpcoes = [
{ valor: 'SAIDA_MANUTENCAO', texto: 'Saída da manutenção' },
{ valor: 'ATUALIZACAO_MANUAL', texto: 'Atualização manual' },
{ valor: 'TELEMETRIA', texto: 'Telemetria' }
];
baixandoPdf: boolean = false;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private orionService: OrionService
) {
this.criarFormularios();
}
ngOnInit() {
var idParam = this.route.snapshot.paramMap.get('id');
var id = Number(idParam);
this.modoEdicao = !!idParam && !isNaN(id) && id > 0;
this.ordemServicoId = this.modoEdicao ? id : null;
this.carregarDadosIniciais();
}
tipoOrdemAlterado(tipo: OrdemServicoTipo) {
var statusPagamentoAtual =
this.formOrdem.get('status_pagamento').value;
if (tipo === 'CORTESIA') {
this.formOrdem.patchValue({
status_pagamento: 'CORTESIA'
});
this.formAtividade.patchValue({
cobravel: false
});
return;
}
if (tipo === 'GARANTIA') {
this.formOrdem.patchValue({
status_pagamento: 'NAO_APLICAVEL'
});
this.formAtividade.patchValue({
cobravel: false
});
return;
}
if (
statusPagamentoAtual === 'CORTESIA' ||
statusPagamentoAtual === 'NAO_APLICAVEL'
) {
this.formOrdem.patchValue({
status_pagamento: 'PENDENTE'
});
}
}
private criarFormularios() {
this.formOrdem = this.formBuilder.group({
cliente_id: [null],
equipamento_id: [null, Validators.required],
tecnico_responsavel_id: [null],
tipo: ['CORRETIVA', Validators.required],
status: ['ABERTA', Validators.required],
status_pagamento: ['PENDENTE'],
prioridade: ['NORMAL', Validators.required],
data_entrada: [this.formatarDataHoraLocal(new Date()), Validators.required],
data_previsao: [''],
data_finalizacao: [''],
data_retirada: [''],
problema_relatado: ['', Validators.required],
diagnostico: [''],
solucao_resumo: [''],
responsavel_entrega: [''],
responsavel_retirada: [''],
observacoes_internas: [''],
observacoes_cliente: [''],
garantia_dias: [null],
garantia_ate: [''],
valor_desconto: [0],
valor_acrescimo: [0],
justificativa_ajuste_valor: [''],
observacao_status: ['']
});
this.formOdometroEntrada = this.formBuilder.group({
odometro_total_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_conectado_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_movimento_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_parcial_segundos: [0, [Validators.required, Validators.min(0)]],
leitura_em: [this.formatarDataHoraLocal(new Date())],
observacoes: ['']
});
this.formAtividade = this.formBuilder.group({
data_atividade: [this.formatarData(new Date()), Validators.required],
descricao: ['', Validators.required],
ordem_exibicao: [0],
horas_expediente: [0, [Validators.min(0)]],
horas_extras: [0, [Validators.min(0)]],
valor_hora_expediente: [60, [Validators.min(0)]],
valor_hora_extra: [90, [Validators.min(0)]],
cobravel: [true],
observacoes_internas: [''],
observacoes_cliente: [''],
tecnico_ids: [[]],
materiais: this.formBuilder.array([])
});
this.formPagamento = this.formBuilder.group({
data_pagamento: [this.formatarDataHoraLocal(new Date()), Validators.required],
valor: [0, [Validators.required, Validators.min(0.01)]],
forma_pagamento: ['PIX', Validators.required],
referencia: [''],
observacoes: ['']
});
this.formOdometro = this.formBuilder.group({
tipo_leitura: ['SAIDA_MANUTENCAO', Validators.required],
leitura_em: [this.formatarDataHoraLocal(new Date()), Validators.required],
odometro_total_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_conectado_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_movimento_segundos: [0, [Validators.required, Validators.min(0)]],
odometro_parcial_segundos: [0, [Validators.required, Validators.min(0)]],
observacoes: ['']
});
}
private carregarDadosIniciais() {
this.carregando = true;
this.erro = '';
Promise.all([
this.orionService.getClientesManutencao(),
this.orionService.getEquipamentosManutencao(),
this.orionService.getTecnicosManutencao(),
this.orionService.getMateriaisManutencao()
])
.then(resultados => {
this.clientes = resultados[0] || [];
this.equipamentos = resultados[1] || [];
this.tecnicos = resultados[2] || [];
this.materiais = resultados[3] || [];
if (this.modoEdicao) {
return this.carregarOrdem();
}
return null;
})
.catch(error => {
console.error('[OS FORM] Erro ao carregar dados:', error);
this.erro = this.obterMensagemErro(error, 'Não foi possível carregar os dados necessários.');
})
.then(() => {
this.carregando = false;
});
}
private carregarOrdem(): Promise<any> {
return this.orionService.getOrdemServico(this.ordemServicoId)
.then(ordem => {
this.ordem = ordem;
this.preencherFormularioOrdem(ordem);
this.preencherNovoOdometroComUltimaLeitura();
});
}
private preencherFormularioOrdem(ordem: OrdemServicoDetalhesModel) {
this.formOrdem.patchValue({
cliente_id: ordem.cliente_id || null,
equipamento_id: ordem.equipamento_id || null,
tecnico_responsavel_id: ordem.tecnico_responsavel_id || null,
tipo: ordem.tipo || 'CORRETIVA',
status: ordem.status || 'ABERTA',
status_pagamento: ordem.status_pagamento || 'PENDENTE',
prioridade: ordem.prioridade || 'NORMAL',
data_entrada: this.converterDataApiParaLocal(ordem.data_entrada),
data_previsao: this.converterDataApiParaLocal(ordem.data_previsao),
data_finalizacao: this.converterDataApiParaLocal(ordem.data_finalizacao),
data_retirada: this.converterDataApiParaLocal(ordem.data_retirada),
problema_relatado: ordem.problema_relatado || '',
diagnostico: ordem.diagnostico || '',
solucao_resumo: ordem.solucao_resumo || '',
responsavel_entrega: ordem.responsavel_entrega || '',
responsavel_retirada: ordem.responsavel_retirada || '',
observacoes_internas: ordem.observacoes_internas || '',
observacoes_cliente: ordem.observacoes_cliente || '',
garantia_dias: ordem.garantia_dias || null,
garantia_ate: ordem.garantia_ate ? String(ordem.garantia_ate).substring(0, 10) : '',
valor_desconto: this.numero(ordem.valor_desconto),
valor_acrescimo: this.numero(ordem.valor_acrescimo),
justificativa_ajuste_valor: ordem.justificativa_ajuste_valor || '',
observacao_status: ''
});
}
equipamentoSelecionado(equipamentoId: number) {
if (!equipamentoId || this.modoEdicao) {
return;
}
var equipamento = this.buscarEquipamento(equipamentoId);
if (!equipamento) {
return;
}
this.formOrdem.patchValue({ cliente_id: equipamento.cliente_id || null });
this.formOdometroEntrada.patchValue({
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)
});
}
salvarOrdem() {
this.limparMensagens();
if (this.formOrdem.invalid) {
this.marcarFormularioComoTocado(this.formOrdem);
this.erro = 'Preencha os campos obrigatórios da ordem de serviço.';
return;
}
if (!this.modoEdicao && this.formOdometroEntrada.invalid) {
this.marcarFormularioComoTocado(this.formOdometroEntrada);
this.erro = 'Confira os odômetros de entrada.';
return;
}
this.salvando = true;
var payload = this.montarPayloadOrdem();
var operacao = this.modoEdicao
? this.orionService.updateOrdemServico(this.ordemServicoId, payload)
: this.orionService.insertOrdemServico(payload);
operacao
.then(resultado => {
if (!this.modoEdicao) {
var novoId = resultado.id || resultado.ordem_servico_id;
this.router.navigate(['/orion-ordens-servico', novoId, 'editar']);
return;
}
this.sucesso = 'Ordem de serviço atualizada com sucesso.';
return this.recarregarOrdem();
})
.catch(error => {
console.error('[OS FORM] Erro ao salvar OS:', error);
this.erro = this.obterMensagemErro(error, 'Não foi possível salvar a ordem de serviço.');
})
.then(() => {
this.salvando = false;
});
}
private montarPayloadOrdem(): OrdemServicoSalvarModel {
var valor = this.formOrdem.getRawValue();
var payload: OrdemServicoSalvarModel = {
cliente_id: this.numeroOuNull(valor.cliente_id),
equipamento_id: this.numeroOuNull(valor.equipamento_id),
tecnico_responsavel_id: this.numeroOuNull(valor.tecnico_responsavel_id),
tipo: valor.tipo,
status: valor.status,
status_pagamento: valor.status_pagamento,
prioridade: valor.prioridade,
data_entrada: this.normalizarDataHoraApi(valor.data_entrada),
data_previsao: this.normalizarDataHoraApi(valor.data_previsao),
data_finalizacao: this.normalizarDataHoraApi(valor.data_finalizacao),
data_retirada: this.normalizarDataHoraApi(valor.data_retirada),
problema_relatado: this.textoOuNull(valor.problema_relatado),
diagnostico: this.textoOuNull(valor.diagnostico),
solucao_resumo: this.textoOuNull(valor.solucao_resumo),
responsavel_entrega: this.textoOuNull(valor.responsavel_entrega),
responsavel_retirada: this.textoOuNull(valor.responsavel_retirada),
observacoes_internas: this.textoOuNull(valor.observacoes_internas),
observacoes_cliente: this.textoOuNull(valor.observacoes_cliente),
garantia_dias: this.numeroOuNull(valor.garantia_dias),
garantia_ate: valor.garantia_ate || null,
valor_desconto: this.numero(valor.valor_desconto),
valor_acrescimo: this.numero(valor.valor_acrescimo),
justificativa_ajuste_valor: this.textoOuNull(valor.justificativa_ajuste_valor),
observacao_status: this.textoOuNull(valor.observacao_status)
};
if (!this.modoEdicao) {
var odometro = this.formOdometroEntrada.getRawValue();
payload.odometro_entrada = {
tipo_leitura: 'ENTRADA_MANUTENCAO',
leitura_em: this.normalizarDataHoraApi(odometro.leitura_em),
odometro_total_segundos: this.numero(odometro.odometro_total_segundos),
odometro_conectado_segundos: this.numero(odometro.odometro_conectado_segundos),
odometro_movimento_segundos: this.numero(odometro.odometro_movimento_segundos),
odometro_parcial_segundos: this.numero(odometro.odometro_parcial_segundos),
observacoes: this.textoOuNull(odometro.observacoes)
};
}
return payload;
}
abrirNovaAtividade() {
this.atividadeEditandoId = null;
this.exibirFormAtividade = true;
this.resetarFormAtividade();
}
editarAtividade(atividade: OrdemServicoAtividadeModel) {
this.atividadeEditandoId = atividade.id;
this.exibirFormAtividade = true;
this.formAtividade.patchValue({
data_atividade: atividade.data_atividade ? String(atividade.data_atividade).substring(0, 10) : this.formatarData(new Date()),
descricao: atividade.descricao || '',
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),
cobravel: !!atividade.cobravel,
observacoes_internas: atividade.observacoes_internas || '',
observacoes_cliente: atividade.observacoes_cliente || '',
tecnico_ids: (atividade.tecnicos || []).map(tecnico => tecnico.tecnico_id)
});
this.limparMateriaisAtividade();
(atividade.materiais || []).forEach(material => this.adicionarMaterialAtividade(material));
}
cancelarEdicaoAtividade() {
this.exibirFormAtividade = false;
this.atividadeEditandoId = null;
this.resetarFormAtividade();
}
salvarAtividade() {
this.limparMensagens();
if (this.formAtividade.invalid) {
this.marcarFormularioComoTocado(this.formAtividade);
this.erro = 'Preencha a data e a descrição da atividade.';
return;
}
this.salvandoAtividade = true;
var payload = this.montarPayloadAtividade();
var operacao = this.atividadeEditandoId
? this.orionService.updateAtividadeOrdemServico(this.atividadeEditandoId, payload)
: this.orionService.insertAtividadeOrdemServico(this.ordemServicoId, payload);
operacao
.then(() => {
this.sucesso = this.atividadeEditandoId
? 'Atividade atualizada com sucesso.'
: 'Atividade adicionada com sucesso.';
this.cancelarEdicaoAtividade();
return this.recarregarOrdem();
})
.catch(error => {
this.erro = this.obterMensagemErro(error, 'Não foi possível salvar a atividade.');
})
.then(() => {
this.salvandoAtividade = false;
});
}
excluirAtividade(atividade: OrdemServicoAtividadeModel) {
if (!window.confirm('Deseja realmente excluir esta atividade?')) {
return;
}
this.orionService.deleteAtividadeOrdemServico(atividade.id)
.then(() => {
this.sucesso = 'Atividade excluída com sucesso.';
return this.recarregarOrdem();
})
.catch(error => {
this.erro = this.obterMensagemErro(error, 'Não foi possível excluir a atividade.');
});
}
private montarPayloadAtividade(): OrdemServicoAtividadeSalvarModel {
var valor = this.formAtividade.getRawValue();
var tecnicosAtividade: OrdemServicoAtividadeTecnicoModel[] = [];
var materiaisAtividade: OrdemServicoAtividadeMaterialModel[] = [];
(valor.tecnico_ids || []).forEach(tecnicoId => {
tecnicosAtividade.push({ tecnico_id: this.numero(tecnicoId), papel: 'EXECUTOR' });
});
(valor.materiais || []).forEach(material => {
materiaisAtividade.push({
material_id: this.numeroOuNull(material.material_id),
descricao_snapshot: material.descricao_snapshot || 'Material não informado',
unidade_snapshot: material.unidade_snapshot || 'UN',
quantidade: this.numero(material.quantidade),
valor_custo_unitario: this.numero(material.valor_custo_unitario),
valor_cobrado_unitario: this.numero(material.valor_cobrado_unitario),
cobravel: !!material.cobravel,
observacoes: this.textoOuNull(material.observacoes)
});
});
return {
data_atividade: valor.data_atividade,
descricao: valor.descricao,
ordem_exibicao: this.numero(valor.ordem_exibicao),
horas_expediente: this.numero(valor.horas_expediente),
horas_extras: this.numero(valor.horas_extras),
valor_hora_expediente: this.numero(valor.valor_hora_expediente),
valor_hora_extra: this.numero(valor.valor_hora_extra),
cobravel: !!valor.cobravel,
observacoes_internas: this.textoOuNull(valor.observacoes_internas),
observacoes_cliente: this.textoOuNull(valor.observacoes_cliente),
tecnicos: tecnicosAtividade,
materiais: materiaisAtividade
};
}
get materiaisAtividade(): FormArray {
return this.formAtividade.get('materiais') as FormArray;
}
adicionarMaterialAtividade(materialExistente?: OrdemServicoAtividadeMaterialModel) {
this.materiaisAtividade.push(this.formBuilder.group({
material_id: [materialExistente ? materialExistente.material_id || null : null],
descricao_snapshot: [materialExistente ? materialExistente.descricao_snapshot || '' : '', Validators.required],
unidade_snapshot: [materialExistente ? materialExistente.unidade_snapshot || 'UN' : 'UN'],
quantidade: [materialExistente ? this.numero(materialExistente.quantidade) : 1, [Validators.required, Validators.min(0.001)]],
valor_custo_unitario: [materialExistente ? this.numero(materialExistente.valor_custo_unitario) : 0, [Validators.min(0)]],
valor_cobrado_unitario: [materialExistente ? this.numero(materialExistente.valor_cobrado_unitario) : 0, [Validators.min(0)]],
cobravel: [materialExistente ? !!materialExistente.cobravel : true],
observacoes: [materialExistente ? materialExistente.observacoes || '' : '']
}));
}
removerMaterialAtividade(index: number) {
this.materiaisAtividade.removeAt(index);
}
materialCatalogoSelecionado(index: number, materialId: number) {
var material = this.buscarMaterial(materialId);
var grupo = this.materiaisAtividade.at(index) as FormGroup;
if (!material || !grupo) {
return;
}
grupo.patchValue({
descricao_snapshot: material.descricao,
unidade_snapshot: material.unidade || 'UN',
valor_custo_unitario: this.numero(material.custo_padrao),
valor_cobrado_unitario: this.numero(material.preco_venda_padrao)
});
}
registrarPagamento() {
this.limparMensagens();
if (this.formPagamento.invalid) {
this.marcarFormularioComoTocado(this.formPagamento);
this.erro = 'Informe a data, o valor e a forma de pagamento.';
return;
}
this.salvandoPagamento = true;
var valor = this.formPagamento.getRawValue();
var payload: OrdemServicoPagamentoModel = {
data_pagamento: this.normalizarDataHoraApi(valor.data_pagamento),
valor: this.numero(valor.valor),
forma_pagamento: valor.forma_pagamento,
referencia: this.textoOuNull(valor.referencia),
observacoes: this.textoOuNull(valor.observacoes)
};
this.orionService.insertPagamentoOrdemServico(this.ordemServicoId, payload)
.then(() => {
this.sucesso = 'Pagamento registrado com sucesso.';
this.exibirFormPagamento = false;
this.resetarFormPagamento();
return this.recarregarOrdem();
})
.catch(error => {
this.erro = this.obterMensagemErro(error, 'Não foi possível registrar o pagamento.');
})
.then(() => {
this.salvandoPagamento = false;
});
}
cancelarPagamento(pagamento: OrdemServicoPagamentoModel) {
if (!pagamento.id || !window.confirm('Deseja realmente cancelar este pagamento?')) {
return;
}
this.orionService.cancelPagamentoOrdemServico(pagamento.id)
.then(() => this.recarregarOrdem())
.catch(error => {
this.erro = this.obterMensagemErro(error, 'Não foi possível cancelar o pagamento.');
});
}
registrarOdometro() {
this.limparMensagens();
if (this.formOdometro.invalid) {
this.marcarFormularioComoTocado(this.formOdometro);
this.erro = 'Confira os campos da leitura de odômetro.';
return;
}
this.salvandoOdometro = true;
var valor = this.formOdometro.getRawValue();
var payload: OrdemServicoOdometroModel = {
tipo_leitura: valor.tipo_leitura,
leitura_em: this.normalizarDataHoraApi(valor.leitura_em),
odometro_total_segundos: this.numero(valor.odometro_total_segundos),
odometro_conectado_segundos: this.numero(valor.odometro_conectado_segundos),
odometro_movimento_segundos: this.numero(valor.odometro_movimento_segundos),
odometro_parcial_segundos: this.numero(valor.odometro_parcial_segundos),
observacoes: this.textoOuNull(valor.observacoes)
};
this.orionService.insertOdometroOrdemServico(this.ordemServicoId, payload)
.then(() => {
this.sucesso = 'Leitura registrada com sucesso.';
this.exibirFormOdometro = false;
return this.recarregarOrdem();
})
.catch(error => {
this.erro = this.obterMensagemErro(error, 'Não foi possível registrar a leitura.');
})
.then(() => {
this.salvandoOdometro = false;
});
}
get atividadesOrdenadas(): OrdemServicoAtividadeModel[] {
if (!this.ordem || !this.ordem.atividades) {
return [];
}
return this.ordem.atividades.slice().sort((a, b) => {
var dataA = new Date(a.data_atividade).getTime();
var dataB = new Date(b.data_atividade).getTime();
return dataA === dataB
? this.numero(a.ordem_exibicao) - this.numero(b.ordem_exibicao)
: dataB - dataA;
});
}
totalHorasAtividade(atividade: OrdemServicoAtividadeModel): number {
return this.numero(atividade.horas_expediente) + this.numero(atividade.horas_extras);
}
valorMateriaisAtividade(atividade: OrdemServicoAtividadeModel): number {
return (atividade.materiais || []).reduce((total, material) => {
return total + this.numero(material.valor_cobrado_total);
}, 0);
}
nomeTecnicosAtividade(atividade: OrdemServicoAtividadeModel): string {
var nomes = (atividade.tecnicos || []).map(tecnico => {
return tecnico.tecnico_nome || ('Técnico #' + tecnico.tecnico_id);
});
return nomes.length > 0 ? nomes.join(', ') : 'Não informado';
}
get valorPendente(): number {
if (!this.ordem) {
return 0;
}
return Math.max(0, this.numero(this.ordem.valor_total) - this.numero(this.ordem.valor_pago));
}
labelStatus(status: OrdemServicoStatus): string {
return this.buscarTexto(this.statusOpcoes, status);
}
labelTipo(tipo: OrdemServicoTipo): string {
return this.buscarTexto(this.tipoOpcoes, tipo);
}
labelPagamento(status: OrdemServicoStatusPagamento): string {
return this.buscarTexto([
{ valor: 'NAO_APLICAVEL', texto: 'Não aplicável' },
{ valor: 'PENDENTE', texto: 'Pendente' },
{ valor: 'PARCIAL', texto: 'Parcial' },
{ valor: 'RECEBIDO', texto: 'Recebido' },
{ valor: 'CORTESIA', texto: 'Cortesia' }
], status);
}
classeStatus(status: OrdemServicoStatus): string {
if (status === 'RETIRADA' || status === 'FINALIZADA') return 'badge-sucesso';
if (status === 'AGUARDANDO_RETIRADA' || status === 'AGUARDANDO_PECA' || status === 'AGUARDANDO_APROVACAO') return 'badge-alerta';
if (status === 'CANCELADA') return 'badge-erro';
if (status === 'RASCUNHO') return 'badge-neutro';
return 'badge-info';
}
classePagamento(status: OrdemServicoStatusPagamento): string {
if (status === 'RECEBIDO') return 'badge-sucesso';
if (status === 'PENDENTE') return 'badge-erro';
if (status === 'PARCIAL') return 'badge-alerta';
return 'badge-neutro';
}
segundosParaTempo(segundos: number): string {
var total = Math.max(0, Math.floor(this.numero(segundos)));
var horas = Math.floor(total / 3600);
var minutos = Math.floor((total % 3600) / 60);
var segundosRestantes = total % 60;
return this.preencherZero(horas) + ':' + this.preencherZero(minutos) + ':' + this.preencherZero(segundosRestantes);
}
voltar() {
this.router.navigate(['/orion-ordens-servico']);
}
private recarregarOrdem(): Promise<any> {
return this.modoEdicao && this.ordemServicoId ? this.carregarOrdem() : Promise.resolve();
}
private preencherNovoOdometroComUltimaLeitura() {
if (!this.ordem || !this.ordem.odometros_da_os || this.ordem.odometros_da_os.length === 0) {
return;
}
var leituras = this.ordem.odometros_da_os.slice().sort((a, b) => {
return new Date(b.leitura_em).getTime() - new Date(a.leitura_em).getTime();
});
var ultima = leituras[0];
this.formOdometro.patchValue({
odometro_total_segundos: this.numero(ultima.odometro_total_segundos),
odometro_conectado_segundos: this.numero(ultima.odometro_conectado_segundos),
odometro_movimento_segundos: this.numero(ultima.odometro_movimento_segundos),
odometro_parcial_segundos: this.numero(ultima.odometro_parcial_segundos),
leitura_em: this.formatarDataHoraLocal(new Date())
});
}
private resetarFormAtividade() {
this.formAtividade.reset({
data_atividade: this.formatarData(new Date()),
descricao: '',
ordem_exibicao: 0,
horas_expediente: 0,
horas_extras: 0,
valor_hora_expediente: 60,
valor_hora_extra: 90,
cobravel: true,
observacoes_internas: '',
observacoes_cliente: '',
tecnico_ids: []
});
this.limparMateriaisAtividade();
}
private resetarFormPagamento() {
this.formPagamento.reset({
data_pagamento: this.formatarDataHoraLocal(new Date()),
valor: 0,
forma_pagamento: 'PIX',
referencia: '',
observacoes: ''
});
}
private limparMateriaisAtividade() {
while (this.materiaisAtividade.length > 0) {
this.materiaisAtividade.removeAt(0);
}
}
private buscarEquipamento(equipamentoId: number): EquipamentoManutencaoModel {
var id = this.numero(equipamentoId);
for (var i = 0; i < this.equipamentos.length; i++) {
if (this.numero(this.equipamentos[i].id) === id) return this.equipamentos[i];
}
return null;
}
private buscarMaterial(materialId: number): MaterialManutencaoModel {
var id = this.numero(materialId);
for (var i = 0; i < this.materiais.length; i++) {
if (this.numero(this.materiais[i].id) === id) return this.materiais[i];
}
return null;
}
private buscarTexto(opcoes: any[], valor: string): string {
for (var i = 0; i < opcoes.length; i++) {
if (opcoes[i].valor === valor) return opcoes[i].texto;
}
return valor || '';
}
private marcarFormularioComoTocado(controle: AbstractControl) {
if (controle instanceof FormGroup) {
Object.keys(controle.controls).forEach(chave => this.marcarFormularioComoTocado(controle.controls[chave]));
} else if (controle instanceof FormArray) {
controle.controls.forEach(item => this.marcarFormularioComoTocado(item));
} else {
controle.markAsTouched();
}
}
private formatarData(data: Date): string {
return data.getFullYear() + '-' + this.preencherZero(data.getMonth() + 1) + '-' + this.preencherZero(data.getDate());
}
private formatarDataHoraLocal(data: Date): string {
return this.formatarData(data) + 'T' + this.preencherZero(data.getHours()) + ':' + this.preencherZero(data.getMinutes());
}
private converterDataApiParaLocal(valor: string): string {
if (!valor) return '';
var texto = String(valor).replace(' ', 'T');
return texto.length >= 16 ? texto.substring(0, 16) : texto;
}
private normalizarDataHoraApi(valor: string): string {
if (!valor) return null;
var texto = String(valor).replace('T', ' ');
return texto.length === 16 ? texto + ':00' : texto;
}
private textoOuNull(valor: any): string {
if (valor === null || valor === undefined) return null;
var texto = String(valor).trim();
return texto ? texto : null;
}
private numero(valor: any): number {
var convertido = Number(valor);
return isNaN(convertido) ? 0 : convertido;
}
private numeroOuNull(valor: any): number {
if (valor === null || valor === undefined || valor === '') return null;
var convertido = Number(valor);
return isNaN(convertido) ? null : convertido;
}
private preencherZero(valor: number): string {
return valor < 10 ? '0' + valor : String(valor);
}
private limparMensagens() {
this.erro = '';
this.sucesso = '';
}
private obterMensagemErro(error: any, fallback: string): string {
return error && error.message ? error.message : fallback;
}
baixarPdfCliente() {
if (!this.modoEdicao || !this.ordemServicoId || this.baixandoPdf) {
return;
}
this.baixandoPdf = true;
this.erro = '';
this.orionService
.downloadOrdemServicoPdf(this.ordemServicoId, false)
.then(blob => {
var url = window.URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'OS_' + this.ordemServicoId + '.pdf';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 1000);
})
.catch(error => {
console.error('[OS FORM] Erro ao gerar PDF:', error);
this.erro =
'Não foi possível gerar o PDF da ordem de serviço.';
})
.then(() => {
this.baixandoPdf = false;
});
}
}

View File

@ -0,0 +1,387 @@
<div class="os-page">
<div class="page-header">
<div>
<span class="eyebrow">ORIONTARD</span>
<h1>Ordens de Serviço</h1>
<p>Histórico de manutenções, horas trabalhadas e valores dos equipamentos.</p>
</div>
<div class="header-actions">
<button
mat-stroked-button
type="button"
(click)="atualizar()"
[disabled]="carregando">
Atualizar
</button>
<button
mat-raised-button
color="primary"
type="button"
(click)="novaOrdem()">
Nova OS
</button>
<button
mat-stroked-button
type="button"
(click)="baixarRelatorioPdf()"
[disabled]="carregando || baixandoRelatorio">
{{ baixandoRelatorio ? 'Gerando relatório...' : 'Relatório PDF' }}
</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="OS, equipamento, NS ou problema">
</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>Pagamento</mat-label>
<mat-select [(ngModel)]="pagamentoSelecionado">
<mat-option
*ngFor="let opcao of pagamentoOpcoes"
[value]="opcao.valor">
{{ opcao.texto }}
</mat-option>
</mat-select>
</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>Entrada 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>Entrada 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>
</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">OS encontradas</span>
<strong>{{ totalRegistros }}</strong>
<small>{{ dataSource.data.length }} nesta página</small>
</article>
<article class="summary-card">
<span class="summary-label">Horas na página</span>
<strong>{{ totalHorasPagina | number:'1.2-2' }}</strong>
<small>Expediente + horas extras</small>
</article>
<article class="summary-card">
<span class="summary-label">Valor na página</span>
<strong>{{ valorTotalPagina | currency:'BRL':'symbol':'1.2-2' }}</strong>
<small>Soma das OS exibidas</small>
</article>
<article class="summary-card">
<span class="summary-label">Pagamentos abertos</span>
<strong>{{ pendentesNaPagina }}</strong>
<small>Pendentes ou parciais na página</small>
</article>
</section>
<section class="table-card">
<div class="loading-state" *ngIf="carregando">
<div class="loading-bar"></div>
<span>Carregando ordens de serviço...</span>
</div>
<div class="error-state" *ngIf="!carregando && erro">
<strong>Não foi possível carregar as OS.</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>Nenhuma ordem de serviço encontrada.</strong>
<p>Altere os filtros ou confira se a importação foi concluída.</p>
</div>
<div
class="table-scroll"
*ngIf="!carregando && !erro && dataSource.data.length > 0">
<table
mat-table
[dataSource]="dataSource"
class="os-table">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef>OS</th>
<td mat-cell *matCellDef="let ordem">
<span class="os-number">#{{ ordem.id }}</span>
</td>
</ng-container>
<ng-container matColumnDef="equipamento">
<th mat-header-cell *matHeaderCellDef>Equipamento</th>
<td mat-cell *matCellDef="let ordem">
<div class="equipment-cell">
<strong>{{ nomeEquipamento(ordem) }}</strong>
<span *ngIf="ordem.numero_serie_snapshot">
NS {{ ordem.numero_serie_snapshot }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="data_entrada">
<th mat-header-cell *matHeaderCellDef>Entrada</th>
<td mat-cell *matCellDef="let ordem">
<div class="date-cell">
<strong>{{ ordem.data_entrada | date:'dd/MM/yyyy' }}</strong>
<span *ngIf="ordem.data_finalizacao">
Finalizada em {{ ordem.data_finalizacao | date:'dd/MM/yyyy' }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="problema_relatado">
<th mat-header-cell *matHeaderCellDef>Problema relatado</th>
<td mat-cell *matCellDef="let ordem">
<span
class="problem-text"
[title]="ordem.problema_relatado || ''">
{{ ordem.problema_relatado || 'Sem descrição' }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="tipo">
<th mat-header-cell *matHeaderCellDef>Tipo</th>
<td mat-cell *matCellDef="let ordem">
{{ labelTipo(ordem.tipo) }}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let ordem">
<span
class="status-badge"
[ngClass]="classeStatus(ordem.status)">
{{ labelStatus(ordem.status) }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="status_pagamento">
<th mat-header-cell *matHeaderCellDef>Pagamento</th>
<td mat-cell *matCellDef="let ordem">
<span
class="status-badge"
[ngClass]="classePagamento(ordem.status_pagamento)">
{{ labelPagamento(ordem.status_pagamento) }}
</span>
</td>
</ng-container>
<ng-container matColumnDef="horas">
<th mat-header-cell *matHeaderCellDef>Horas</th>
<td mat-cell *matCellDef="let ordem">
<div class="hours-cell">
<strong>{{ totalHoras(ordem) | number:'1.2-2' }}</strong>
<span>
{{ ordem.total_horas_expediente | number:'1.2-2' }} exp.
·
{{ ordem.total_horas_extras | number:'1.2-2' }} extra
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="valor_total">
<th mat-header-cell *matHeaderCellDef>Total</th>
<td mat-cell *matCellDef="let ordem">
<div class="money-cell">
<strong>
{{ ordem.valor_total | currency:'BRL':'symbol':'1.2-2' }}
</strong>
<span *ngIf="ordem.valor_pendente > 0">
Pendente:
{{ ordem.valor_pendente | currency:'BRL':'symbol':'1.2-2' }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="acoes">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let ordem">
<div class="row-actions">
<button
mat-stroked-button
type="button"
(click)="abrirOrdem(ordem)">
Detalhes
</button>
<button
mat-button
type="button"
(click)="baixarPdf(ordem)"
[disabled]="baixandoPdfId === ordem.id">
{{ baixandoPdfId === ordem.id ? 'Gerando...' : 'PDF' }}
</button>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: displayedColumns;"
[class.selected-row]="ordemSelecionada && ordemSelecionada.id === row.id"
(dblclick)="selecionarOrdem(row)">
</tr>
</table>
</div>
<mat-paginator
*ngIf="!erro && totalRegistros > 0"
[length]="totalRegistros"
[pageIndex]="paginaAtual - 1"
[pageSize]="tamanhoPagina"
[pageSizeOptions]="opcoesTamanhoPagina"
[disabled]="carregando"
showFirstLastButtons
(page)="onPageChange($event)">
</mat-paginator>
</section>
<section class="quick-view" *ngIf="ordemSelecionada">
<div class="quick-view-header">
<div>
<span class="eyebrow">VISUALIZAÇÃO RÁPIDA</span>
<h2>OS #{{ ordemSelecionada.id }}</h2>
</div>
<button
mat-button
type="button"
(click)="ordemSelecionada = null">
Fechar
</button>
</div>
<div class="quick-view-grid">
<div>
<span>Equipamento</span>
<strong>{{ nomeEquipamento(ordemSelecionada) }}</strong>
<small *ngIf="ordemSelecionada.numero_serie_snapshot">
NS {{ ordemSelecionada.numero_serie_snapshot }}
</small>
</div>
<div>
<span>Entrada</span>
<strong>{{ ordemSelecionada.data_entrada | date:'dd/MM/yyyy' }}</strong>
<small>
{{ labelTipo(ordemSelecionada.tipo) }}
</small>
</div>
<div>
<span>Horas</span>
<strong>{{ totalHoras(ordemSelecionada) | number:'1.2-2' }}</strong>
<small>
{{ ordemSelecionada.total_horas_extras | number:'1.2-2' }}
hora(s) extra(s)
</small>
</div>
<div>
<span>Valor total</span>
<strong>
{{ ordemSelecionada.valor_total | currency:'BRL':'symbol':'1.2-2' }}
</strong>
<small>
Pago:
{{ ordemSelecionada.valor_pago | currency:'BRL':'symbol':'1.2-2' }}
</small>
</div>
</div>
<div class="quick-view-problem">
<span>Problema relatado</span>
<p>{{ ordemSelecionada.problema_relatado || 'Sem descrição' }}</p>
</div>
</section>
</div>

View File

@ -0,0 +1,630 @@
:host {
display: block;
min-height: 100%;
background: #f4f6f8;
color: #1f2933;
}
.os-page {
padding: 24px;
max-width: 1680px;
margin: 0 auto;
box-sizing: border-box;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 20px;
h1 {
margin: 4px 0 6px;
font-size: 30px;
font-weight: 700;
letter-spacing: -0.5px;
}
p {
margin: 0;
color: #667085;
font-size: 14px;
}
}
.eyebrow {
display: inline-block;
color: #1769aa;
font-size: 11px;
font-weight: 700;
letter-spacing: 1.2px;
}
.header-actions,
.filters-actions {
display: flex;
align-items: center;
gap: 10px;
}
.filters-card,
.table-card,
.quick-view {
background: #ffffff;
border: 1px solid #e4e7ec;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(16, 24, 40, 0.04);
}
.filters-card {
padding: 18px;
margin-bottom: 16px;
}
.filters-grid {
display: grid;
grid-template-columns:
minmax(260px, 2fr)
repeat(3, minmax(150px, 1fr))
repeat(2, minmax(150px, 1fr));
gap: 12px;
align-items: start;
}
.filters-grid .mat-form-field {
width: 100%;
}
.filters-actions {
justify-content: flex-end;
margin-top: 4px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: 14px;
margin-bottom: 16px;
}
.summary-card {
background: #ffffff;
border: 1px solid #e4e7ec;
border-radius: 12px;
padding: 16px 18px;
box-shadow: 0 2px 8px rgba(16, 24, 40, 0.03);
strong {
display: block;
margin-top: 6px;
font-size: 24px;
line-height: 1.2;
color: #101828;
}
small {
display: block;
margin-top: 5px;
color: #98a2b3;
font-size: 12px;
}
}
.summary-label {
color: #667085;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.table-card {
overflow: hidden;
}
.table-scroll {
width: 100%;
overflow-x: auto;
}
.os-table {
width: 100%;
min-width: 1380px;
border-collapse: collapse;
th.mat-header-cell {
background: #f8fafc;
color: #475467;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.45px;
white-space: nowrap;
}
td.mat-cell,
th.mat-header-cell {
padding: 12px 14px;
border-bottom-color: #edf0f3;
}
tr.mat-row {
transition: background 120ms ease;
}
tr.mat-row:hover {
background: #f8fbff;
}
tr.selected-row {
background: #eef6ff;
}
}
.os-number {
color: #1769aa;
font-weight: 700;
}
.equipment-cell,
.date-cell,
.hours-cell,
.money-cell {
display: flex;
flex-direction: column;
gap: 3px;
strong {
color: #344054;
font-size: 13px;
}
span {
color: #98a2b3;
font-size: 11px;
}
}
.problem-text {
display: block;
max-width: 340px;
color: #475467;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-badge {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
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;
}
.loading-state,
.error-state,
.empty-state {
min-height: 220px;
padding: 36px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
strong {
color: #344054;
font-size: 16px;
}
p,
span {
color: #667085;
font-size: 13px;
}
}
.loading-bar {
width: 160px;
height: 4px;
margin-bottom: 16px;
border-radius: 999px;
overflow: hidden;
background:
linear-gradient(
90deg,
#d0d5dd 0%,
#1769aa 45%,
#d0d5dd 100%
);
background-size: 200% 100%;
animation: loading 1.2s linear infinite;
}
@keyframes loading {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.quick-view {
margin-top: 16px;
padding: 20px;
}
.quick-view-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
h2 {
margin: 4px 0 0;
font-size: 22px;
}
}
.quick-view-grid {
display: grid;
grid-template-columns: repeat(4, minmax(160px, 1fr));
gap: 14px;
margin-top: 18px;
> div {
padding: 14px;
background: #f8fafc;
border-radius: 9px;
}
span,
small {
display: block;
color: #667085;
font-size: 11px;
}
strong {
display: block;
margin: 5px 0;
color: #344054;
font-size: 14px;
}
}
.quick-view-problem {
margin-top: 14px;
padding: 14px;
border: 1px solid #eaecf0;
border-radius: 9px;
span {
color: #667085;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
p {
margin: 7px 0 0;
color: #344054;
line-height: 1.55;
}
}
mat-paginator {
border-top: 1px solid #edf0f3;
}
@media (max-width: 1250px) {
.filters-grid {
grid-template-columns: repeat(3, minmax(180px, 1fr));
}
.summary-grid,
.quick-view-grid {
grid-template-columns: repeat(2, minmax(180px, 1fr));
}
}
@media (max-width: 760px) {
.os-page {
padding: 14px;
}
.page-header {
flex-direction: column;
}
.header-actions {
width: 100%;
button {
flex: 1;
}
}
.filters-grid,
.summary-grid,
.quick-view-grid {
grid-template-columns: 1fr;
}
.filters-actions {
button {
flex: 1;
}
}
}
// ============================================================
// ISOLAMENTO CONTRA ESTILOS GLOBAIS E TEMA ESCURO DO MATERIAL
// ============================================================
:host {
display: block;
min-height: 100vh;
background: #f4f6f8;
color: #1f2933;
}
/* ------------------------------------------------------------
CAMPOS DOS FILTROS
------------------------------------------------------------ */
: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;
}
: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 .mat-datepicker-toggle {
color: #667085 !important;
}
:host ::ng-deep .filters-card input::placeholder {
color: #98a2b3 !important;
opacity: 1;
}
/* Remove qualquer fundo escuro herdado */
:host ::ng-deep .filters-card .mat-form-field-flex {
background: transparent !important;
}
/* ------------------------------------------------------------
TABELA
------------------------------------------------------------ */
:host ::ng-deep .os-table,
:host ::ng-deep table.os-table {
background: #ffffff !important;
color: #344054 !important;
}
:host ::ng-deep .os-table tr.mat-header-row {
background: #f8fafc !important;
}
:host ::ng-deep .os-table tr.mat-row {
background: #ffffff !important;
color: #344054 !important;
}
/*
* Sobrescreve o tr:nth-child(even) global.
*/
:host ::ng-deep .os-table tr.mat-row:nth-child(even) {
background: #fbfcfd !important;
}
:host ::ng-deep .os-table tr.mat-row:hover {
background: #f2f7fc !important;
}
:host ::ng-deep .os-table tr.mat-row.selected-row {
background: #eaf4ff !important;
}
:host ::ng-deep .os-table th.mat-header-cell {
background: #f8fafc !important;
color: #475467 !important;
border-color: #eaecf0 !important;
}
:host ::ng-deep .os-table td.mat-cell {
background: transparent !important;
color: #344054 !important;
border-color: #eaecf0 !important;
}
:host ::ng-deep .os-table td,
:host ::ng-deep .os-table th {
color: #344054 !important;
border-left: 0 !important;
border-right: 0 !important;
}
/* Textos internos das células */
:host ::ng-deep .os-table .equipment-cell strong,
:host ::ng-deep .os-table .date-cell strong,
:host ::ng-deep .os-table .hours-cell strong,
:host ::ng-deep .os-table .money-cell strong {
color: #344054 !important;
}
:host ::ng-deep .os-table .equipment-cell span,
:host ::ng-deep .os-table .date-cell span,
:host ::ng-deep .os-table .hours-cell span,
:host ::ng-deep .os-table .money-cell span {
color: #667085 !important;
}
:host ::ng-deep .os-table .problem-text {
color: #475467 !important;
}
:host ::ng-deep .os-table .os-number {
color: #1769aa !important;
}
/* ------------------------------------------------------------
BOTÕES
------------------------------------------------------------ */
:host ::ng-deep .header-actions .mat-stroked-button,
:host ::ng-deep .filters-actions .mat-button,
:host ::ng-deep .os-table .mat-stroked-button,
:host ::ng-deep .quick-view .mat-button {
color: #344054 !important;
}
:host ::ng-deep .header-actions .mat-stroked-button,
:host ::ng-deep .os-table .mat-stroked-button {
border-color: #d0d5dd !important;
background: #ffffff !important;
}
:host ::ng-deep .header-actions .mat-raised-button[disabled] {
background: #e4e7ec !important;
color: #98a2b3 !important;
opacity: 1 !important;
}
:host ::ng-deep .filters-actions .mat-raised-button {
color: #ffffff !important;
}
/* ------------------------------------------------------------
PAGINADOR
------------------------------------------------------------ */
: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 {
color: #667085 !important;
}
:host ::ng-deep .mat-paginator .mat-icon-button {
color: #475467 !important;
}
:host ::ng-deep .mat-paginator .mat-icon-button[disabled] {
color: #d0d5dd !important;
}
/* ------------------------------------------------------------
BADGES
------------------------------------------------------------ */
: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;
}
.row-actions {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OrionOrdensServicoComponent } from './orion-ordens-servico.component';
describe('OrionOrdensServicoComponent', () => {
let component: OrionOrdensServicoComponent;
let fixture: ComponentFixture<OrionOrdensServicoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ OrionOrdensServicoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrionOrdensServicoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,429 @@
import { Component, OnInit } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import {
OrdemServicoFiltrosModel,
OrdemServicoListaModel,
OrdemServicoStatus,
OrdemServicoStatusPagamento,
OrdemServicoTipo
} from '../../models/orionModel';
import { OrionService } from '../../services/orionService';
@Component({
selector: 'app-orion-ordens-servico',
templateUrl: './orion-ordens-servico.component.html',
styleUrls: ['./orion-ordens-servico.component.scss']
})
export class OrionOrdensServicoComponent implements OnInit {
displayedColumns: string[] = [
'id',
'equipamento',
'data_entrada',
'problema_relatado',
'tipo',
'status',
'status_pagamento',
'horas',
'valor_total',
'acoes'
];
dataSource = new MatTableDataSource<OrdemServicoListaModel>([]);
ordemSelecionada: OrdemServicoListaModel = null;
carregando: boolean = false;
erro: string = '';
paginaAtual: number = 1;
tamanhoPagina: number = 25;
totalRegistros: number = 0;
totalPaginas: number = 0;
opcoesTamanhoPagina: number[] = [10, 25, 50, 100];
busca: string = '';
statusSelecionado: OrdemServicoStatus | '' = '';
pagamentoSelecionado: OrdemServicoStatusPagamento | '' = '';
tipoSelecionado: OrdemServicoTipo | '' = '';
dataInicio: Date = null;
dataFim: Date = null;
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: 'RETIRADA', texto: 'Retirada' },
{ valor: 'CANCELADA', texto: 'Cancelada' }
];
pagamentoOpcoes = [
{ valor: '', texto: 'Todos os pagamentos' },
{ valor: 'NAO_APLICAVEL', texto: 'Não aplicável' },
{ valor: 'PENDENTE', texto: 'Pendente' },
{ valor: 'PARCIAL', texto: 'Parcial' },
{ valor: 'RECEBIDO', texto: 'Recebido' },
{ valor: 'CORTESIA', texto: 'Cortesia' }
];
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' }
];
baixandoPdfId: number = null;
baixandoRelatorio: boolean = false;
constructor(
private orionService: OrionService,
private router: Router
) { }
ngOnInit() {
this.carregarOrdens();
}
carregarOrdens() {
this.carregando = true;
this.erro = '';
var filtros: OrdemServicoFiltrosModel = {
page: this.paginaAtual,
pageSize: this.tamanhoPagina,
busca: this.normalizarTexto(this.busca),
status: this.statusSelecionado,
status_pagamento: this.pagamentoSelecionado,
tipo: this.tipoSelecionado,
data_inicio: this.formatarDataApi(this.dataInicio),
data_fim: this.formatarDataApi(this.dataFim)
};
this.orionService
.getOrdensServico(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.ordemSelecionada &&
!this.dataSource.data.some(
ordem => ordem.id === this.ordemSelecionada.id
)
) {
this.ordemSelecionada = null;
}
})
.catch(error => {
console.error('[OS] Erro ao carregar ordens:', error);
this.dataSource.data = [];
this.totalRegistros = 0;
this.totalPaginas = 0;
this.erro = error && error.message
? error.message
: 'Não foi possível carregar as ordens de serviço.';
})
.then(() => {
this.carregando = false;
});
}
novaOrdem() {
this.router.navigate([
'/orion-ordens-servico',
'nova'
]);
}
abrirOrdem(ordem: OrdemServicoListaModel) {
this.router.navigate([
'/orion-ordens-servico',
ordem.id,
'editar'
]);
}
aplicarFiltros() {
this.paginaAtual = 1;
this.ordemSelecionada = null;
this.carregarOrdens();
}
limparFiltros() {
this.busca = '';
this.statusSelecionado = '';
this.pagamentoSelecionado = '';
this.tipoSelecionado = '';
this.dataInicio = null;
this.dataFim = null;
this.paginaAtual = 1;
this.ordemSelecionada = null;
this.carregarOrdens();
}
atualizar() {
this.carregarOrdens();
}
onPageChange(event: PageEvent) {
this.paginaAtual = event.pageIndex + 1;
this.tamanhoPagina = event.pageSize;
this.ordemSelecionada = null;
this.carregarOrdens();
}
selecionarOrdem(ordem: OrdemServicoListaModel) {
this.ordemSelecionada = ordem;
}
trackById(index: number, ordem: OrdemServicoListaModel): number {
return ordem.id;
}
get totalHorasPagina(): number {
return this.dataSource.data.reduce(
(total, ordem) =>
total +
this.numero(ordem.total_horas_expediente) +
this.numero(ordem.total_horas_extras),
0
);
}
get valorTotalPagina(): number {
return this.dataSource.data.reduce(
(total, ordem) => total + this.numero(ordem.valor_total),
0
);
}
get pendentesNaPagina(): number {
return this.dataSource.data.filter(
ordem =>
ordem.status_pagamento === 'PENDENTE' ||
ordem.status_pagamento === 'PARCIAL'
).length;
}
totalHoras(ordem: OrdemServicoListaModel): number {
return (
this.numero(ordem.total_horas_expediente) +
this.numero(ordem.total_horas_extras)
);
}
nomeEquipamento(ordem: OrdemServicoListaModel): string {
return (
ordem.equipamento_nome_snapshot ||
ordem.modelo_snapshot ||
'Equipamento não informado'
);
}
labelStatus(status: OrdemServicoStatus): string {
return this.buscarTextoOpcao(this.statusOpcoes, status);
}
labelPagamento(status: OrdemServicoStatusPagamento): string {
return this.buscarTextoOpcao(this.pagamentoOpcoes, status);
}
labelTipo(tipo: OrdemServicoTipo): string {
return this.buscarTextoOpcao(this.tipoOpcoes, 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':
return 'badge-alerta';
case 'CANCELADA':
return 'badge-erro';
case 'RASCUNHO':
return 'badge-neutro';
default:
return 'badge-info';
}
}
classePagamento(status: OrdemServicoStatusPagamento): string {
switch (status) {
case 'RECEBIDO':
return 'badge-sucesso';
case 'PARCIAL':
return 'badge-alerta';
case 'PENDENTE':
return 'badge-erro';
case 'CORTESIA':
return 'badge-info';
default:
return 'badge-neutro';
}
}
private buscarTextoOpcao(opcoes: any[], valor: string): string {
for (var i = 0; i < opcoes.length; i++) {
if (opcoes[i].valor === valor) {
return opcoes[i].texto;
}
}
return valor;
}
private normalizarTexto(valor: string): string {
return valor ? valor.trim() : '';
}
private formatarDataApi(data: Date): string {
if (!data) {
return '';
}
var ano = data.getFullYear();
var mes = data.getMonth() + 1;
var dia = data.getDate();
return (
ano +
'-' +
(mes < 10 ? '0' : '') + mes +
'-' +
(dia < 10 ? '0' : '') + dia
);
}
private numero(valor: any): number {
var convertido = Number(valor);
return isNaN(convertido) ? 0 : convertido;
}
baixarPdf(ordem: OrdemServicoListaModel) {
if (!ordem || this.baixandoPdfId !== null) {
return;
}
this.baixandoPdfId = ordem.id;
this.orionService
.downloadOrdemServicoPdf(ordem.id, false)
.then(blob => {
this.salvarBlob(
blob,
'OS_' +
ordem.id +
'_' +
this.nomeEquipamento(ordem).replace(/[^a-zA-Z0-9]+/g, '_') +
'.pdf'
);
})
.catch(error => {
console.error('[OS] Erro ao baixar PDF:', error);
this.erro =
'Não foi possível gerar o PDF da ordem de serviço.';
})
.then(() => {
this.baixandoPdfId = null;
});
}
baixarRelatorioPdf() {
if (this.baixandoRelatorio) {
return;
}
this.baixandoRelatorio = true;
this.erro = '';
var filtros: any = {
busca: this.normalizarTexto(this.busca),
status: this.statusSelecionado,
status_pagamento: this.pagamentoSelecionado,
tipo: this.tipoSelecionado,
data_inicio: this.formatarDataApi(this.dataInicio),
data_fim: this.formatarDataApi(this.dataFim),
// Para fechamento mensal, a data de finalização é o critério mais seguro.
criterio: 'data_finalizacao'
};
this.orionService
.downloadRelatorioOrdensServicoPdf(filtros)
.then(blob => {
var inicio = filtros.data_inicio || 'inicio';
var fim = filtros.data_fim || 'fim';
this.salvarBlob(
blob,
'Relatorio_OS_' + inicio + '_' + fim + '.pdf'
);
})
.catch(error => {
console.error('[OS] Erro ao baixar relatório:', error);
this.erro =
'Não foi possível gerar o relatório de serviços.';
})
.then(() => {
this.baixandoRelatorio = false;
});
}
private salvarBlob(blob: Blob, nomeArquivo: string) {
var url = window.URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = nomeArquivo;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 1000);
}
}

View File

@ -1,31 +1,489 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { GeoCoordenadasResponseModel, OrionLogsModel } from '../models/orionModel';
import {
ClienteManutencaoModel,
EquipamentoManutencaoModel,
GeoCoordenadasResponseModel,
MaterialManutencaoModel,
OrionApiResponseModel,
OrionLogInsertModel,
OrionLogsModel,
OrdemServicoAtividadeSalvarModel,
OrdemServicoDetalhesModel,
OrdemServicoFiltrosModel,
OrdemServicoListaModel,
OrdemServicoListaResponseModel,
OrdemServicoOdometroModel,
OrdemServicoOperacaoResultModel,
OrdemServicoPagamentoModel,
OrdemServicoSalvarModel,
TecnicoManutencaoModel
} from '../models/orionModel';
@Injectable()
export class OrionService {
Rota: String = environment.ApiUrl + "/otp";
private readonly Rota: string = environment.ApiUrl + '/otp';
constructor(private http: HttpClient) { }
getAllLogs() {
return this.http.get<OrionLogsModel[]>(`${this.Rota}/getAllLogs`)
.toPromise()
.then(response => <OrionLogsModel[]>response)
.then(data => {
return <OrionLogsModel[]>data['response'];
})
// ==========================================================
// RESPOSTA PADRÃO
// ==========================================================
private executar<T>(
requisicao: Promise<OrionApiResponseModel<T>>
): Promise<T> {
return requisicao.then(resultado => {
if (!resultado) {
throw new Error('A API não retornou uma resposta.');
}
coordenadaToEndereco(Coordenadas: string) {
return this.http.get<GeoCoordenadasResponseModel>(`http://api.positionstack.com/v1/reverse?access_key=${environment.GeoAPI}&query=${Coordenadas}`)
if (resultado.status >= 400 || resultado.error) {
const mensagem = resultado.error && resultado.error.message
? resultado.error.message
: 'Não foi possível concluir a operação.';
throw new Error(mensagem);
}
return resultado.response;
});
}
private montarParametros(filtros?: any): HttpParams {
let params = new HttpParams();
if (!filtros) {
return params;
}
Object.keys(filtros).forEach(chave => {
const valor = filtros[chave];
if (valor !== undefined && valor !== null && valor !== '') {
params = params.set(chave, String(valor));
}
});
return params;
}
private numero(valor: any): number {
const convertido = Number(valor);
return Number.isFinite(convertido) ? convertido : 0;
}
private normalizarOrdemLista(
ordem: OrdemServicoListaModel
): OrdemServicoListaModel {
return {
...ordem,
id: this.numero(ordem.id),
cliente_id: ordem.cliente_id ? this.numero(ordem.cliente_id) : null,
equipamento_id: ordem.equipamento_id ? this.numero(ordem.equipamento_id) : null,
tecnico_responsavel_id: ordem.tecnico_responsavel_id
? this.numero(ordem.tecnico_responsavel_id)
: null,
total_horas_expediente: this.numero(ordem.total_horas_expediente),
total_horas_extras: this.numero(ordem.total_horas_extras),
valor_mao_obra: this.numero(ordem.valor_mao_obra),
valor_insumos: this.numero(ordem.valor_insumos),
valor_desconto: this.numero(ordem.valor_desconto),
valor_acrescimo: this.numero(ordem.valor_acrescimo),
valor_total: this.numero(ordem.valor_total),
valor_pago: this.numero(ordem.valor_pago),
valor_pendente: this.numero(ordem.valor_pendente)
};
}
// ==========================================================
// LOGS
// ==========================================================
getAllLogs(): Promise<OrionLogsModel[]> {
return this.executar(
this.http
.get<OrionApiResponseModel<OrionLogsModel[]>>(
`${this.Rota}/getAllLogs`
)
.toPromise()
.then(response => <GeoCoordenadasResponseModel>response)
.then(data => {
return data;
);
}
insertLog(log: OrionLogInsertModel): Promise<{ id: number }> {
return this.executar(
this.http
.post<OrionApiResponseModel<{ id: number }>>(
`${this.Rota}/insertLog`,
log
)
.toPromise()
);
}
coordenadaToEndereco(
coordenadas: string
): Promise<GeoCoordenadasResponseModel> {
return this.http
.get<GeoCoordenadasResponseModel>(
`http://api.positionstack.com/v1/reverse` +
`?access_key=${environment.GeoAPI}` +
`&query=${encodeURIComponent(coordenadas)}`
)
.toPromise();
}
// ==========================================================
// CADASTROS AUXILIARES
// ==========================================================
getClientesManutencao(): Promise<ClienteManutencaoModel[]> {
return this.executar(
this.http
.get<OrionApiResponseModel<ClienteManutencaoModel[]>>(
`${this.Rota}/getClientesManutencao`
)
.toPromise()
);
}
getEquipamentosManutencao(): Promise<EquipamentoManutencaoModel[]> {
return this.executar(
this.http
.get<OrionApiResponseModel<EquipamentoManutencaoModel[]>>(
`${this.Rota}/getEquipamentosManutencao`
)
.toPromise()
).then(equipamentos =>
equipamentos.map(equipamento => ({
...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
)
}))
);
}
getTecnicosManutencao(): Promise<TecnicoManutencaoModel[]> {
return this.executar(
this.http
.get<OrionApiResponseModel<TecnicoManutencaoModel[]>>(
`${this.Rota}/getTecnicosManutencao`
)
.toPromise()
).then(tecnicos =>
tecnicos.map(tecnico => ({
...tecnico,
id: this.numero(tecnico.id),
usuario_sistema_id: tecnico.usuario_sistema_id
? this.numero(tecnico.usuario_sistema_id)
: null,
valor_hora_expediente_padrao: this.numero(
tecnico.valor_hora_expediente_padrao
),
valor_hora_extra_padrao: this.numero(
tecnico.valor_hora_extra_padrao
)
}))
);
}
getMateriaisManutencao(): Promise<MaterialManutencaoModel[]> {
return this.executar(
this.http
.get<OrionApiResponseModel<MaterialManutencaoModel[]>>(
`${this.Rota}/getMateriaisManutencao`
)
.toPromise()
).then(materiais =>
materiais.map(material => ({
...material,
id: this.numero(material.id),
custo_padrao: this.numero(material.custo_padrao),
preco_venda_padrao: this.numero(material.preco_venda_padrao)
}))
);
}
// ==========================================================
// ORDENS DE SERVIÇO
// ==========================================================
getOrdensServico(
filtros: OrdemServicoFiltrosModel = {}
): Promise<OrdemServicoListaResponseModel> {
const params = this.montarParametros(filtros);
return this.executar(
this.http
.get<OrionApiResponseModel<OrdemServicoListaResponseModel>>(
`${this.Rota}/getOrdensServico`,
{ params }
)
.toPromise()
).then(resultado => ({
items: resultado.items.map(item => this.normalizarOrdemLista(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)
}
}));
}
getOrdemServico(id: number): Promise<OrdemServicoDetalhesModel> {
return this.executar(
this.http
.get<OrionApiResponseModel<OrdemServicoDetalhesModel>>(
`${this.Rota}/getOrdemServico/${id}`
)
.toPromise()
).then(ordem => {
const normalizada = this.normalizarOrdemLista(ordem);
return {
...ordem,
...normalizada,
custo_insumos: this.numero(ordem.custo_insumos),
atividades: (ordem.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_da_os: ordem.odometros_da_os || [],
pagamentos: (ordem.pagamentos || []).map(pagamento => ({
...pagamento,
valor: this.numero(pagamento.valor)
})),
historico_status: ordem.historico_status || []
};
});
}
insertOrdemServico(
ordem: OrdemServicoSalvarModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.post<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/insertOrdemServico`,
ordem
)
.toPromise()
);
}
updateOrdemServico(
id: number,
ordem: OrdemServicoSalvarModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.put<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/updateOrdemServico/${id}`,
ordem
)
.toPromise()
);
}
deleteOrdemServico(
id: number,
usuarioId?: number
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.request<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
'DELETE',
`${this.Rota}/deleteOrdemServico/${id}`,
{
body: {
usuario_id: usuarioId || null
}
}
)
.toPromise()
);
}
// ==========================================================
// ATIVIDADES
// ==========================================================
insertAtividadeOrdemServico(
ordemServicoId: number,
atividade: OrdemServicoAtividadeSalvarModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.post<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/insertAtividadeOrdemServico/${ordemServicoId}`,
atividade
)
.toPromise()
);
}
updateAtividadeOrdemServico(
atividadeId: number,
atividade: OrdemServicoAtividadeSalvarModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.put<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/updateAtividadeOrdemServico/${atividadeId}`,
atividade
)
.toPromise()
);
}
deleteAtividadeOrdemServico(
atividadeId: number
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.delete<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/deleteAtividadeOrdemServico/${atividadeId}`
)
.toPromise()
);
}
// ==========================================================
// ODÔMETROS
// ==========================================================
insertOdometroOrdemServico(
ordemServicoId: number,
odometro: OrdemServicoOdometroModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.post<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/insertOdometroOrdemServico/${ordemServicoId}`,
odometro
)
.toPromise()
);
}
// ==========================================================
// PAGAMENTOS
// ==========================================================
insertPagamentoOrdemServico(
ordemServicoId: number,
pagamento: OrdemServicoPagamentoModel
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.post<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
`${this.Rota}/insertPagamentoOrdemServico/${ordemServicoId}`,
pagamento
)
.toPromise()
);
}
cancelPagamentoOrdemServico(
pagamentoId: number,
usuarioId?: number
): Promise<OrdemServicoOperacaoResultModel> {
return this.executar(
this.http
.request<OrionApiResponseModel<OrdemServicoOperacaoResultModel>>(
'DELETE',
`${this.Rota}/cancelPagamentoOrdemServico/${pagamentoId}`,
{
body: {
usuario_id: usuarioId || null
}
}
)
.toPromise()
);
}
downloadOrdemServicoPdf(
id: number,
interno: boolean = false
): Promise<Blob> {
var url =
this.Rota +
'/downloadOrdemServicoPdf/' +
id +
'?interno=' +
(interno ? '1' : '0');
return this.http
.get(url, {
responseType: 'blob' as 'json'
})
.toPromise()
.then(response => response as Blob);
}
downloadRelatorioOrdensServicoPdf(
filtros: any = {}
): Promise<Blob> {
var params = this.montarParametros(filtros);
return this.http
.get(
this.Rota + '/downloadRelatorioOrdensServicoPdf',
{
params: params,
responseType: 'blob' as 'json'
}
)
.toPromise()
.then(response => response as Blob);
}
}