From a149c5ead6e60acde0e06ba1f1cd5f8d8e13ebd1 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 1 Dec 2025 11:45:27 -0300 Subject: [PATCH] feat: enhance error handling in dashboard layout and improve UI consistency across notification templates with updated styling and structure --- .../src/routes/(dashboard)/+layout.server.ts | 13 +- .../registro-pontos/+page.svelte | 1668 +++++---- .../ti/central-chamados/+page.svelte | 3231 +++++++++-------- .../(dashboard)/ti/notificacoes/+page.svelte | 40 +- .../ti/notificacoes/templates/+page.svelte | 385 +- .../notificacoes/templates/[id]/+page.svelte | 111 +- .../notificacoes/templates/novo/+page.svelte | 96 +- packages/backend/convex/monitoramento.ts | 6 +- 8 files changed, 3100 insertions(+), 2450 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/+layout.server.ts b/apps/web/src/routes/(dashboard)/+layout.server.ts index 12b79a6..f837de7 100644 --- a/apps/web/src/routes/(dashboard)/+layout.server.ts +++ b/apps/web/src/routes/(dashboard)/+layout.server.ts @@ -2,8 +2,13 @@ import { api } from "@sgse-app/backend/convex/_generated/api"; import { createConvexHttpClient } from "@mmailaender/convex-better-auth-svelte/sveltekit"; export const load = async ({ locals }) => { - const client = createConvexHttpClient({ token: locals.token }); - - const currentUser = await client.query(api.auth.getCurrentUser, {}); - return { currentUser }; + try { + const client = createConvexHttpClient({ token: locals.token }); + const currentUser = await client.query(api.auth.getCurrentUser, {}); + return { currentUser }; + } catch (error) { + console.error("Erro ao carregar usuário atual no layout do dashboard:", error); + // Evita quebrar toda a área logada em caso de falha transitória na API/auth + return { currentUser: null }; + } }; diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte index a4b1d16..8663c72 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte @@ -2,7 +2,19 @@ import { onMount, onDestroy } from 'svelte'; import { useQuery, useConvexClient } from 'convex-svelte'; import { api } from '@sgse-app/backend/convex/_generated/api'; - import { Clock, Filter, Download, Printer, BarChart3, Users, CheckCircle2, XCircle, TrendingUp, TrendingDown, FileText } from 'lucide-svelte'; + import { + Clock, + Filter, + Download, + Printer, + BarChart3, + Users, + CheckCircle2, + XCircle, + TrendingUp, + TrendingDown, + FileText + } from 'lucide-svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto'; import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte'; @@ -15,7 +27,7 @@ import { toast } from 'svelte-sonner'; import { Chart, registerables } from 'chart.js'; import Papa from 'papaparse'; - + Chart.register(...registerables); const client = useConvexClient(); @@ -25,7 +37,7 @@ const hoje = new Date(); const trintaDiasAtras = new Date(hoje); trintaDiasAtras.setDate(hoje.getDate() - 30); - + let dataInicio = $state(trintaDiasAtras.toISOString().split('T')[0]!); let dataFim = $state(hoje.toISOString().split('T')[0]!); let funcionarioIdFiltro = $state | ''>(''); @@ -41,14 +53,16 @@ // Parâmetros reativos para queries const registrosParams = $derived({ - funcionarioId: funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined, + funcionarioId: + funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined, dataInicio, - dataFim, + dataFim }); const estatisticasParams = $derived({ dataInicio, dataFim, - funcionarioId: funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined, + funcionarioId: + funcionarioIdFiltro && funcionarioIdFiltro !== '' ? funcionarioIdFiltro : undefined }); // Queries @@ -77,11 +91,10 @@ } }); - // Dados do gráfico baseados nas estatísticas const chartData = $derived.by(() => { if (!estatisticas) return null; - + return { labels: ['Estatísticas de Registros'], datasets: [ @@ -90,14 +103,14 @@ data: [estatisticas.dentroDoPrazo || 0], backgroundColor: 'rgba(34, 197, 94, 0.8)', borderColor: 'rgba(34, 197, 94, 1)', - borderWidth: 2, + borderWidth: 2 }, { label: 'Fora do Prazo', data: [estatisticas.foraDoPrazo || 0], backgroundColor: 'rgba(239, 68, 68, 0.8)', borderColor: 'rgba(239, 68, 68, 1)', - borderWidth: 2, + borderWidth: 2 } ] }; @@ -135,10 +148,10 @@ color: 'hsl(var(--bc))', font: { size: 12, - family: "'Inter', sans-serif", + family: "'Inter', sans-serif" }, usePointStyle: true, - padding: 15, + padding: 15 } }, tooltip: { @@ -149,7 +162,7 @@ borderWidth: 1, padding: 12, callbacks: { - label: function(context) { + label: function (context) { const label = context.dataset.label || ''; const value = context.parsed.y; const total = estatisticas.totalRegistros; @@ -163,12 +176,12 @@ x: { stacked: true, grid: { - display: false, + display: false }, ticks: { color: 'hsl(var(--bc))', font: { - size: 12, + size: 12 } } }, @@ -176,14 +189,14 @@ stacked: true, beginAtZero: true, grid: { - color: 'rgba(0, 0, 0, 0.05)', + color: 'rgba(0, 0, 0, 0.05)' }, ticks: { color: 'hsl(var(--bc))', font: { - size: 11, + size: 11 }, - stepSize: 1, + stepSize: 1 } } }, @@ -193,7 +206,7 @@ }, interaction: { mode: 'index', - intersect: false, + intersect: false } } }); @@ -210,7 +223,7 @@ chartInstance.destroy(); chartInstance = null; } - + // Aguardar um pouco para garantir que o canvas está renderizado const timeoutId = setTimeout(() => { try { @@ -255,21 +268,21 @@ // Os filtros de status e localização são aplicados aqui no frontend const registrosFiltrados = $derived.by(() => { if (!registros || registros.length === 0) return []; - + let resultado = [...registros]; - + // Filtro de status (Dentro/Fora do Prazo) if (statusFiltro !== 'todos') { - resultado = resultado.filter(r => { + resultado = resultado.filter((r) => { if (statusFiltro === 'dentro') return r.dentroDoPrazo === true; if (statusFiltro === 'fora') return r.dentroDoPrazo === false; return true; }); } - + // Filtro de localização (Dentro/Fora do Raio) if (localizacaoFiltro !== 'todos') { - resultado = resultado.filter(r => { + resultado = resultado.filter((r) => { // Se não houver informação de localização, excluir do resultado quando filtro está ativo if (r.dentroRaioPermitido === undefined || r.dentroRaioPermitido === null) { return false; @@ -279,7 +292,7 @@ return true; }); } - + return resultado; }); @@ -295,9 +308,18 @@ string, { data: string; - registros: Array; - saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean }; - saldoDiarioComparativo?: { trabalhadoMinutos: number; esperadoMinutos: number; diferencaMinutos: number }; + registros: Array<(typeof registros)[number]>; + saldoDiario?: { + saldoMinutos: number; + horas: number; + minutos: number; + positivo: boolean; + }; + saldoDiarioComparativo?: { + trabalhadoMinutos: number; + esperadoMinutos: number; + diferencaMinutos: number; + }; } >; } @@ -308,7 +330,7 @@ // Usar registros filtrados ao invés de registros originais const registrosParaAgrupar = registrosFiltrados; - + // Verificar se registros é um array válido if (!Array.isArray(registrosParaAgrupar) || registrosParaAgrupar.length === 0) { return []; @@ -333,7 +355,7 @@ agrupados[key] = { funcionario: registro.funcionario, funcionarioId: registro.funcionarioId, - registrosPorData: {}, + registrosPorData: {} }; } @@ -342,10 +364,10 @@ agrupados[key]!.registrosPorData[dataKey] = { data: dataKey, registros: [], - saldoDiario: undefined, + saldoDiario: undefined }; } - + // Verificar se o registro já não está no array antes de adicionar const jaExiste = agrupados[key]!.registrosPorData[dataKey]!.registros.some( (r) => r._id === registro._id @@ -357,7 +379,7 @@ // Ordenar registros por data e hora dentro de cada grupo e usar saldo diário da query const resultado = Object.values(agrupados); - + // Ordenar grupos por nome do funcionário resultado.sort((a, b) => { const nomeA = a.funcionario?.nome || ''; @@ -372,7 +394,7 @@ }); // Criar novo objeto com datas ordenadas - const registrosPorDataOrdenado: Record = {}; + const registrosPorDataOrdenado: Record = {}; for (const dataKey of datasOrdenadas) { registrosPorDataOrdenado[dataKey] = grupo.registrosPorData[dataKey]!; } @@ -393,32 +415,52 @@ if (configData) { // Calcular saldos parciais (Par 1, Par 2, etc.) const saldosParciais = calcularSaldosParciais(grupoData.registros); - + // Somar todos os saldos parciais para obter o total trabalhado let totalTrabalhado = 0; for (const saldoParcial of saldosParciais.values()) { totalTrabalhado += saldoParcial.saldoMinutos; } - + // Calcular carga horária diária total esperada (soma dos dois pares) - const [horaEntradaConfig, minutoEntradaConfig] = configData.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = configData.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = configData.horarioRetornoAlmoco.split(':').map(Number); - const [horaSaidaConfig, minutoSaidaConfig] = configData.horarioSaida.split(':').map(Number); - + const [horaEntradaConfig, minutoEntradaConfig] = configData.horarioEntrada + .split(':') + .map(Number); + const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = configData.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = + configData.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaConfig, minutoSaidaConfig] = configData.horarioSaida + .split(':') + .map(Number); + // Par 1: entrada -> saida_almoco - const minutosPar1EsperadoConfig = (horaSaidaAlmocoConfig * 60 + minutoSaidaAlmocoConfig) - (horaEntradaConfig * 60 + minutoEntradaConfig); - const minutosPar1EsperadoAjustadoConfig = minutosPar1EsperadoConfig < 0 ? minutosPar1EsperadoConfig + 24 * 60 : minutosPar1EsperadoConfig; - + const minutosPar1EsperadoConfig = + horaSaidaAlmocoConfig * 60 + + minutoSaidaAlmocoConfig - + (horaEntradaConfig * 60 + minutoEntradaConfig); + const minutosPar1EsperadoAjustadoConfig = + minutosPar1EsperadoConfig < 0 + ? minutosPar1EsperadoConfig + 24 * 60 + : minutosPar1EsperadoConfig; + // Par 2: retorno_almoco -> saida - const minutosPar2EsperadoConfig = (horaSaidaConfig * 60 + minutoSaidaConfig) - (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig); - const minutosPar2EsperadoAjustadoConfig = minutosPar2EsperadoConfig < 0 ? minutosPar2EsperadoConfig + 24 * 60 : minutosPar2EsperadoConfig; - - const cargaHorariaDiariaEsperadaMinutos = minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig; - + const minutosPar2EsperadoConfig = + horaSaidaConfig * 60 + + minutoSaidaConfig - + (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig); + const minutosPar2EsperadoAjustadoConfig = + minutosPar2EsperadoConfig < 0 + ? minutosPar2EsperadoConfig + 24 * 60 + : minutosPar2EsperadoConfig; + + const cargaHorariaDiariaEsperadaMinutos = + minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig; + // Calcular diferença em relação à carga horária diária total configurada const diferencaMinutos = totalTrabalhado - cargaHorariaDiariaEsperadaMinutos; - + // Armazenar saldo comparativo grupoData.saldoDiarioComparativo = { trabalhadoMinutos: totalTrabalhado, @@ -427,11 +469,15 @@ }; } else { // Fallback: usar cálculo simples se não houver configuração - const primeiroRegistro = grupoData.registros[0]; - if (primeiroRegistro && 'saldoDiario' in primeiroRegistro && primeiroRegistro.saldoDiario) { - grupoData.saldoDiario = primeiroRegistro.saldoDiario; - } else { - grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros); + const primeiroRegistro = grupoData.registros[0]; + if ( + primeiroRegistro && + 'saldoDiario' in primeiroRegistro && + primeiroRegistro.saldoDiario + ) { + grupoData.saldoDiario = primeiroRegistro.saldoDiario; + } else { + grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros); } } } @@ -440,10 +486,10 @@ // Filtrar grupos que não têm registros após aplicar os filtros // Isso garante que apenas funcionários com registros que passam pelos filtros sejam exibidos - const resultadoFiltrado = resultado.filter(grupo => { + const resultadoFiltrado = resultado.filter((grupo) => { // Verificar se o grupo tem pelo menos um registro em alguma data const temRegistros = Object.values(grupo.registrosPorData).some( - grupoData => grupoData.registros && grupoData.registros.length > 0 + (grupoData) => grupoData.registros && grupoData.registros.length > 0 ); return temRegistros; }); @@ -465,7 +511,12 @@ } // Função para formatar saldo diário - function formatarSaldoDiario(saldo?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean }): string { + function formatarSaldoDiario(saldo?: { + saldoMinutos: number; + horas: number; + minutos: number; + positivo: boolean; + }): string { if (!saldo) return '-'; const sinal = saldo.positivo ? '+' : '-'; return `${sinal}${saldo.horas}h ${saldo.minutos}min`; @@ -476,7 +527,7 @@ // Obter nome do funcionário selecionado const funcionarioSelecionadoNome = $derived.by(() => { if (!funcionarioIdFiltro) return null; - return funcionarios.find(f => f._id === funcionarioIdFiltro)?.nome || null; + return funcionarios.find((f) => f._id === funcionarioIdFiltro)?.nome || null; }); // Função para calcular saldo diário como diferença entre saída e entrada @@ -484,13 +535,21 @@ * Calcula saldos parciais entre cada par entrada/saída * Retorna um mapa com o índice do registro e seu saldo parcial */ - function calcularSaldosParciais(registros: Array<{ tipo: string; hora: number; minuto: number; _id?: any }>): Map { - const saldos = new Map(); + function calcularSaldosParciais( + registros: Array<{ tipo: string; hora: number; minuto: number; _id?: any }> + ): Map< + number, + { saldoMinutos: number; horas: number; minutos: number; positivo: boolean; parNumero: number } + > { + const saldos = new Map< + number, + { saldoMinutos: number; horas: number; minutos: number; positivo: boolean; parNumero: number } + >(); if (registros.length === 0) return saldos; // Criar array com índices originais const registrosComIndice = registros.map((r, idx) => ({ ...r, originalIndex: idx })); - + // Ordenar registros por hora e minuto para processar em ordem cronológica const registrosOrdenados = [...registrosComIndice].sort((a, b) => { if (a.hora !== b.hora) { @@ -502,12 +561,12 @@ // Identificar pares entrada/saída // Par 1: entrada -> saida_almoco // Par 2: retorno_almoco -> saida - let entradaAtual: typeof registrosComIndice[0] | null = null; + let entradaAtual: (typeof registrosComIndice)[0] | null = null; let parNumero = 1; for (let i = 0; i < registrosOrdenados.length; i++) { const registro = registrosOrdenados[i]; - + // Considerar entrada ou retorno_almoco como início de um período if (registro.tipo === 'entrada' || registro.tipo === 'retorno_almoco') { entradaAtual = registro; @@ -517,7 +576,7 @@ // Calcular diferença entre saída e entrada const minutosEntrada = entradaAtual.hora * 60 + entradaAtual.minuto; const minutosSaida = registro.hora * 60 + registro.minuto; - + let saldoMinutos = minutosSaida - minutosEntrada; if (saldoMinutos < 0) { saldoMinutos += 24 * 60; // Adicionar um dia em minutos @@ -544,7 +603,9 @@ return saldos; } - function calcularSaldoDiario(registros: Array<{ tipo: string; hora: number; minuto: number }>): { saldoMinutos: number; horas: number; minutos: number; positivo: boolean } | null { + function calcularSaldoDiario( + registros: Array<{ tipo: string; hora: number; minuto: number }> + ): { saldoMinutos: number; horas: number; minutos: number; positivo: boolean } | null { if (registros.length === 0) return null; // Ordenar registros por hora e minuto @@ -565,7 +626,7 @@ // Calcular diferença em minutos const minutosEntrada = entrada.hora * 60 + entrada.minuto; const minutosSaida = saida.hora * 60 + saida.minuto; - + // Se a saída for no dia seguinte (após meia-noite), adicionar 24 horas let saldoMinutos = minutosSaida - minutosEntrada; if (saldoMinutos < 0) { @@ -579,7 +640,7 @@ saldoMinutos, horas, minutos, - positivo: true, // Sempre positivo, pois é tempo trabalhado + positivo: true // Sempre positivo, pois é tempo trabalhado }; } @@ -587,9 +648,17 @@ * Calcula saldos por par entrada/saída * Retorna um mapa com o índice do registro e informações do saldo do par */ - function calcularSaldosPorPar(registros: Array<{ tipo: string; hora: number; minuto: number }>): Map { - const saldos = new Map(); - + function calcularSaldosPorPar( + registros: Array<{ tipo: string; hora: number; minuto: number }> + ): Map< + number, + { saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number } + > { + const saldos = new Map< + number, + { saldoMinutos: number; horas: number; minutos: number; parIndex: number; tamanhoPar: number } + >(); + if (registros.length === 0) return saldos; // Ordenar registros por hora e minuto @@ -606,7 +675,7 @@ for (let i = 0; i < registrosOrdenados.length; i++) { const reg = registrosOrdenados[i]; - + // Identificar início de um par (entrada ou retorno_almoco) if (reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') { // Se havia um par anterior incompleto, limpar @@ -619,11 +688,11 @@ // Identificar fim de um par (saida_almoco ou saida) else if ((reg.tipo === 'saida_almoco' || reg.tipo === 'saida') && entradaAtual) { indicesPar.push(i); - + // Calcular saldo do par (saída - entrada) const minutosEntrada = entradaAtual.hora * 60 + entradaAtual.minuto; const minutosSaida = reg.hora * 60 + reg.minuto; - + let saldoMinutos = minutosSaida - minutosEntrada; if (saldoMinutos < 0) { saldoMinutos += 24 * 60; // Adicionar um dia em minutos @@ -665,20 +734,9 @@ horarioRetornoAlmoco: string; horarioSaida: string; } - ): Map { - const saldos = new Map(); + } + > { + const saldos = new Map< + number, + { + trabalhadoMinutos: number; + trabalhadoHoras: number; + trabalhadoMinutosResto: number; + esperadoMinutos: number; + esperadoHoras: number; + esperadoMinutosResto: number; + diferencaMinutos: number; + diferencaHoras: number; + diferencaMinutosResto: number; + parIndex: number; + tamanhoPar: number; + } + >(); if (registros.length === 0) return saldos; // Parsear horários esperados da configuração - const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada + .split(':') + .map(Number); + const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number); // Ordenar registros por hora e minuto @@ -748,7 +829,8 @@ } } else { // Par 2: retorno_almoco -> saida - const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; + const minutosEntradaEsperada = + horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; const minutosSaidaEsperada = horaSaidaEsperada * 60 + minutoSaidaEsperado; esperadoMinutos = minutosSaidaEsperada - minutosEntradaEsperada; if (esperadoMinutos < 0) { @@ -802,33 +884,38 @@ const dias: string[] = []; const inicio = new Date(dataInicio); const fim = new Date(dataFim); - + for (let d = new Date(inicio); d <= fim; d.setDate(d.getDate() + 1)) { dias.push(d.toISOString().split('T')[0]!); } - + return dias; } /** * Gera registros esperados para um dia baseado na configuração */ - function gerarRegistrosEsperados(data: string, config: { - horarioEntrada: string; - horarioSaidaAlmoco: string; - horarioRetornoAlmoco: string; - horarioSaida: string; - }): Array<{ tipo: string; hora: number; minuto: number; data: string }> { + function gerarRegistrosEsperados( + data: string, + config: { + horarioEntrada: string; + horarioSaidaAlmoco: string; + horarioRetornoAlmoco: string; + horarioSaida: string; + } + ): Array<{ tipo: string; hora: number; minuto: number; data: string }> { const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number); const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number); return [ { tipo: 'entrada', hora: horaEntrada, minuto: minutoEntrada, data }, { tipo: 'saida_almoco', hora: horaSaidaAlmoco, minuto: minutoSaidaAlmoco, data }, { tipo: 'retorno_almoco', hora: horaRetornoAlmoco, minuto: minutoRetornoAlmoco, data }, - { tipo: 'saida', hora: horaSaida, minuto: minutoSaida, data }, + { tipo: 'saida', hora: horaSaida, minuto: minutoSaida, data } ]; } @@ -839,9 +926,7 @@ registroEsperado: { tipo: string; hora: number; minuto: number }, registrosReais: Array<{ tipo: string; hora: number; minuto: number }> ): boolean { - return registrosReais.some( - (r) => r.tipo === registroEsperado.tipo - ); + return registrosReais.some((r) => r.tipo === registroEsperado.tipo); } function abrirModalImpressao(funcionarioId: Id<'funcionarios'>) { @@ -854,7 +939,7 @@ const hoje = new Date(); const trintaDiasAtras = new Date(hoje); trintaDiasAtras.setDate(hoje.getDate() - 30); - + dataInicio = trintaDiasAtras.toISOString().split('T')[0]!; dataFim = hoje.toISOString().split('T')[0]!; funcionarioIdFiltro = ''; @@ -867,7 +952,7 @@ async function exportarCSV() { try { const registrosParaExportar = registrosFiltrados; - + if (!registrosParaExportar || registrosParaExportar.length === 0) { toast.error('Nenhum registro para exportar'); return; @@ -882,28 +967,29 @@ nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, + nomeSaida: config.nomeSaida }) : getTipoRegistroLabel(registro.tipo); - + const horario = formatarHoraPonto(registro.hora, registro.minuto); const status = registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'; - const localizacao = registro.dentroRaioPermitido === true - ? 'Dentro do Raio' - : registro.dentroRaioPermitido === false - ? 'Fora do Raio' - : 'Não Validado'; - + const localizacao = + registro.dentroRaioPermitido === true + ? 'Dentro do Raio' + : registro.dentroRaioPermitido === false + ? 'Fora do Raio' + : 'Não Validado'; + return { - 'Data': formatarDataDDMMAAAA(registro.data), - 'Funcionário': funcionarioNome, - 'Matrícula': funcionarioMatricula, - 'Tipo': tipo, - 'Horário': horario, - 'Status': status, - 'Localização': localizacao, - 'IP': registro.ipAddress || 'N/A', - 'Dispositivo': registro.deviceType || 'N/A', + Data: formatarDataDDMMAAAA(registro.data), + Funcionário: funcionarioNome, + Matrícula: funcionarioMatricula, + Tipo: tipo, + Horário: horario, + Status: status, + Localização: localizacao, + IP: registro.ipAddress || 'N/A', + Dispositivo: registro.deviceType || 'N/A' }; }); @@ -911,7 +997,7 @@ const csv = Papa.unparse(csvData, { header: true, delimiter: ';', - encoding: 'UTF-8', + encoding: 'UTF-8' }); // Adicionar BOM para Excel reconhecer UTF-8 corretamente @@ -951,7 +1037,7 @@ if (!funcionarioParaImprimir) return; const funcionarioId = funcionarioParaImprimir; - + // Verificar se pelo menos uma seção foi selecionada if (!Object.values(sections).some((v) => v)) { toast.error('Selecione pelo menos uma seção para imprimir'); @@ -968,7 +1054,7 @@ const registrosFuncionario = await client.query(api.pontos.listarRegistrosPeriodo, { funcionarioId, dataInicio, - dataFim, + dataFim }); if (!registrosFuncionario || registrosFuncionario.length === 0) { @@ -1053,7 +1139,7 @@ motivoTipo?: string; observacoes?: string; }> = []; - + let dispensas: Array<{ dataInicio: string; dataFim: string; @@ -1067,14 +1153,15 @@ if (sections.alteracoesGestor) { try { - const todasHomologacoes = await client.query(api.pontos.listarHomologacoes, { - funcionarioId, - }) || []; - + const todasHomologacoes = + (await client.query(api.pontos.listarHomologacoes, { + funcionarioId + })) || []; + // Filtrar homologações pelo período selecionado const dataInicioTimestamp = new Date(dataInicio + 'T00:00:00').getTime(); const dataFimTimestamp = new Date(dataFim + 'T23:59:59').getTime(); - + homologacoes = todasHomologacoes.filter((h) => { return h.criadoEm >= dataInicioTimestamp && h.criadoEm <= dataFimTimestamp; }); @@ -1086,19 +1173,20 @@ if (sections.dispensasRegistro) { try { - const todasDispensas = await client.query(api.pontos.listarDispensas, { - funcionarioId, - apenasAtivas: false, - }) || []; - + const todasDispensas = + (await client.query(api.pontos.listarDispensas, { + funcionarioId, + apenasAtivas: false + })) || []; + // Filtrar dispensas que têm interseção com o período selecionado const dataInicioPeriodo = new Date(dataInicio + 'T00:00:00'); const dataFimPeriodo = new Date(dataFim + 'T23:59:59'); - + dispensas = todasDispensas.filter((d) => { const dispensaInicio = new Date(d.dataInicio + 'T00:00:00'); const dispensaFim = new Date(d.dataFim + 'T23:59:59'); - + // Verificar se há interseção entre os períodos return dispensaInicio <= dataFimPeriodo && dispensaFim >= dataInicioPeriodo; }); @@ -1109,11 +1197,14 @@ } // Variável para armazenar saldos diários (usada no resumo do banco de horas) - const saldosDiariosPorData: Record = {}; + const saldosDiariosPorData: Record< + string, + { + diferencaMinutos: number; + trabalhadoMinutos: number; + esperadoMinutos: number; + } + > = {}; // Tabela de registros if (sections.registrosPonto) { @@ -1160,7 +1251,7 @@ acelerometroZ: r.acelerometroZ, movimentoDetectado: r.movimentoDetectado, magnitudeMovimento: r.magnitudeMovimento, - sensorDisponivel: r.sensorDisponivel, + sensorDisponivel: r.sensorDisponivel }); } @@ -1189,7 +1280,7 @@ hora: reg.hora, minuto: reg.minuto, real: true, - dentroDoPrazo: reg.dentroDoPrazo, + dentroDoPrazo: reg.dentroDoPrazo }); } @@ -1200,7 +1291,7 @@ tipo: regEsperado.tipo, hora: regEsperado.hora, minuto: regEsperado.minuto, - real: false, + real: false }); } } @@ -1218,34 +1309,40 @@ if (a.hora !== b.hora) return a.hora - b.hora; return a.minuto - b.minuto; }); - const saldosComparativosPorPar = calcularSaldoComparativoPorPar(regsReaisOrdenados, config); + const saldosComparativosPorPar = calcularSaldoComparativoPorPar( + regsReaisOrdenados, + config + ); // Calcular saldos esperados para pares incompletos ou dias sem registros - const saldosEsperadosPorPar: Map = new Map(); - + const saldosEsperadosPorPar: Map< + number, + { + trabalhadoMinutos: number; + trabalhadoHoras: number; + trabalhadoMinutosResto: number; + esperadoMinutos: number; + esperadoHoras: number; + esperadoMinutosResto: number; + diferencaMinutos: number; + diferencaHoras: number; + diferencaMinutosResto: number; + tamanhoPar: number; + incompleto: boolean; + } + > = new Map(); + // Calcular saldo diário total (diferença acumulada de todos os pares) // Declarar variáveis ANTES de usá-las let saldoDiarioTotalDiferencaMinutos = 0; let saldoDiarioTotalTrabalhadoMinutos = 0; let saldoDiarioTotalEsperadoMinutos = 0; - + // Criar conjunto de chaves de registros que já foram processados como completos // Isso será usado tanto para evitar criar pares incompletos quanto para evitar somar duplicados const chavesProcessadasCompletas = new Set(); const paresCompletosProcessados = new Set(); // Rastrear parIndex já processados - + // Criar conjunto de chaves e somar saldos dos pares completos // IMPORTANTE: saldosComparativosPorPar associa o saldo a AMBOS os registros do par (entrada + saída) // Então precisamos garantir que somamos apenas UMA VEZ por par, não uma vez por registro @@ -1260,7 +1357,7 @@ saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos; paresCompletosProcessados.add(saldo.parIndex); } - + // Adicionar chaves para evitar processamento duplicado depois const chaveEntrada = `${regEntrada.tipo}-${regEntrada.hora}-${regEntrada.minuto}`; chavesProcessadasCompletas.add(chaveEntrada); @@ -1276,12 +1373,12 @@ } } } - + // Identificar pares incompletos e calcular saldos esperados // IMPORTANTE: Só processar pares que NÃO foram processados como completos for (let i = 0; i < todosRegistros.length; i++) { const reg = todosRegistros[i]; - + // Se é entrada ou retorno_almoco (início de par) e é real if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && reg.real) { // Verificar se este registro já foi processado como par completo @@ -1289,11 +1386,11 @@ if (chavesProcessadasCompletas.has(chaveRegistro)) { continue; // Já foi processado como par completo, pular } - + // Verificar se há saída correspondente real const tipoSaidaEsperado = reg.tipo === 'entrada' ? 'saida_almoco' : 'saida'; - const saidaEncontrada = regsReais.find(r => r.tipo === tipoSaidaEsperado); - + const saidaEncontrada = regsReais.find((r) => r.tipo === tipoSaidaEsperado); + // Se há saída correspondente, verificar se ela também está em chavesProcessadasCompletas // Se estiver, significa que este par já foi processado como completo if (saidaEncontrada) { @@ -1307,7 +1404,7 @@ // Par incompleto: entrada real sem saída correspondente // NÃO calcular tempo trabalhado aqui porque não há saída marcada // O tempo trabalhado será 0, e a diferença será negativa (0 - esperado) - const regEsperado = regsEsperados.find(r => r.tipo === tipoSaidaEsperado); + const regEsperado = regsEsperados.find((r) => r.tipo === tipoSaidaEsperado); if (regEsperado) { // Tempo trabalhado = 0 (não há saída marcada, então não podemos assumir tempo trabalhado) const trabalhadoMinutos = 0; @@ -1315,16 +1412,24 @@ // Calcular tempo esperado let esperadoMinutos: number; if (reg.tipo === 'entrada') { - const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number); + const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada + .split(':') + .map(Number); + const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = + config.horarioSaidaAlmoco.split(':').map(Number); const minutosEntradaEsperada = horaEntradaEsperada * 60 + minutoEntradaEsperado; - const minutosSaidaEsperadaConfig = horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado; + const minutosSaidaEsperadaConfig = + horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado; esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada; if (esperadoMinutos < 0) esperadoMinutos += 24 * 60; } else { - const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number); - const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number); - const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; + const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = + config.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida + .split(':') + .map(Number); + const minutosEntradaEsperada = + horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; const minutosSaidaEsperadaConfig = horaSaidaEsperada * 60 + minutoSaidaEsperado; esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada; if (esperadoMinutos < 0) esperadoMinutos += 24 * 60; @@ -1342,9 +1447,13 @@ // Contar quantos registros fazem parte deste par na lista todosRegistros const indexNaListaTodos = todosRegistros.findIndex( - (r, idx) => idx >= i && r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto + (r, idx) => + idx >= i && + r.tipo === reg.tipo && + r.hora === reg.hora && + r.minuto === reg.minuto ); - + // Contar registros do par (entrada + saída esperada) let tamanhoPar = 1; // entrada const saidaEsperadaNaLista = todosRegistros.find( @@ -1378,7 +1487,7 @@ if (regsReais.length > 0) { for (let i = 0; i < todosRegistros.length; i++) { const reg = todosRegistros[i]; - + // Se é entrada ou retorno_almoco (início de par) e NÃO é real if ((reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco') && !reg.real) { // Verificar se a saída correspondente também não foi marcada @@ -1392,16 +1501,24 @@ // Tempo trabalhado = 0, tempo esperado = configurado, diferença = -esperado let esperadoMinutos: number; if (reg.tipo === 'entrada') { - const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = config.horarioSaidaAlmoco.split(':').map(Number); + const [horaEntradaEsperada, minutoEntradaEsperado] = config.horarioEntrada + .split(':') + .map(Number); + const [horaSaidaAlmocoEsperada, minutoSaidaAlmocoEsperado] = + config.horarioSaidaAlmoco.split(':').map(Number); const minutosEntradaEsperada = horaEntradaEsperada * 60 + minutoEntradaEsperado; - const minutosSaidaEsperadaConfig = horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado; + const minutosSaidaEsperadaConfig = + horaSaidaAlmocoEsperada * 60 + minutoSaidaAlmocoEsperado; esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada; if (esperadoMinutos < 0) esperadoMinutos += 24 * 60; } else { - const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = config.horarioRetornoAlmoco.split(':').map(Number); - const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida.split(':').map(Number); - const minutosEntradaEsperada = horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; + const [horaRetornoAlmocoEsperado, minutoRetornoAlmocoEsperado] = + config.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaEsperada, minutoSaidaEsperado] = config.horarioSaida + .split(':') + .map(Number); + const minutosEntradaEsperada = + horaRetornoAlmocoEsperado * 60 + minutoRetornoAlmocoEsperado; const minutosSaidaEsperadaConfig = horaSaidaEsperada * 60 + minutoSaidaEsperado; esperadoMinutos = minutosSaidaEsperadaConfig - minutosEntradaEsperada; if (esperadoMinutos < 0) esperadoMinutos += 24 * 60; @@ -1450,7 +1567,7 @@ // Usar o conjunto chavesProcessadasCompletas criado anteriormente for (const [indexNaListaTodos, saldo] of saldosEsperadosPorPar.entries()) { const regNaListaTodos = todosRegistros[indexNaListaTodos]; - + // Verificar se este registro real já foi processado como par completo if (regNaListaTodos && regNaListaTodos.real) { const chaveRegistro = `${regNaListaTodos.tipo}-${regNaListaTodos.hora}-${regNaListaTodos.minuto}`; @@ -1458,21 +1575,22 @@ // Este registro está em chavesProcessadasCompletas // Verificar se há uma saída correspondente que também está lá // Se ambas estão, significa que o par completo já foi processado - const tipoSaidaEsperado = regNaListaTodos.tipo === 'entrada' ? 'saida_almoco' : 'saida'; - + const tipoSaidaEsperado = + regNaListaTodos.tipo === 'entrada' ? 'saida_almoco' : 'saida'; + // Procurar saída correspondente em regsReais que também está em chavesProcessadasCompletas - const saidaCompletaEncontrada = regsReais.find(r => { + const saidaCompletaEncontrada = regsReais.find((r) => { if (r.tipo !== tipoSaidaEsperado) return false; const chaveSaida = `${r.tipo}-${r.hora}-${r.minuto}`; return chavesProcessadasCompletas.has(chaveSaida); }); - + if (saidaCompletaEncontrada) { continue; // Par completo já processado (entrada + saída estão em chavesProcessadasCompletas), não somar novamente } } } - + if (saldo.trabalhadoMinutos === 0 && !saldo.incompleto) { // Par completamente não marcado: adicionar diferença negativa saldoDiarioTotalDiferencaMinutos += saldo.diferencaMinutos; @@ -1486,13 +1604,17 @@ saldoDiarioTotalEsperadoMinutos += saldo.esperadoMinutos; } } - + // Se não há registros reais, calcular saldo esperado baseado na configuração if (regsReais.length === 0) { // Calcular saldo esperado do dia completo const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number); // Par 1: entrada -> saida_almoco @@ -1511,10 +1633,11 @@ saldoDiarioTotalEsperadoMinutos = saldoPar1 + saldoPar2; saldoDiarioTotalDiferencaMinutos = -saldoDiarioTotalEsperadoMinutos; // Diferença negativa (0 - esperado) } - + // Calcular diferença corretamente: trabalhado - esperado (não somar diferenças dos pares) - const diferencaDiariaCorrigida = saldoDiarioTotalTrabalhadoMinutos - saldoDiarioTotalEsperadoMinutos; - + const diferencaDiariaCorrigida = + saldoDiarioTotalTrabalhadoMinutos - saldoDiarioTotalEsperadoMinutos; + // Armazenar saldo diário completo (usado no resumo do banco de horas) saldosDiariosPorData[data] = { diferencaMinutos: diferencaDiariaCorrigida, @@ -1524,21 +1647,30 @@ // Calcular carga horária diária total esperada para inicializar saldo acumulado const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number); - const minutosPar1Esperado = (horaSaidaAlmoco * 60 + minutoSaidaAlmoco) - (horaEntrada * 60 + minutoEntrada); - const minutosPar1EsperadoAjustado = minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado; + const minutosPar1Esperado = + horaSaidaAlmoco * 60 + minutoSaidaAlmoco - (horaEntrada * 60 + minutoEntrada); + const minutosPar1EsperadoAjustado = + minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado; - const minutosPar2Esperado = (horaSaida * 60 + minutoSaida) - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco); - const minutosPar2EsperadoAjustado = minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado; + const minutosPar2Esperado = + horaSaida * 60 + minutoSaida - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco); + const minutosPar2EsperadoAjustado = + minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado; + + const cargaHorariaDiariaTotalMinutos = + minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado; - const cargaHorariaDiariaTotalMinutos = minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado; - // Inicializar saldo diário acumulado com a carga horária total diária let saldoDiarioAcumuladoMinutos = cargaHorariaDiariaTotalMinutos; - + // Rastrear quais pares já foram processados para evitar decrementar múltiplas vezes // Usar string como chave: "tipo-parIndex" ou "tipo-indice" para pares incompletos const paresProcessadosParaSaldo = new Set(); @@ -1549,14 +1681,19 @@ const linha: any[] = [ dataFormatada, config - ? getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', { - nomeEntrada: config.nomeEntrada, - nomeSaidaAlmoco: config.nomeSaidaAlmoco, - nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, - }) - : getTipoRegistroLabel(reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida'), - formatarHoraPonto(reg.hora, reg.minuto), + ? getTipoRegistroLabel( + reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida', + { + nomeEntrada: config.nomeEntrada, + nomeSaidaAlmoco: config.nomeSaidaAlmoco, + nomeRetornoAlmoco: config.nomeRetornoAlmoco, + nomeSaida: config.nomeSaida + } + ) + : getTipoRegistroLabel( + reg.tipo as 'entrada' | 'saida_almoco' | 'retorno_almoco' | 'saida' + ), + formatarHoraPonto(reg.hora, reg.minuto) ]; // Marcar linha como não marcada para aplicar cor vermelha depois @@ -1567,7 +1704,7 @@ // Saldo Diário por par entrada/saída com cálculo comparativo if (sections.saldoDiario) { const isInicioPar = reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco'; - + if (reg.real && isInicioPar) { const indexReal = regsReaisOrdenados.findIndex( (r) => r.tipo === reg.tipo && r.hora === reg.hora && r.minuto === reg.minuto @@ -1577,30 +1714,32 @@ const saldoEsperado = saldosEsperadosPorPar.get(i); if (saldoEsperado) { // Par incompleto: decrementar saldo acumulado - + // Decrementar saldo acumulado apenas uma vez por par const chavePar = `${reg.tipo}-incompleto-${i}`; if (!paresProcessadosParaSaldo.has(chavePar)) { saldoDiarioAcumuladoMinutos -= saldoEsperado.trabalhadoMinutos; paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + linha.push({ content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: saldoEsperado.tamanhoPar @@ -1614,86 +1753,91 @@ saldoDiarioAcumuladoMinutos -= saldoPar.trabalhadoMinutos; paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + linha.push({ content: `${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: saldoPar.tamanhoPar }); - } else { - linha.push('-'); - } + } else { + linha.push('-'); + } } else { linha.push('-'); } } else if (reg.real) { // Saída real: não adicionar (já coberto pelo rowspan da entrada) // Mas verificar se precisa adicionar '-' se não houver par completo - const tipoEntradaEsperado = reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco'; - const entradaReal = regsReais.find(r => r.tipo === tipoEntradaEsperado); + const tipoEntradaEsperado = + reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco'; + const entradaReal = regsReais.find((r) => r.tipo === tipoEntradaEsperado); if (!entradaReal) { linha.push('-'); } } else { // Registro não marcado: verificar se faz parte de um par incompleto ou dia sem registros const isInicioPar = reg.tipo === 'entrada' || reg.tipo === 'retorno_almoco'; - + if (isInicioPar) { // Verificar se há saldo esperado para este par const saldoEsperado = saldosEsperadosPorPar.get(i); if (saldoEsperado) { // Par incompleto ou completamente não marcado: decrementar saldo acumulado - + // Decrementar saldo acumulado apenas uma vez por par const chavePar = `${reg.tipo}-esperado-${i}`; if (!paresProcessadosParaSaldo.has(chavePar)) { // Se não há tempo trabalhado, decrementar o tempo esperado completo if (saldoEsperado.trabalhadoMinutos === 0) { saldoDiarioAcumuladoMinutos -= saldoEsperado.esperadoMinutos; - } else { + } else { saldoDiarioAcumuladoMinutos -= saldoEsperado.trabalhadoMinutos; - } + } paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado - } else { + } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + // Se par completamente não marcado (trabalhado = 0), mostrar apenas diferença negativa if (saldoEsperado.trabalhadoMinutos === 0) { linha.push({ content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: saldoEsperado.tamanhoPar }); - } else { + } else { linha.push({ content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: saldoEsperado.tamanhoPar @@ -1709,30 +1853,32 @@ if (saldoMinutos < 0) saldoMinutos += 24 * 60; const horas = Math.floor(saldoMinutos / 60); const minutos = saldoMinutos % 60; - + // Decrementar saldo acumulado apenas uma vez por par const chavePar = `entrada-sem-registros-${i}`; if (!paresProcessadosParaSaldo.has(chavePar)) { saldoDiarioAcumuladoMinutos -= saldoMinutos; paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + // Para dia sem registros, mostrar 0h trabalhado linha.push({ content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, @@ -1746,38 +1892,40 @@ if (saldoMinutos < 0) saldoMinutos += 24 * 60; const horas = Math.floor(saldoMinutos / 60); const minutos = saldoMinutos % 60; - + // Decrementar saldo acumulado apenas uma vez por par const chavePar = `retorno_almoco-sem-registros-${i}`; if (!paresProcessadosParaSaldo.has(chavePar)) { saldoDiarioAcumuladoMinutos -= saldoMinutos; paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + // Para dia sem registros, mostrar 0h trabalhado linha.push({ content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: 2 // retorno_almoco + saida }); - } else { - linha.push('-'); - } + } else { + linha.push('-'); + } } else { // Há registros reais mas este par não foi marcado completamente // Verificar se é um par completamente não marcado @@ -1785,7 +1933,7 @@ const saidaEsperadaExiste = todosRegistros.some( (r, idx) => idx > i && r.tipo === tipoSaidaEsperado && !r.real ); - + if (saidaEsperadaExiste) { // Par completamente não marcado: calcular saldo negativo e decrementar saldo acumulado const saldoEsperadoCompleto = saldosEsperadosPorPar.get(i); @@ -1796,43 +1944,47 @@ saldoDiarioAcumuladoMinutos -= saldoEsperadoCompleto.esperadoMinutos; paresProcessadosParaSaldo.add(chavePar); } - + // Calcular saldo acumulado formatado // Se saldoDiarioAcumuladoMinutos > 0: ainda falta trabalhar (mostrar como negativo) // Se saldoDiarioAcumuladoMinutos < 0: trabalhou mais que o esperado (mostrar como positivo) - const saldoAcumuladoHoras = Math.floor(Math.abs(saldoDiarioAcumuladoMinutos) / 60); - const saldoAcumuladoMinutosResto = Math.abs(saldoDiarioAcumuladoMinutos) % 60; + const saldoAcumuladoHoras = Math.floor( + Math.abs(saldoDiarioAcumuladoMinutos) / 60 + ); + const saldoAcumuladoMinutosResto = + Math.abs(saldoDiarioAcumuladoMinutos) % 60; // Inverter sinal: positivo quando trabalhou mais, negativo quando ainda falta const sinalSaldo = saldoDiarioAcumuladoMinutos < 0 ? '+' : '-'; const trabalhouMaisQueEsperado = saldoDiarioAcumuladoMinutos < 0; - + // Marcar linha para aplicar cor no saldo if (trabalhouMaisQueEsperado) { linha._saldoPositivo = true; // Verde: trabalhou mais que o esperado } else { linha._saldoNegativo = true; // Vermelho: ainda falta trabalhar } - + linha.push({ content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`, rowSpan: saldoEsperadoCompleto.tamanhoPar }); - } else { + } else { linha.push('-'); - } - } else { - linha.push('-'); - } + } + } else { + linha.push('-'); + } } } else { // Saída não marcada: verificar se faz parte de um par completamente não marcado // Se a entrada correspondente também não foi marcada, o saldo já foi adicionado na linha da entrada // Então apenas não adicionar nada aqui (será coberto pelo rowspan) - const tipoEntradaEsperado = reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco'; + const tipoEntradaEsperado = + reg.tipo === 'saida_almoco' ? 'entrada' : 'retorno_almoco'; const entradaEsperadaExiste = todosRegistros.some( (r, idx) => idx < i && r.tipo === tipoEntradaEsperado && !r.real ); - + if (!entradaEsperadaExiste) { // Saída sem entrada correspondente esperada: não tem saldo linha.push('-'); @@ -1864,7 +2016,7 @@ theme: 'grid', headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' }, styles: { fontSize: 9 }, - didParseCell: function(data) { + didParseCell: function (data) { // Aplicar cor vermelha para registros não marcados if (data.row.raw && (data.row.raw as any)._naoMarcado) { // Aplicar cor vermelha nas colunas Tipo e Horário @@ -1877,7 +2029,7 @@ if (data.row.raw) { const rowData = data.row.raw as any; const indiceSaldoDiario = sections.saldoDiario ? 3 : -1; - + if (data.column.index === indiceSaldoDiario) { if (rowData._saldoNegativo) { // Saldo negativo: cor vermelha @@ -1916,7 +2068,7 @@ } const bancoHoras = await client.query(api.pontos.obterBancoHorasFuncionario, { - funcionarioId, + funcionarioId }); // Calcular total de dias do período selecionado @@ -1924,21 +2076,40 @@ const totalDiasPeriodo = diasPeriodo.length; // Calcular carga horária diária esperada baseada na configuração - const [horaEntradaConfig, minutoEntradaConfig] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaEntradaConfig, minutoEntradaConfig] = config.horarioEntrada + .split(':') + .map(Number); + const [horaSaidaAlmocoConfig, minutoSaidaAlmocoConfig] = config.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmocoConfig, minutoRetornoAlmocoConfig] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaidaConfig, minutoSaidaConfig] = config.horarioSaida.split(':').map(Number); // Par 1: entrada -> saida_almoco - const minutosPar1EsperadoConfig = (horaSaidaAlmocoConfig * 60 + minutoSaidaAlmocoConfig) - (horaEntradaConfig * 60 + minutoEntradaConfig); - const minutosPar1EsperadoAjustadoConfig = minutosPar1EsperadoConfig < 0 ? minutosPar1EsperadoConfig + 24 * 60 : minutosPar1EsperadoConfig; + const minutosPar1EsperadoConfig = + horaSaidaAlmocoConfig * 60 + + minutoSaidaAlmocoConfig - + (horaEntradaConfig * 60 + minutoEntradaConfig); + const minutosPar1EsperadoAjustadoConfig = + minutosPar1EsperadoConfig < 0 + ? minutosPar1EsperadoConfig + 24 * 60 + : minutosPar1EsperadoConfig; // Par 2: retorno_almoco -> saida - const minutosPar2EsperadoConfig = (horaSaidaConfig * 60 + minutoSaidaConfig) - (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig); - const minutosPar2EsperadoAjustadoConfig = minutosPar2EsperadoConfig < 0 ? minutosPar2EsperadoConfig + 24 * 60 : minutosPar2EsperadoConfig; + const minutosPar2EsperadoConfig = + horaSaidaConfig * 60 + + minutoSaidaConfig - + (horaRetornoAlmocoConfig * 60 + minutoRetornoAlmocoConfig); + const minutosPar2EsperadoAjustadoConfig = + minutosPar2EsperadoConfig < 0 + ? minutosPar2EsperadoConfig + 24 * 60 + : minutosPar2EsperadoConfig; + + const cargaHorariaDiariaEsperadaMinutos = + minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig; - const cargaHorariaDiariaEsperadaMinutos = minutosPar1EsperadoAjustadoConfig + minutosPar2EsperadoAjustadoConfig; - // Calcular saldos do período selecionado baseado nos saldos diários calculados let saldoPeriodoTrabalhadoMinutos = 0; let diasComSaldoPositivo = 0; @@ -1949,24 +2120,27 @@ // Somar todos os saldos diários do período for (const saldo of Object.values(saldosDiariosPorData)) { saldoPeriodoTrabalhadoMinutos += saldo.trabalhadoMinutos; - + // Calcular diferença diária corretamente: trabalhado - esperado const diferencaDiaria = saldo.trabalhadoMinutos - saldo.esperadoMinutos; - + if (diferencaDiaria > 0) { diasComSaldoPositivo++; } else if (diferencaDiaria < 0) { diasComSaldoNegativo++; } - + if (saldo.trabalhadoMinutos === 0 && saldo.esperadoMinutos > 0) { diasSemRegistros++; } } } else { // Fallback: calcular a partir dos registros se não tiver saldos diários - const registrosPorDataPeriodo: Record> = {}; - + const registrosPorDataPeriodo: Record< + string, + Array<{ tipo: string; hora: number; minuto: number }> + > = {}; + for (const r of registrosFuncionario) { const dataKey = r.data; if (!registrosPorDataPeriodo[dataKey]) { @@ -1975,7 +2149,7 @@ registrosPorDataPeriodo[dataKey]!.push({ tipo: r.tipo, hora: r.hora, - minuto: r.minuto, + minuto: r.minuto }); } @@ -1990,33 +2164,50 @@ // Calcular saldo esperado do período: carga horária diária × número de dias // SEMPRE calcular diretamente, não somar saldos diários esperados (pode duplicar) const saldoPeriodoEsperadoMinutos = cargaHorariaDiariaEsperadaMinutos * totalDiasPeriodo; - + // Calcular diferença do período corretamente: trabalhado - esperado (para "Saldo do Período Exibido") - const saldoPeriodoDiferencaMinutos = saldoPeriodoTrabalhadoMinutos - saldoPeriodoEsperadoMinutos; - + const saldoPeriodoDiferencaMinutos = + saldoPeriodoTrabalhadoMinutos - saldoPeriodoEsperadoMinutos; + // Calcular diferença do período (trabalhado - esperado) para exibição na linha "Diferença do Período" // Negativo quando trabalhado < esperado (vermelho), positivo quando trabalhado > esperado (verde) - const diferencaPeriodoTrabalhadoMenosEsperado = saldoPeriodoTrabalhadoMinutos - saldoPeriodoEsperadoMinutos; + const diferencaPeriodoTrabalhadoMenosEsperado = + saldoPeriodoTrabalhadoMinutos - saldoPeriodoEsperadoMinutos; // Calcular médias diárias - const mediaDiariaTrabalhadaHoras = totalDiasPeriodo > 0 ? Math.floor(saldoPeriodoTrabalhadoMinutos / 60 / totalDiasPeriodo) : 0; - const mediaDiariaTrabalhadaMinutos = totalDiasPeriodo > 0 ? Math.floor((saldoPeriodoTrabalhadoMinutos / totalDiasPeriodo) % 60) : 0; - + const mediaDiariaTrabalhadaHoras = + totalDiasPeriodo > 0 + ? Math.floor(saldoPeriodoTrabalhadoMinutos / 60 / totalDiasPeriodo) + : 0; + const mediaDiariaTrabalhadaMinutos = + totalDiasPeriodo > 0 + ? Math.floor((saldoPeriodoTrabalhadoMinutos / totalDiasPeriodo) % 60) + : 0; + // Calcular média esperada baseada na configuração padrão (não no período) const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number); - const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number); - const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number); + const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco + .split(':') + .map(Number); + const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco + .split(':') + .map(Number); const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number); // Par 1: entrada -> saida_almoco - const minutosPar1Esperado = (horaSaidaAlmoco * 60 + minutoSaidaAlmoco) - (horaEntrada * 60 + minutoEntrada); - const minutosPar1EsperadoAjustado = minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado; + const minutosPar1Esperado = + horaSaidaAlmoco * 60 + minutoSaidaAlmoco - (horaEntrada * 60 + minutoEntrada); + const minutosPar1EsperadoAjustado = + minutosPar1Esperado < 0 ? minutosPar1Esperado + 24 * 60 : minutosPar1Esperado; // Par 2: retorno_almoco -> saida - const minutosPar2Esperado = (horaSaida * 60 + minutoSaida) - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco); - const minutosPar2EsperadoAjustado = minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado; + const minutosPar2Esperado = + horaSaida * 60 + minutoSaida - (horaRetornoAlmoco * 60 + minutoRetornoAlmoco); + const minutosPar2EsperadoAjustado = + minutosPar2Esperado < 0 ? minutosPar2Esperado + 24 * 60 : minutosPar2Esperado; - const totalEsperadoDiarioMinutos = minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado; + const totalEsperadoDiarioMinutos = + minutosPar1EsperadoAjustado + minutosPar2EsperadoAjustado; const mediaDiariaEsperadaHoras = Math.floor(totalEsperadoDiarioMinutos / 60); const mediaDiariaEsperadaMinutos = totalEsperadoDiarioMinutos % 60; @@ -2029,7 +2220,9 @@ // Diferença do Período: trabalhado - esperado // Negativo quando trabalhado < esperado (vermelho), positivo quando trabalhado > esperado (verde) - const horasDiferencaPeriodo = Math.floor(Math.abs(diferencaPeriodoTrabalhadoMenosEsperado) / 60); + const horasDiferencaPeriodo = Math.floor( + Math.abs(diferencaPeriodoTrabalhadoMenosEsperado) / 60 + ); const minutosDiferencaPeriodo = Math.abs(diferencaPeriodoTrabalhadoMenosEsperado) % 60; const sinalDiferencaPeriodo = diferencaPeriodoTrabalhadoMenosEsperado >= 0 ? '+' : '-'; const diferencaPeriodoFormatado = `${sinalDiferencaPeriodo}${horasDiferencaPeriodo}h ${minutosDiferencaPeriodo}min`; @@ -2088,8 +2281,14 @@ // Adicionar estatísticas bancoHorasData.push(['', '']); // Linha separadora - bancoHorasData.push(['Média Diária de Horas Trabalhadas', `+${mediaDiariaTrabalhadaHoras}h ${mediaDiariaTrabalhadaMinutos}min`]); - bancoHorasData.push(['Média Diária Esperada', `+${mediaDiariaEsperadaHoras}h ${mediaDiariaEsperadaMinutos}min`]); + bancoHorasData.push([ + 'Média Diária de Horas Trabalhadas', + `+${mediaDiariaTrabalhadaHoras}h ${mediaDiariaTrabalhadaMinutos}min` + ]); + bancoHorasData.push([ + 'Média Diária Esperada', + `+${mediaDiariaEsperadaHoras}h ${mediaDiariaEsperadaMinutos}min` + ]); // Adicionar contagens bancoHorasData.push(['', '']); // Linha separadora @@ -2110,7 +2309,7 @@ 0: { fontStyle: 'bold', cellWidth: 80 }, 1: { cellWidth: 'auto' } }, - didParseCell: function(data) { + didParseCell: function (data) { // Ignorar linhas separadoras vazias if (data.cell.text[0] === '' && data.column.index === 0) { data.cell.styles.fillColor = [240, 240, 240]; // Cor de fundo cinza claro @@ -2124,21 +2323,29 @@ if (data.column.index === 1 && valor) { // Aplicar cor no Saldo Atual (vermelho para negativo, azul para positivo) if (campo === 'Saldo Atual') { - if (saldoMinutos < 0) { + if (saldoMinutos < 0) { data.cell.styles.textColor = [200, 0, 0]; // Vermelho para negativo } else if (saldoMinutos > 0) { data.cell.styles.textColor = [0, 100, 200]; // Azul para positivo } data.cell.styles.fontStyle = 'bold'; } - + // Verificar se o valor contém sinal negativo if (valor.includes('-') && !valor.includes('±')) { data.cell.styles.textColor = [200, 0, 0]; // Vermelho para negativo - if (campo === 'Saldo do Período Exibido' || campo === 'Diferença do Período' || campo === 'Resultado Final') { + if ( + campo === 'Saldo do Período Exibido' || + campo === 'Diferença do Período' || + campo === 'Resultado Final' + ) { data.cell.styles.fontStyle = 'bold'; } - } else if (valor.includes('+') || campo.includes('Trabalhado') || campo.includes('Esperado')) { + } else if ( + valor.includes('+') || + campo.includes('Trabalhado') || + campo.includes('Esperado') + ) { // Verde para valores positivos ou campos de trabalhado/esperado if (campo.includes('Diferença do Período')) { // Diferença do Período: trabalhado - esperado @@ -2189,7 +2396,7 @@ const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY; if (finalY) { yPosition = finalY + 10; - } else { + } else { yPosition += bancoHorasData.length * 7 + 10; } } else { @@ -2200,7 +2407,7 @@ body: [['Banco de horas', 'Não disponível']], theme: 'grid', headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' }, - styles: { fontSize: 9 }, + styles: { fontSize: 9 } }); const lastPage = doc.getNumberOfPages(); @@ -2229,32 +2436,39 @@ doc.setTextColor(0, 0, 0); yPosition += 10; - const homologacoesData = homologacoes.map((h) => { - // Formatar data de criação usando função centralizada (DD/MM/AAAA) - const dataFormatada = formatarDataDDMMAAAA(h.criadoEm); - - if (h.registroId && h.horaAnterior !== undefined) { - return [ - dataFormatada, - 'Edição de Registro', - h.horaAnterior !== undefined - ? `${formatarHoraPonto(h.horaAnterior, h.minutoAnterior || 0)} → ${formatarHoraPonto(h.horaNova || 0, h.minutoNova || 0)}` - : '-', - h.motivoDescricao || h.motivoTipo || '-', - h.observacoes || '-', - ]; - } else if (h.tipoAjuste) { - const tipoAjusteLabel = h.tipoAjuste === 'compensar' ? 'Compensar' : h.tipoAjuste === 'abonar' ? 'Abonar' : 'Descontar'; - return [ - dataFormatada, - `Ajuste: ${tipoAjusteLabel}`, - `${h.periodoDias || 0}d ${h.periodoHoras || 0}h ${h.periodoMinutos || 0}min`, - h.motivoDescricao || h.motivoTipo || '-', - h.observacoes || '-', - ]; - } - return []; - }).filter((row) => row.length > 0); + const homologacoesData = homologacoes + .map((h) => { + // Formatar data de criação usando função centralizada (DD/MM/AAAA) + const dataFormatada = formatarDataDDMMAAAA(h.criadoEm); + + if (h.registroId && h.horaAnterior !== undefined) { + return [ + dataFormatada, + 'Edição de Registro', + h.horaAnterior !== undefined + ? `${formatarHoraPonto(h.horaAnterior, h.minutoAnterior || 0)} → ${formatarHoraPonto(h.horaNova || 0, h.minutoNova || 0)}` + : '-', + h.motivoDescricao || h.motivoTipo || '-', + h.observacoes || '-' + ]; + } else if (h.tipoAjuste) { + const tipoAjusteLabel = + h.tipoAjuste === 'compensar' + ? 'Compensar' + : h.tipoAjuste === 'abonar' + ? 'Abonar' + : 'Descontar'; + return [ + dataFormatada, + `Ajuste: ${tipoAjusteLabel}`, + `${h.periodoDias || 0}d ${h.periodoHoras || 0}h ${h.periodoMinutos || 0}min`, + h.motivoDescricao || h.motivoTipo || '-', + h.observacoes || '-' + ]; + } + return []; + }) + .filter((row) => row.length > 0); if (homologacoesData.length > 0) { autoTable(doc, { @@ -2269,15 +2483,15 @@ 1: { cellWidth: 40 }, // Tipo 2: { cellWidth: 50, cellPadding: { top: 2, bottom: 2, left: 2, right: 2 } }, // Detalhes - maior largura 3: { cellWidth: 40 }, // Motivo - 4: { cellWidth: 'auto' }, // Observações + 4: { cellWidth: 'auto' } // Observações }, - didParseCell: function(data) { + didParseCell: function (data) { // Permitir quebra de linha na coluna Detalhes (índice 2) if (data.column.index === 2) { data.cell.styles.overflow = 'linebreak'; data.cell.styles.cellWidth = 50; } - }, + } }); const lastPage = doc.getNumberOfPages(); @@ -2310,12 +2524,12 @@ // Formatar data de início e fim usando função centralizada (DD/MM/AAAA) const dataInicioFormatada = formatarDataDDMMAAAA(d.dataInicio); const dataFimFormatada = formatarDataDDMMAAAA(d.dataFim); - + return [ `${dataInicioFormatada} ${d.horaInicio.toString().padStart(2, '0')}:${d.minutoInicio.toString().padStart(2, '0')}`, `${dataFimFormatada} ${d.horaFim.toString().padStart(2, '0')}:${d.minutoFim.toString().padStart(2, '0')}`, d.motivo, - d.isento ? 'Isento (sem expiração)' : 'Temporária', + d.isento ? 'Isento (sem expiração)' : 'Temporária' ]; }); @@ -2325,7 +2539,7 @@ body: dispensasData, theme: 'grid', headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' }, - styles: { fontSize: 9 }, + styles: { fontSize: 9 } }); const lastPage = doc.getNumberOfPages(); @@ -2355,7 +2569,7 @@ // Salvar const nomeArquivo = `ficha-ponto-${funcionario.matricula || funcionario.nome}-${dataInicio}-${dataFim}.pdf`; doc.save(nomeArquivo); - + // Fechar modal após gerar PDF mostrarModalImpressao = false; funcionarioParaImprimir = ''; @@ -2385,7 +2599,7 @@ try { // Buscar dados completos do registro const registro = await client.query(api.pontos.obterRegistro, { registroId }); - + if (!registro) { alert('Registro não encontrado'); return; @@ -2419,7 +2633,7 @@ doc.setTextColor(41, 128, 185); doc.setFont('helvetica', 'bold'); doc.text('DETALHES DO REGISTRO DE PONTO', 105, yPosition, { align: 'center' }); - + // Linha decorativa abaixo do título doc.setDrawColor(41, 128, 185); doc.setLineWidth(0.5); @@ -2442,22 +2656,22 @@ if (funcionarioData.length > 0) { doc.setFontSize(12); doc.setTextColor(41, 128, 185); - doc.setFont('helvetica', 'bold'); + doc.setFont('helvetica', 'bold'); doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition); - yPosition += 8; + yPosition += 8; autoTable(doc, { startY: yPosition, head: [['Campo', 'Valor']], body: funcionarioData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10, textColor: [0, 0, 0] }, @@ -2483,12 +2697,12 @@ nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, + nomeSaida: config.nomeSaida }) : getTipoRegistroLabel(registro.tipo); const dataHora = `${registro.data} ${registro.hora.toString().padStart(2, '0')}:${registro.minuto.toString().padStart(2, '0')}:${registro.segundo.toString().padStart(2, '0')}`; - + const registroData: any[][] = [ ['Tipo', tipoLabel], ['Data e Hora', dataHora], @@ -2501,30 +2715,30 @@ registroData.push(['Justificativa', registro.justificativa]); } - // Verificar se precisa de nova página - if (yPosition > 200) { - doc.addPage(); - yPosition = 20; - } + // Verificar se precisa de nova página + if (yPosition > 200) { + doc.addPage(); + yPosition = 20; + } doc.setFontSize(12); doc.setTextColor(41, 128, 185); - doc.setFont('helvetica', 'bold'); + doc.setFont('helvetica', 'bold'); doc.text('DADOS DO REGISTRO', 15, yPosition); - yPosition += 8; + yPosition += 8; autoTable(doc, { startY: yPosition, head: [['Campo', 'Valor']], body: registroData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10, textColor: [0, 0, 0] }, @@ -2602,13 +2816,13 @@ head: [['Campo', 'Valor']], body: localizacaoData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10, textColor: [0, 0, 0] }, @@ -2623,7 +2837,8 @@ type JsPDFWithAutoTable3 = jsPDF & { lastAutoTable?: { finalY: number }; }; - const finalYLocalizacao = (doc as JsPDFWithAutoTable3).lastAutoTable?.finalY ?? yPosition + 10; + const finalYLocalizacao = + (doc as JsPDFWithAutoTable3).lastAutoTable?.finalY ?? yPosition + 10; yPosition = finalYLocalizacao + 10; } @@ -2653,13 +2868,13 @@ head: [['Campo', 'Valor']], body: gpsData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10, textColor: [0, 0, 0] }, @@ -2685,7 +2900,10 @@ confiabilidadeData.push(['Confiabilidade GPS (Frontend)', `${confiabilidadePercent}%`]); } - if (registro.scoreConfiancaBackend !== null && registro.scoreConfiancaBackend !== undefined) { + if ( + registro.scoreConfiancaBackend !== null && + registro.scoreConfiancaBackend !== undefined + ) { const scorePercent = (registro.scoreConfiancaBackend * 100).toFixed(1); confiabilidadeData.push(['Score de Confiança (Backend)', `${scorePercent}%`]); } @@ -2693,8 +2911,8 @@ if (confiabilidadeData.length > 0) { doc.setFontSize(11); doc.setTextColor(0, 0, 0); - doc.setFont('helvetica', 'bold'); - doc.text('Confiabilidade:', 15, yPosition); + doc.setFont('helvetica', 'bold'); + doc.text('Confiabilidade:', 15, yPosition); yPosition += 8; autoTable(doc, { @@ -2702,13 +2920,13 @@ head: [['Campo', 'Valor']], body: confiabilidadeData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [60, 60, 60], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10 }, columnStyles: { @@ -2770,13 +2988,13 @@ head: [['Campo', 'Valor']], body: statusData, theme: 'striped', - headStyles: { + headStyles: { fillColor: registro.suspeitaSpoofing ? [200, 0, 0] : [0, 128, 0], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10 }, columnStyles: { @@ -2801,7 +3019,8 @@ type JsPDFWithAutoTable6 = jsPDF & { lastAutoTable?: { finalY: number }; }; - const finalYStatus = (doc as JsPDFWithAutoTable6).lastAutoTable?.finalY ?? yPosition + 10; + const finalYStatus = + (doc as JsPDFWithAutoTable6).lastAutoTable?.finalY ?? yPosition + 10; yPosition = finalYStatus + 5; } } @@ -2809,7 +3028,7 @@ // Avisos de Validação em tabela if (registro.avisosValidacao && registro.avisosValidacao.length > 0) { const avisosData = registro.avisosValidacao.map((aviso: string) => ['', aviso]); - + doc.setFontSize(11); doc.setTextColor(0, 0, 0); doc.setFont('helvetica', 'bold'); @@ -2821,13 +3040,13 @@ head: [['', 'Aviso']], body: avisosData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [255, 165, 0], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10, textColor: [0, 0, 0] }, @@ -2851,21 +3070,33 @@ let propriedadesGPS = 0; let propriedadesTotais = 5; - if (registro.altitude !== null && registro.altitude !== undefined && registro.altitude !== 0) { + if ( + registro.altitude !== null && + registro.altitude !== undefined && + registro.altitude !== 0 + ) { propriedadesData.push(['Altitude', '✓ Disponível']); propriedadesGPS++; } else { propriedadesData.push(['Altitude', '✗ Não disponível']); } - if (registro.altitudeAccuracy !== null && registro.altitudeAccuracy !== undefined && registro.altitudeAccuracy > 0) { + if ( + registro.altitudeAccuracy !== null && + registro.altitudeAccuracy !== undefined && + registro.altitudeAccuracy > 0 + ) { propriedadesData.push(['Precisão de Altitude', '✓ Disponível']); propriedadesGPS++; } else { propriedadesData.push(['Precisão de Altitude', '✗ Não disponível']); } - if (registro.heading !== null && registro.heading !== undefined && !isNaN(registro.heading)) { + if ( + registro.heading !== null && + registro.heading !== undefined && + !isNaN(registro.heading) + ) { propriedadesData.push(['Direção (Heading)', '✓ Disponível']); propriedadesGPS++; } else { @@ -2879,10 +3110,19 @@ propriedadesData.push(['Velocidade', '✗ Não disponível']); } - if (registro.precisao !== null && registro.precisao !== undefined && registro.precisao < 20) { + if ( + registro.precisao !== null && + registro.precisao !== undefined && + registro.precisao < 20 + ) { propriedadesData.push(['Precisão GPS', '✓ Alta precisão (< 20m)']); propriedadesGPS++; - } else if (registro.precisao !== null && registro.precisao !== undefined && registro.precisao >= 20 && registro.precisao < 100) { + } else if ( + registro.precisao !== null && + registro.precisao !== undefined && + registro.precisao >= 20 && + registro.precisao < 100 + ) { propriedadesData.push(['Precisão GPS', '⚠ Precisão média (20-100m)']); propriedadesGPS += 0.5; } else { @@ -2891,8 +3131,14 @@ // Indicador de qualidade GPS const qualidadeGPS = (propriedadesGPS / propriedadesTotais) * 100; - const qualidadeTexto = qualidadeGPS >= 80 ? 'Alta qualidade (GPS real)' : qualidadeGPS >= 50 ? 'Qualidade média' : 'Baixa qualidade (possível spoofing)'; - const qualidadeCor = qualidadeGPS >= 80 ? [0, 128, 0] : qualidadeGPS >= 50 ? [255, 165, 0] : [255, 0, 0]; + const qualidadeTexto = + qualidadeGPS >= 80 + ? 'Alta qualidade (GPS real)' + : qualidadeGPS >= 50 + ? 'Qualidade média' + : 'Baixa qualidade (possível spoofing)'; + const qualidadeCor = + qualidadeGPS >= 80 ? [0, 128, 0] : qualidadeGPS >= 50 ? [255, 165, 0] : [255, 0, 0]; propriedadesData.push(['Qualidade GPS', `${qualidadeTexto} (${qualidadeGPS.toFixed(0)}%)`]); doc.setFontSize(11); @@ -2906,13 +3152,13 @@ head: [['Propriedade', 'Status']], body: propriedadesData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [60, 60, 60], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10 }, columnStyles: { @@ -2945,7 +3191,8 @@ type JsPDFWithAutoTable8 = jsPDF & { lastAutoTable?: { finalY: number }; }; - const finalYPropriedades = (doc as JsPDFWithAutoTable8).lastAutoTable?.finalY ?? yPosition + 10; + const finalYPropriedades = + (doc as JsPDFWithAutoTable8).lastAutoTable?.finalY ?? yPosition + 10; yPosition = finalYPropriedades + 10; } @@ -2972,10 +3219,9 @@ if (registro.enderecoMarcacaoEsperado) { try { - const enderecoEsperado = await client.query( - api.enderecosMarcacao.obterEndereco, - { enderecoId: registro.enderecoMarcacaoEsperado } - ); + const enderecoEsperado = await client.query(api.enderecosMarcacao.obterEndereco, { + enderecoId: registro.enderecoMarcacaoEsperado + }); if (enderecoEsperado) { enderecoEsperadoNome = enderecoEsperado.nome; enderecoEsperadoEndereco = `${enderecoEsperado.endereco}, ${enderecoEsperado.cidade}/${enderecoEsperado.estado}`; @@ -2993,28 +3239,39 @@ ]; if (enderecoEsperadoLatitude !== null && enderecoEsperadoLongitude !== null) { - geofencingData.push(['Coordenadas Esperadas', `${enderecoEsperadoLatitude.toFixed(6)}, ${enderecoEsperadoLongitude.toFixed(6)}`]); + geofencingData.push([ + 'Coordenadas Esperadas', + `${enderecoEsperadoLatitude.toFixed(6)}, ${enderecoEsperadoLongitude.toFixed(6)}` + ]); } - geofencingData.push(['Coordenadas do Registro', `${registro.latitude.toFixed(6)}, ${registro.longitude.toFixed(6)}`]); + geofencingData.push([ + 'Coordenadas do Registro', + `${registro.latitude.toFixed(6)}, ${registro.longitude.toFixed(6)}` + ]); - if (registro.distanciaEnderecoEsperado !== null && registro.distanciaEnderecoEsperado !== undefined) { + if ( + registro.distanciaEnderecoEsperado !== null && + registro.distanciaEnderecoEsperado !== undefined + ) { const distanciaKm = (registro.distanciaEnderecoEsperado / 1000).toFixed(2); const distanciaMetros = registro.distanciaEnderecoEsperado.toFixed(0); - const distanciaTexto = registro.distanciaEnderecoEsperado >= 1000 - ? `${distanciaKm} km (${distanciaMetros} metros)` - : `${distanciaMetros} metros`; + const distanciaTexto = + registro.distanciaEnderecoEsperado >= 1000 + ? `${distanciaKm} km (${distanciaMetros} metros)` + : `${distanciaMetros} metros`; geofencingData.push(['Distância', distanciaTexto]); } if (registro.raioToleranciaUsado !== null && registro.raioToleranciaUsado !== undefined) { const raioKm = (registro.raioToleranciaUsado / 1000).toFixed(2); const raioMetros = registro.raioToleranciaUsado.toFixed(0); - const raioTexto = registro.raioToleranciaUsado >= 1000 - ? `${raioKm} km (${raioMetros} metros)` - : `${raioMetros} metros`; + const raioTexto = + registro.raioToleranciaUsado >= 1000 + ? `${raioKm} km (${raioMetros} metros)` + : `${raioMetros} metros`; geofencingData.push(['Raio Permitido', raioTexto]); - } else { + } else { geofencingData.push(['Raio Permitido', 'Não configurado']); } @@ -3030,15 +3287,17 @@ registro.raioToleranciaUsado !== null && registro.raioToleranciaUsado !== undefined ) { - const distanciaExcedente = registro.distanciaEnderecoEsperado - registro.raioToleranciaUsado; + const distanciaExcedente = + registro.distanciaEnderecoEsperado - registro.raioToleranciaUsado; const distanciaExcedenteKm = (distanciaExcedente / 1000).toFixed(2); const distanciaExcedenteMetros = distanciaExcedente.toFixed(0); - const excedenteTexto = distanciaExcedente >= 1000 - ? `${distanciaExcedenteKm} km além do permitido` - : `${distanciaExcedenteMetros} metros além do permitido`; + const excedenteTexto = + distanciaExcedente >= 1000 + ? `${distanciaExcedenteKm} km além do permitido` + : `${distanciaExcedenteMetros} metros além do permitido`; geofencingData.push(['Distância Excedente', excedenteTexto]); - } } + } geofencingData.push(['Status', statusTexto]); autoTable(doc, { @@ -3046,13 +3305,13 @@ head: [['Campo', 'Valor']], body: geofencingData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 10 }, columnStyles: { @@ -3078,7 +3337,8 @@ type JsPDFWithAutoTable9 = jsPDF & { lastAutoTable?: { finalY: number }; }; - const finalYGeofencing = (doc as JsPDFWithAutoTable9).lastAutoTable?.finalY ?? yPosition + 10; + const finalYGeofencing = + (doc as JsPDFWithAutoTable9).lastAutoTable?.finalY ?? yPosition + 10; yPosition = finalYGeofencing + 5; // Observação se fora do raio @@ -3099,7 +3359,11 @@ } else { doc.setFontSize(10); doc.setTextColor(100, 100, 100); - doc.text('Validação de localização permitida não configurada para este registro.', 15, yPosition); + doc.text( + 'Validação de localização permitida não configurada para este registro.', + 15, + yPosition + ); yPosition += 8; doc.setTextColor(0, 0, 0); } @@ -3137,7 +3401,10 @@ // Informações do Navegador if (registro.browser || registro.userAgent) { if (registro.browser) { - dadosTecnicosData.push(['Navegador', `${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}`]); + dadosTecnicosData.push([ + 'Navegador', + `${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}` + ]); } if (registro.engine) { dadosTecnicosData.push(['Engine', registro.engine]); @@ -3150,7 +3417,10 @@ // Informações do Sistema if (registro.sistemaOperacional || registro.arquitetura) { if (registro.sistemaOperacional) { - dadosTecnicosData.push(['Sistema Operacional', `${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}`]); + dadosTecnicosData.push([ + 'Sistema Operacional', + `${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}` + ]); } if (registro.arquitetura) { dadosTecnicosData.push(['Arquitetura', registro.arquitetura]); @@ -3175,7 +3445,11 @@ dadosTecnicosData.push(['Cores da Tela', registro.coresTela]); } if (registro.isMobile || registro.isTablet || registro.isDesktop) { - const tipoDispositivo = registro.isMobile ? 'Mobile' : registro.isTablet ? 'Tablet' : 'Desktop'; + const tipoDispositivo = registro.isMobile + ? 'Mobile' + : registro.isTablet + ? 'Tablet' + : 'Desktop'; dadosTecnicosData.push(['Categoria', tipoDispositivo]); } if (registro.idioma) { @@ -3195,13 +3469,13 @@ head: [['Campo', 'Valor']], body: dadosTecnicosData, theme: 'striped', - headStyles: { + headStyles: { fillColor: [60, 60, 60], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 10 }, - bodyStyles: { + bodyStyles: { fontSize: 9, textColor: [0, 0, 0] }, @@ -3216,7 +3490,8 @@ type JsPDFWithAutoTable10 = jsPDF & { lastAutoTable?: { finalY: number }; }; - const finalYTecnicos = (doc as JsPDFWithAutoTable10).lastAutoTable?.finalY ?? yPosition + 10; + const finalYTecnicos = + (doc as JsPDFWithAutoTable10).lastAutoTable?.finalY ?? yPosition + 10; yPosition = finalYTecnicos + 10; } @@ -3240,10 +3515,10 @@ if (!response.ok) { throw new Error('Erro ao carregar imagem'); } - + const blob = await response.blob(); const reader = new FileReader(); - + // Converter blob para base64 const base64 = await new Promise((resolve, reject) => { reader.onloadend = () => { @@ -3287,7 +3562,7 @@ // Centralizar imagem const xPosition = (doc.internal.pageSize.getWidth() - imgWidth) / 2; - + // Verificar se cabe na página atual if (yPosition + imgHeight > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); @@ -3309,19 +3584,24 @@ const pageCount = doc.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); - + // Linha decorativa no rodapé doc.setDrawColor(200, 200, 200); doc.setLineWidth(0.3); - doc.line(15, doc.internal.pageSize.getHeight() - 20, 195, doc.internal.pageSize.getHeight() - 20); - + doc.line( + 15, + doc.internal.pageSize.getHeight() - 20, + 195, + doc.internal.pageSize.getHeight() - 20 + ); + // Texto do rodapé doc.setFontSize(8); doc.setTextColor(100, 100, 100); doc.setFont('helvetica', 'normal'); - const dataGeracao = new Date().toLocaleDateString('pt-BR', { - day: '2-digit', - month: '2-digit', + const dataGeracao = new Date().toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' @@ -3350,27 +3630,34 @@ } -
+
-
+
-
- +
+
-

+

Registro de Pontos

- Gerencie e visualize os registros de ponto dos funcionários com informações detalhadas e relatórios + Gerencie e visualize os registros de ponto dos funcionários com informações detalhadas e + relatórios

{#if estatisticas} -
+

Total de Registros

{estatisticas.totalRegistros}

@@ -3379,7 +3666,9 @@

Funcionários

{estatisticas.totalFuncionarios}

-
+
{estatisticas.totalRegistros > 0 @@ -3395,16 +3684,18 @@ {#if estatisticas} -
+
-
+
-

Total de Registros

-

{estatisticas.totalRegistros}

+

Total de Registros

+

{estatisticas.totalRegistros}

-
+
@@ -3412,19 +3703,21 @@
-
+
-

Dentro do Prazo

+

Dentro do Prazo

{estatisticas.dentroDoPrazo}

-

+

{estatisticas.totalRegistros > 0 ? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) : 0}% do total

-
+
@@ -3432,19 +3725,21 @@
-
+
-

Fora do Prazo

+

Fora do Prazo

{estatisticas.foraDoPrazo}

-

+

{estatisticas.totalRegistros > 0 ? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) : 0}% do total

-
+
@@ -3452,17 +3747,19 @@
-
+
-

Funcionários

+

Funcionários

{estatisticas.totalFuncionarios}

-

+

{estatisticas.funcionariosDentroPrazo} dentro, {estatisticas.funcionariosForaPrazo} fora

-
+
@@ -3472,62 +3769,66 @@ {/if} -
-
-
-

-
- -
- Visão Geral das Estatísticas -

-
-
+
+
+
+

+
+ +
+ Visão Geral das Estatísticas +

+
+
{#if estatisticasQuery === undefined || estatisticasQuery?.isLoading} -
+
Carregando estatísticas...
{:else if estatisticasQuery?.error} -
+

Erro ao carregar estatísticas

-
{estatisticasQuery.error?.message || String(estatisticasQuery.error) || 'Erro desconhecido'}
+
+ {estatisticasQuery.error?.message || + String(estatisticasQuery.error) || + 'Erro desconhecido'} +
{:else if !estatisticas || !chartData} -
+
- +

Nenhuma estatística disponível

{:else} - + {/if} -
+
-
+
-
+
-
- +
+
-

Filtros de Busca

+

Filtros de Busca

-
+
@@ -3565,7 +3866,7 @@ id="data-fim" type="date" bind:value={dataFim} - class="input input-bordered input-primary focus:input-primary focus:ring-2 focus:ring-primary/20" + class="input input-bordered input-primary focus:input-primary focus:ring-primary/20 focus:ring-2" />
@@ -3576,7 +3877,7 @@ @@ -3607,7 +3908,7 @@
- + {#if statusFiltro !== 'todos' || localizacaoFiltro !== 'todos'} -
-

- {registrosFiltrados.length} registro(s) encontrado(s) com os filtros aplicados +

+

+ {registrosFiltrados.length} registro(s) encontrado(s) com os filtros + aplicados {#if registros.length !== registrosFiltrados.length} - + (de {registros.length} total) {/if} @@ -3633,19 +3935,19 @@

-
+
-
+
-
- +
+
-

Registros de Ponto

+

Registros de Ponto

- + {#if funcionarioIdFiltro || dataInicio || dataFim} -
+
{#if funcionarioIdFiltro && funcionarioSelecionadoNome}
@@ -3669,44 +3971,60 @@
{#if registrosQuery === undefined || registrosQuery?.isLoading} -
+
Carregando registros... - Aguarde um momento + Aguarde um momento
{:else if registrosQuery?.error} -
+

Erro ao carregar registros

-
{registrosQuery.error?.message || String(registrosQuery.error) || 'Erro desconhecido'}
+
+ {registrosQuery.error?.message || String(registrosQuery.error) || 'Erro desconhecido'} +
{:else if !registrosQuery?.data} -
+
Aguardando dados da consulta...
{:else if registros.length === 0} -
- +
+
-

Nenhum registro encontrado

-
-

Período: {formatarDataDDMMAAAA(dataInicio)} até {formatarDataDDMMAAAA(dataFim)}

+

Nenhum registro encontrado

+
+

+ Período: {formatarDataDDMMAAAA(dataInicio)} até {formatarDataDDMMAAAA(dataFim)} +

{#if funcionarioIdFiltro && funcionarioSelecionadoNome} -

Funcionário: {funcionarioSelecionadoNome}

+

+ Funcionário: {funcionarioSelecionadoNome} +

{/if} -

Tente ajustar os filtros para encontrar registros.

+

+ Tente ajustar os filtros para encontrar registros. +

{:else if registrosAgrupados.length === 0} -
- +
+

Registros encontrados, mas não foi possível agrupá-los

-
+
Total de registros: {registros.length}
@@ -3714,89 +4032,133 @@ {:else}
{#each registrosAgrupados as grupo} -
+
-
-
+
+
-
-
- +
+
+
-

+

{grupo.funcionario?.nome || 'Funcionário não encontrado'}

{#if grupo.funcionario?.matricula} -

- Matrícula: {grupo.funcionario.matricula} +

+ Matrícula: + {grupo.funcionario.matricula}

{/if}
{#if grupo.funcionario?.descricaoCargo} -

+

{grupo.funcionario.descricaoCargo}

{/if}
- -
- - {#key grupo.funcionarioId} - {@const bancoHorasQuery = useQuery( - api.pontos.obterBancoHorasFuncionario, - { funcionarioId: grupo.funcionarioId } - )} - {@const bancoHoras = bancoHorasQuery?.data} - {@const saldoAcumulado = bancoHoras?.saldoAcumuladoMinutos ?? 0} - {@const saldoPositivo = saldoAcumulado >= 0} - - {#if bancoHoras} -
-
-
- {#if saldoPositivo} - - {:else} - - {/if} -
-
-

Banco de Horas

-

- {formatarSaldoHoras(saldoAcumulado)} -

+ +
+ + {#key grupo.funcionarioId} + {@const bancoHorasQuery = useQuery(api.pontos.obterBancoHorasFuncionario, { + funcionarioId: grupo.funcionarioId + })} + {@const bancoHoras = bancoHorasQuery?.data} + {@const saldoAcumulado = bancoHoras?.saldoAcumuladoMinutos ?? 0} + {@const saldoPositivo = saldoAcumulado >= 0} + + {#if bancoHoras} +
+
+
+ {#if saldoPositivo} + + {:else} + + {/if} +
+
+

Banco de Horas

+

+ {formatarSaldoHoras(saldoAcumulado)} +

+
-
- {/if} - {/key} - - -
+ {/if} + {/key} + + +
-
- - +
+
+ - - - - - - - - + + + + + + + + @@ -3804,27 +4166,38 @@ {@const totalRegistros = grupoData.registros.length} {@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)} {@const saldosParciais = calcularSaldosParciais(grupoData.registros)} - {@const isUltimoDia = dataIndex === Object.values(grupo.registrosPorData).length - 1} + {@const isUltimoDia = + dataIndex === Object.values(grupo.registrosPorData).length - 1} {#each grupoData.registros as registro, index} {@const saldoParcial = saldosParciais.get(index)} - - + + - + + + {/each} + +
DataTipoHorárioSaldo ParcialSaldo DiárioLocalizaçãoStatusAçõesDataTipoHorárioSaldo ParcialSaldo DiárioLocalizaçãoStatusAções
{dataFormatada}
{dataFormatada} - - {config - ? getTipoRegistroLabel(registro.tipo, { - nomeEntrada: config.nomeEntrada, - nomeSaidaAlmoco: config.nomeSaidaAlmoco, - nomeRetornoAlmoco: config.nomeRetornoAlmoco, - nomeSaida: config.nomeSaida, - }) - : getTipoRegistroLabel(registro.tipo)} + + {config + ? getTipoRegistroLabel(registro.tipo, { + nomeEntrada: config.nomeEntrada, + nomeSaidaAlmoco: config.nomeSaidaAlmoco, + nomeRetornoAlmoco: config.nomeRetornoAlmoco, + nomeSaida: config.nomeSaida + }) + : getTipoRegistroLabel(registro.tipo)} {formatarHoraPonto(registro.hora, registro.minuto)}{formatarHoraPonto(registro.hora, registro.minuto)} {#if saldoParcial} - + Par {saldoParcial.parNumero}: +{saldoParcial.horas}h {saldoParcial.minutos}min {:else} @@ -3834,9 +4207,12 @@ {#if index === 0} {#if grupoData.saldoDiarioComparativo} - + {:else if grupoData.saldoDiario} - + {:else} - {/if} @@ -3847,14 +4223,16 @@ {registro.dentroDoPrazo ? '✓ Dentro do Prazo' : '✗ Fora do Prazo'} - +
{#if registroDetalhesQuery === undefined || registroDetalhesQuery?.isLoading}
@@ -3911,7 +4300,11 @@

Erro ao carregar detalhes

-
{registroDetalhesQuery.error?.message || String(registroDetalhesQuery.error) || 'Erro desconhecido'}
+
+ {registroDetalhesQuery.error?.message || + String(registroDetalhesQuery.error) || + 'Erro desconhecido'} +
{:else if !registroDetalhes} @@ -3923,7 +4316,7 @@
-

Informações do Registro

+

Informações do Registro

{#if registroDetalhes.funcionario}

Funcionário: {registroDetalhes.funcionario.nome}

{#if registroDetalhes.funcionario.matricula} @@ -3931,8 +4324,17 @@ {/if} {/if}

Data: {formatarDataDDMMAAAA(registroDetalhes.data)}

-

Horário: {formatarHoraPonto(registroDetalhes.hora, registroDetalhes.minuto)}

-

Status: {registroDetalhes.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}

+

+ Horário: + {formatarHoraPonto(registroDetalhes.hora, registroDetalhes.minuto)} +

+

+ Status: + {registroDetalhes.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'} +

@@ -3940,20 +4342,31 @@ {#if registroDetalhes.latitude !== undefined && registroDetalhes.longitude !== undefined}
-

Localização GPS

+

Localização GPS

Latitude: {registroDetalhes.latitude.toFixed(6)}

Longitude: {registroDetalhes.longitude.toFixed(6)}

{#if registroDetalhes.precisao !== undefined}

Precisão: {registroDetalhes.precisao.toFixed(2)}m

{/if} {#if registroDetalhes.endereco || registroDetalhes.cidade} -

Endereço: {registroDetalhes.endereco || ''} {registroDetalhes.cidade ? `, ${registroDetalhes.cidade}` : ''} {registroDetalhes.estado ? ` - ${registroDetalhes.estado}` : ''}

+

+ Endereço: + {registroDetalhes.endereco || ''} + {registroDetalhes.cidade ? `, ${registroDetalhes.cidade}` : ''} + {registroDetalhes.estado ? ` - ${registroDetalhes.estado}` : ''} +

{/if} {#if registroDetalhes.confiabilidadeGPS !== undefined} -

Confiabilidade GPS: {(registroDetalhes.confiabilidadeGPS * 100).toFixed(1)}%

+

+ Confiabilidade GPS: + {(registroDetalhes.confiabilidadeGPS * 100).toFixed(1)}% +

{/if} {#if registroDetalhes.scoreConfiancaBackend !== undefined} -

Score de Confiança: {(registroDetalhes.scoreConfiancaBackend * 100).toFixed(1)}%

+

+ Score de Confiança: + {(registroDetalhes.scoreConfiancaBackend * 100).toFixed(1)}% +

{/if}
@@ -3963,31 +4376,58 @@ {#if registroDetalhes.acelerometroX !== undefined || registroDetalhes.sensorDisponivel !== undefined}
-

Dados de Sensores

+

Dados de Sensores

{#if registroDetalhes.sensorDisponivel === false && registroDetalhes.isDesktop !== true} -

Sensor: Não disponível neste dispositivo

+

+ Sensor: Não disponível neste dispositivo +

{:else if registroDetalhes.permissaoSensorNegada === true}

Sensor: Permissão negada

{:else if registroDetalhes.acelerometroX !== undefined}

Sensor: Disponível

-

Acelerômetro X: {registroDetalhes.acelerometroX.toFixed(3)} m/s²

+

+ Acelerômetro X: + {registroDetalhes.acelerometroX.toFixed(3)} m/s² +

{#if registroDetalhes.acelerometroY !== undefined} -

Acelerômetro Y: {registroDetalhes.acelerometroY.toFixed(3)} m/s²

+

+ Acelerômetro Y: + {registroDetalhes.acelerometroY.toFixed(3)} m/s² +

{/if} {#if registroDetalhes.acelerometroZ !== undefined} -

Acelerômetro Z: {registroDetalhes.acelerometroZ.toFixed(3)} m/s²

+

+ Acelerômetro Z: + {registroDetalhes.acelerometroZ.toFixed(3)} m/s² +

{/if} {#if registroDetalhes.magnitudeMovimento !== undefined} -

Magnitude: {registroDetalhes.magnitudeMovimento.toFixed(3)} m/s²

+

+ Magnitude: + {registroDetalhes.magnitudeMovimento.toFixed(3)} m/s² +

{/if} {#if registroDetalhes.movimentoDetectado !== undefined} -

Movimento Detectado: {registroDetalhes.movimentoDetectado ? 'Sim' : 'Não'}

+

+ Movimento Detectado: + {registroDetalhes.movimentoDetectado ? 'Sim' : 'Não'} +

{/if} {#if registroDetalhes.variacaoAcelerometro !== undefined} -

Variação: {registroDetalhes.variacaoAcelerometro.toFixed(6)}

+

+ Variação: + {registroDetalhes.variacaoAcelerometro.toFixed(6)} +

{/if} {:else if registroDetalhes.isDesktop === true} -

Sensor: Não disponível em desktop (normal)

+

+ Sensor: Não disponível em desktop (normal) +

{/if}
@@ -3995,9 +4435,12 @@ {/if}
-
+
{#if registroDetalhes} - @@ -4008,4 +4451,3 @@ {/if} - diff --git a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte index 491ed3a..64e998d 100644 --- a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte @@ -1,1593 +1,1780 @@ -
- - +
+ + - -
-
-
- - - -
-
-

Central de Chamados

-

Monitore tickets, configure SLA, atribua responsáveis e acompanhe alertas de prazos

-
-
-
+ +
+
+
+ + + +
+
+

Central de Chamados

+

+ Monitore tickets, configure SLA, atribua responsáveis e acompanhe alertas de prazos +

+
+
+
- -
- - - -
+ +
+ + + +
-
- - {#if abaAtiva === 'dashboard'} -
-
-

Total de chamados

-

{estatisticas.total ?? 0}

-
-
-

Abertos

-

{estatisticas.abertos ?? 0}

-
-
-

Em andamento

-

{estatisticas.emAndamento ?? 0}

-
-
-

Vencidos/Cancelados

-

{estatisticas.vencidos ?? 0}

-
-
+
+ + {#if abaAtiva === 'dashboard'} +
+
+

Total de chamados

+

{estatisticas.total ?? 0}

+
+
+

Abertos

+

{estatisticas.abertos ?? 0}

+
+
+

Em andamento

+

{estatisticas.emAndamento ?? 0}

+
+
+

Vencidos/Cancelados

+

{estatisticas.vencidos ?? 0}

+
+
- -
-
-
-

Performance de SLA

-

Monitoramento em tempo real do cumprimento de SLA por prioridade

-
- {#if dadosSlaGraficoQuery !== undefined && dadosSlaGraficoQuery !== null} - {@const dadosSla = typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery - ? dadosSlaGraficoQuery.data - : (typeof dadosSlaGraficoQuery === 'object' && 'taxaCumprimento' in dadosSlaGraficoQuery - ? dadosSlaGraficoQuery - : null)} - {#if dadosSla} -
-
-

Taxa de Cumprimento

-

- {dadosSla.taxaCumprimento}% -

-
-
-

Última atualização

-

- {new Date(dadosSla.atualizadoEm).toLocaleTimeString('pt-BR')} -

-
-
- {/if} - {/if} -
- - {#if dadosSlaGraficoQuery === undefined || dadosSlaGraficoQuery === null} -
- -
- {:else} - {@const dadosSla = typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery - ? dadosSlaGraficoQuery.data - : (typeof dadosSlaGraficoQuery === 'object' && 'taxaCumprimento' in dadosSlaGraficoQuery - ? dadosSlaGraficoQuery - : null)} - {#if dadosSla} -
-
-

Dentro do Prazo

-

{dadosSla.statusSla.dentroPrazo}

-
-
-

Próximo Vencimento

-

{dadosSla.statusSla.proximoVencimento}

-
-
-

Vencidos

-

{dadosSla.statusSla.vencido}

-
-
-

Sem Prazo

-

{dadosSla.statusSla.semPrazo}

-
-
- - {:else} -
-

Carregando dados de SLA...

-
- {/if} - {/if} -
- {/if} + +
+
+
+

Performance de SLA

+

+ Monitoramento em tempo real do cumprimento de SLA por prioridade +

+
+ {#if dadosSlaGraficoQuery !== undefined && dadosSlaGraficoQuery !== null} + {@const dadosSla = + typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery.data + : typeof dadosSlaGraficoQuery === 'object' && + 'taxaCumprimento' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery + : null} + {#if dadosSla} +
+
+

Taxa de Cumprimento

+

+ {dadosSla.taxaCumprimento}% +

+
+
+

Última atualização

+

+ {new Date(dadosSla.atualizadoEm).toLocaleTimeString('pt-BR')} +

+
+
+ {/if} + {/if} +
- - {#if abaAtiva === 'chamados'} -
-
-
-

Painel de chamados

-

- Filtros por status, responsável e setor. -

-
-
- - - -
-
+ {#if dadosSlaGraficoQuery === undefined || dadosSlaGraficoQuery === null} +
+ +
+ {:else} + {@const dadosSla = + typeof dadosSlaGraficoQuery === 'object' && 'data' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery.data + : typeof dadosSlaGraficoQuery === 'object' && + 'taxaCumprimento' in dadosSlaGraficoQuery + ? dadosSlaGraficoQuery + : null} + {#if dadosSla} +
+
+

Dentro do Prazo

+

{dadosSla.statusSla.dentroPrazo}

+
+
+

Próximo Vencimento

+

{dadosSla.statusSla.proximoVencimento}

+
+
+

Vencidos

+

{dadosSla.statusSla.vencido}

+
+
+

Sem Prazo

+

{dadosSla.statusSla.semPrazo}

+
+
+ + {:else} +
+

Carregando dados de SLA...

+
+ {/if} + {/if} +
+ {/if} -
- - - - - - - - - - - - - {#if carregandoChamados} - - - - {:else if tickets.length === 0} - - - - {:else} - {#each tickets as ticket (ticket._id)} - selecionarTicket(ticket._id)} - > - - - - - - - - {/each} - {/if} - -
TicketTipoStatusResponsávelPrioridadePrazo
-
- -
-
- Nenhum chamado encontrado. -
-
{ticket.numero}
-
{ticket.solicitanteNome}
-
{ticket.tipo} - {getStatusLabel(ticket.status)} - {(ticket as Ticket & { responsavelNome?: string }).responsavelNome ?? ticket.setorResponsavel ?? "—"}{ticket.prioridade} - {ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : "--"} -
-
-
+ + {#if abaAtiva === 'chamados'} +
+
+
+

Painel de chamados

+

Filtros por status, responsável e setor.

+
+
+ + + +
+
- - {#if !detalheSelecionado} -
-
- - - -

Nenhum chamado selecionado

-

- Selecione um chamado na tabela acima para visualizar detalhes e realizar ações. -

-
-
- {:else} - -
- -
-
-
-
-
- {detalheSelecionado.numero} -
- - {getStatusLabel(detalheSelecionado.status)} - -
-

{detalheSelecionado.titulo}

-
-
- - - - Solicitante: - {detalheSelecionado.solicitanteNome} -
- {#if detalheSelecionado.prazoConclusao} -
- - - - Prazo: - {prazoRestante(detalheSelecionado.prazoConclusao) ?? "--"} -
- {/if} -
-
-
-
+
+ + + + + + + + + + + + + {#if carregandoChamados} + + + + {:else if tickets.length === 0} + + + + {:else} + {#each tickets as ticket (ticket._id)} + selecionarTicket(ticket._id)} + > + + + + + + + + {/each} + {/if} + +
TicketTipoStatusResponsávelPrioridadePrazo
+
+ +
+
+ Nenhum chamado encontrado. +
+
{ticket.numero}
+
{ticket.solicitanteNome}
+
{ticket.tipo} + {getStatusLabel(ticket.status)} + {(ticket as Ticket & { responsavelNome?: string }).responsavelNome ?? + ticket.setorResponsavel ?? + '—'}{ticket.prioridade} + {ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : '--'} +
+
+
- -
- -
-
- - - -

Detalhes do chamado

-
-
-
-

Descrição

-

- {detalheSelecionado.descricao} -

-
-
-
-

Prazo de resposta

-

- {prazoRestante(detalheSelecionado.prazoResposta) ?? "--"} -

-
-
-

Prazo de conclusão

-

- {prazoRestante(detalheSelecionado.prazoConclusao) ?? "--"} -

-
-
-
-

Histórico e Timeline

- -
-
-
+ + {#if !detalheSelecionado} +
+
+ + + +

Nenhum chamado selecionado

+

+ Selecione um chamado na tabela acima para visualizar detalhes e realizar ações. +

+
+
+ {:else} + +
+ +
+
+
+
+
+ {detalheSelecionado.numero} +
+ + {getStatusLabel(detalheSelecionado.status)} + +
+

+ {detalheSelecionado.titulo} +

+
+
+ + + + Solicitante: + {detalheSelecionado.solicitanteNome} +
+ {#if detalheSelecionado.prazoConclusao} +
+ + + + Prazo: + {prazoRestante(detalheSelecionado.prazoConclusao) ?? '--'} +
+ {/if} +
+
+
+
- -
-
- - - -

Responder chamado

-
-
-
- - -
+ +
+ +
+
+ + + +

Detalhes do chamado

+
+
+
+

Descrição

+

+ {detalheSelecionado.descricao} +

+
+
+
+

Prazo de resposta

+

+ {prazoRestante(detalheSelecionado.prazoResposta) ?? '--'} +

+
+
+

Prazo de conclusão

+

+ {prazoRestante(detalheSelecionado.prazoConclusao) ?? '--'} +

+
+
+
+

Histórico e Timeline

+ +
+
+
-
- - - {#if respostaAnexoNome} -
- {respostaAnexoNome} - -
- {/if} -
+ +
+
+ + + +

Responder chamado

+
+
+
+ + +
- {#if respostaFeedback} -
- {respostaFeedback} -
- {/if} +
+ + + {#if respostaAnexoNome} +
+ {respostaAnexoNome} + +
+ {/if} +
-
- - -
-
-
+ {#if respostaFeedback} +
+ {respostaFeedback} +
+ {/if} - -
-
- - - -

Atribuir responsável

-
-
- - - {#if assignFeedback} -
- {assignFeedback} -
- {/if} - -
-
+
+ + +
+
+
- -
-
- - - -
-

Prorrogar prazo

-

Recurso exclusivo para a equipe de TI

-
-
-
-
- - -
-
- - -
-
- - -
- {#if prorrogacaoFeedback} -
- {prorrogacaoFeedback} -
- {/if} - -
-
-
-
- {/if} - {/if} + +
+
+ + + +

Atribuir responsável

+
+
+ + + {#if assignFeedback} +
+ {assignFeedback} +
+ {/if} + +
+
- - {#if abaAtiva === 'sla'} - -
-
-
-

SLAs Configurados

-

Visualize todos os SLAs ativos com seus tempos e configurações

-
-
+ +
+
+ + + +
+

Prorrogar prazo

+

Recurso exclusivo para a equipe de TI

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ {#if prorrogacaoFeedback} +
+ {prorrogacaoFeedback} +
+ {/if} + +
+
+
+
+ {/if} + {/if} - {#if slaConfigsQuery === undefined || slaConfigsQuery === null || ('data' in slaConfigsQuery && slaConfigsQuery.data === undefined)} -
- -
- {:else} - {@const slaConfigs = ('data' in slaConfigsQuery && slaConfigsQuery.data !== undefined) - ? (Array.isArray(slaConfigsQuery.data) ? slaConfigsQuery.data : []) - : (Array.isArray(slaConfigsQuery) ? slaConfigsQuery : [])} - {@const slaConfigsAtivos = slaConfigs.filter((s: SlaConfig) => s.ativo)} - {@const slaConfigsPorPrioridadeCount = { - baixa: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'baixa').length, - media: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'media').length, - alta: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'alta').length, - critica: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'critica').length, - }} - - {#if slaConfigsAtivos.length === 0} -
-

Nenhum SLA configurado

-

Configure SLAs para cada prioridade na seção abaixo

-
- {:else} - -
-
-
{slaConfigsAtivos.length}
-
Total de SLAs
-
-
-
{slaConfigsPorPrioridadeCount.baixa}
-
Prioridade Baixa
-
-
-
{slaConfigsPorPrioridadeCount.media}
-
Prioridade Média
-
-
-
{slaConfigsPorPrioridadeCount.alta}
-
Prioridade Alta
-
-
-
{slaConfigsPorPrioridadeCount.critica}
-
Prioridade Crítica
-
-
+ + {#if abaAtiva === 'sla'} + +
+
+
+

SLAs Configurados

+

+ Visualize todos os SLAs ativos com seus tempos e configurações +

+
+
- -
- - - - - - - - - - - - - - - {#each slaConfigsAtivos as sla (sla._id)} - - - - - - - - - - - {/each} - -
NomePrioridadeTempo de RespostaTempo de ConclusãoAuto-encerramentoAlerta AntecedênciaStatusAções
-
{sla.nome}
- {#if sla.descricao} -
{sla.descricao}
- {/if} -
- - {sla.prioridade} - - -
- {sla.tempoRespostaHoras}h - {#if sla.tempoRespostaHoras >= 24} - - ({Math.floor(sla.tempoRespostaHoras / 24)}d {sla.tempoRespostaHoras % 24}h) - - {/if} -
-
-
- {sla.tempoConclusaoHoras}h - {#if sla.tempoConclusaoHoras >= 24} - - ({Math.floor(sla.tempoConclusaoHoras / 24)}d {sla.tempoConclusaoHoras % 24}h) - - {/if} -
-
- {#if sla.tempoEncerramentoHoras} -
- {sla.tempoEncerramentoHoras}h - {#if sla.tempoEncerramentoHoras >= 24} - - ({Math.floor(sla.tempoEncerramentoHoras / 24)}d {sla.tempoEncerramentoHoras % 24}h) - - {/if} -
- {:else} - Não configurado - {/if} -
-
- {sla.alertaAntecedenciaHoras}h - antes -
-
- - - Ativo - - -
- - -
-
-
- {/if} - {/if} -
+ {#if slaConfigsQuery === undefined || slaConfigsQuery === null || ('data' in slaConfigsQuery && slaConfigsQuery.data === undefined)} +
+ +
+ {:else} + {@const slaConfigs = + 'data' in slaConfigsQuery && slaConfigsQuery.data !== undefined + ? Array.isArray(slaConfigsQuery.data) + ? slaConfigsQuery.data + : [] + : Array.isArray(slaConfigsQuery) + ? slaConfigsQuery + : []} + {@const slaConfigsAtivos = slaConfigs.filter((s: SlaConfig) => s.ativo)} + {@const slaConfigsPorPrioridadeCount = { + baixa: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'baixa').length, + media: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'media').length, + alta: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'alta').length, + critica: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'critica').length + }} - -
-
-

Configuração de SLA por Prioridade

-

Configure SLAs separados para cada nível de prioridade

-
+ {#if slaConfigsAtivos.length === 0} +
+

Nenhum SLA configurado

+

+ Configure SLAs para cada prioridade na seção abaixo +

+
+ {:else} + +
+
+
{slaConfigsAtivos.length}
+
Total de SLAs
+
+
+
+ {slaConfigsPorPrioridadeCount.baixa} +
+
Prioridade Baixa
+
+
+
{slaConfigsPorPrioridadeCount.media}
+
Prioridade Média
+
+
+
+ {slaConfigsPorPrioridadeCount.alta} +
+
Prioridade Alta
+
+
+
+ {slaConfigsPorPrioridadeCount.critica} +
+
Prioridade Crítica
+
+
- -
- {#each ["baixa", "media", "alta", "critica"] as prioridade} - {@const slaAtual = slaConfigsPorPrioridade[prioridade]} -
-
-

{prioridade}

- {#if slaAtual} - Configurado - {:else} - Não configurado - {/if} -
- {#if slaAtual} -
-
- Resposta: - {slaAtual.tempoRespostaHoras}h -
-
- Conclusão: - {slaAtual.tempoConclusaoHoras}h -
- {#if slaAtual.tempoEncerramentoHoras} -
- Auto-encerramento: - {slaAtual.tempoEncerramentoHoras}h -
- {/if} -
- Alerta: - {slaAtual.alertaAntecedenciaHoras}h antes -
-
-
- - -
- {:else} - - {/if} -
- {/each} -
+ +
+ + + + + + + + + + + + + + + {#each slaConfigsAtivos as sla (sla._id)} + + + + + + + + + + + {/each} + +
NomePrioridadeTempo de RespostaTempo de ConclusãoAuto-encerramentoAlerta AntecedênciaStatusAções
+
{sla.nome}
+ {#if sla.descricao} +
+ {sla.descricao} +
+ {/if} +
+ + {sla.prioridade} + + +
+ {sla.tempoRespostaHoras}h + {#if sla.tempoRespostaHoras >= 24} + + ({Math.floor(sla.tempoRespostaHoras / 24)}d {sla.tempoRespostaHoras % + 24}h) + + {/if} +
+
+
+ {sla.tempoConclusaoHoras}h + {#if sla.tempoConclusaoHoras >= 24} + + ({Math.floor(sla.tempoConclusaoHoras / 24)}d {sla.tempoConclusaoHoras % + 24}h) + + {/if} +
+
+ {#if sla.tempoEncerramentoHoras} +
+ {sla.tempoEncerramentoHoras}h + {#if sla.tempoEncerramentoHoras >= 24} + + ({Math.floor(sla.tempoEncerramentoHoras / 24)}d {sla.tempoEncerramentoHoras % + 24}h) + + {/if} +
+ {:else} + Não configurado + {/if} +
+
+ {sla.alertaAntecedenciaHoras}h + antes +
+
+ + + Ativo + + +
+ + +
+
+
+ {/if} + {/if} +
- -
-

- {slaForm.slaId ? "Editar" : "Novo"} SLA - Prioridade {slaForm.prioridade.charAt(0).toUpperCase() + slaForm.prioridade.slice(1)} -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- {#if slaFeedback} -
-

{slaFeedback}

-
- {/if} -
- - {#if slaForm.slaId} - - {/if} -
-
- - {/if} + +
+
+

+ Configuração de SLA por Prioridade +

+

+ Configure SLAs separados para cada nível de prioridade +

+
- - {#if slaParaExcluir} - - {/if} + +
+ {#each ['baixa', 'media', 'alta', 'critica'] as prioridade} + {@const slaAtual = slaConfigsPorPrioridade[prioridade]} +
+
+

{prioridade}

+ {#if slaAtual} + Configurado + {:else} + Não configurado + {/if} +
+ {#if slaAtual} +
+
+ Resposta: + {slaAtual.tempoRespostaHoras}h +
+
+ Conclusão: + {slaAtual.tempoConclusaoHoras}h +
+ {#if slaAtual.tempoEncerramentoHoras} +
+ Auto-encerramento: + {slaAtual.tempoEncerramentoHoras}h +
+ {/if} +
+ Alerta: + {slaAtual.alertaAntecedenciaHoras}h antes +
+
+
+ + +
+ {:else} + + {/if} +
+ {/each} +
-
+ +
+

+ {slaForm.slaId ? 'Editar' : 'Novo'} SLA - Prioridade {slaForm.prioridade + .charAt(0) + .toUpperCase() + slaForm.prioridade.slice(1)} +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {#if slaFeedback} +
+

+ {slaFeedback} +

+
+ {/if} +
+ + {#if slaForm.slaId} + + {/if} +
+
+ + {/if} + + + {#if slaParaExcluir} + + {/if} +
- diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte index ffaa5e2..c78b1cc 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/+page.svelte @@ -1147,27 +1147,27 @@
- +
-
+
- - - -
-
-

Notificações e Mensagens

+ fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + + +
+
+

Notificações e Mensagens

Envie avisos importantes por chat e email HTML padronizado para os usuários do SGSE. @@ -1186,11 +1186,11 @@

{totalTemplates}
-
+
- + {#if mensagem}
-
- -
-
-
- - - -
-
-

Gerenciar Templates

-

Criar, editar e excluir templates de emails e mensagens

-
-
- - - - - Voltar - -
- - - {#if mensagem} -
- {mensagem.texto} - -
- {/if} - - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
-
-

Templates ({templatesFiltrados.length})

- +
+
+ +
+
+ - - {#if templatesFiltrados.length === 0} -
-

Nenhum template encontrado.

- {:else} -
- - - - - - - - - - - - - {#each templatesFiltrados as template (template._id)} +
+

Gerenciar Templates

+

+ Crie, edite e organize templates de chat (texto puro) e + email HTML padronizado usados em todas as notificações do SGSE. +

+
+ + + + + + Voltar para Notificações + + + + + {#if mensagem} +
+ {mensagem.texto} + +
+ {/if} + + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+

Templates ({templatesFiltrados.length})

+ + + + + Novo Template + +
+ + {#if templatesFiltrados.length === 0} +
+

Nenhum template encontrado.

+
+ {:else} +
+
CódigoNomeTipoCategoriaVariáveisAções
+ - - - - - - + + + + + + + + + {#each templatesFiltrados as template (template._id)} + + + + + + + - - {/each} - -
- {template.codigo} - -
{template.nome}
-
{template.titulo}
-
- {#if template.tipo === 'sistema'} - Sistema - {:else} - Customizado - {/if} - - {#if template.categoria} - {template.categoria} - {:else} - - - {/if} - - {#if template.variaveis && template.variaveis.length > 0} -
- {#each template.variaveis.slice(0, 3) as variavel} - {{variavel}} - {/each} - {#if template.variaveis.length > 3} - +{template.variaveis.length - 3} - {/if} -
- {:else} - - - {/if} -
-
- - - - - - {#if template.tipo === 'customizado'} -
CódigoNomeTipoCategoriaVariáveisAções
+ {template.codigo} + +
{template.nome}
+
{template.titulo}
+
+ {#if template.tipo === 'sistema'} + Sistema + {:else} + Customizado + {/if} + + {#if template.categoria} + {template.categoria} + {:else} + - + {/if} + + {#if template.variaveis && template.variaveis.length > 0} +
+ {#each template.variaveis.slice(0, 3) as variavel} + {{variavel}} + {/each} + {#if template.variaveis.length > 3} + +{template.variaveis.length - 3} + {/if} +
+ {:else} + - + {/if} +
+ -
-
- {/if} + + {#if template.tipo === 'customizado'} + + {/if} +
+
+
+ {/if} +
diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/[id]/+page.svelte index 0c456fa..2533506 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/[id]/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/[id]/+page.svelte @@ -110,63 +110,66 @@ } -
-
-
-
- - - -
-
-

Editar Template

-

- Atualize as informações do template selecionado. Templates de sistema não podem ser - editados. -

+
+
+
+
+
+ + + +
+
+

Editar Template

+

+ Ajuste o texto base usado em chat e na versão HTML de + email. Templates de sistema podem ter restrições de edição. +

+
+ Voltar
- Voltar -
- {#if !template} -
- Template não encontrado. -
- {:else} - {#if mensagem} -
- {mensagem.texto} - + {#if !template} +
+ Template não encontrado.
- {/if} + {:else} + {#if mensagem} +
+ {mensagem.texto} + +
+ {/if} -
-
+
+
-
- {/if} + {/if} +
diff --git a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte index ef98c0a..4c32f13 100644 --- a/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/notificacoes/templates/novo/+page.svelte @@ -79,57 +79,61 @@ } -
-
-
-
- - - -
-
-

Novo Template

-

- Crie um template de email ou mensagem para reutilizar no sistema. -

+
+
+
+
+
+ + + +
+
+

Novo Template

+

+ Defina o texto base que será usado em chat e na versão + HTML de email com o estilo padrão do SGSE. +

+
+ Voltar
- Voltar -
- {#if mensagem} -
- {mensagem.texto} - -
- {/if} + {mensagem.texto} + +
+ {/if} -
-
+
+