@@ -1742,10 +2862,18 @@
-
-
{periodo.funcionario?.nome.substring(0, 2).toUpperCase()}
+
+ {#if periodo.funcionario && 'fotoPerfilUrl' in periodo.funcionario && periodo.funcionario.fotoPerfilUrl}
+
+ {:else}
+
{periodo.funcionario?.nome?.substring(0, 2).toUpperCase() || '??'}
+ {/if}
@@ -1816,29 +2944,45 @@
{/if}
-
-
- {#if isLoading && !hasError}
-
- {#each Array.from({ length: 3 }, (_, i) => i) as index (index)}
-
-
+ {:else if abaAtiva === 'relatorios'}
+
+
+
+
+
- {/each}
+
+
Imprimir Relatórios
+
+ Configure os filtros e gere relatórios de programação de férias em PDF ou Excel
+
+
+
- {:else}
-
-
-
-
+
+
+
+
+
-
+
-
- Dias de Férias Programados por Mês
-
+
Filtros
- Somatório de dias planejados considerando a data de início de cada período
+ Configure os filtros para gerar o relatório personalizado
-
- {#if periodosPorMesAtivos.length === 0}
-
-
Sem dados registrados até o momento.
+
+ Limpar tudo
+
+
+
+
+
+
+
+
+ Período
+ {
+ dataInicioRelatorio = '';
+ dataFimRelatorio = '';
+ }}
+ disabled={!dataInicioRelatorio && !dataFimRelatorio}
+ >
+ Limpar
+
- {:else}
-
- {#if periodosPorMes.length > 1}
-
-
+
+ Data inicial
- Janela exibida
-
- {periodosPorMes[rangeInicioIndice]?.label ?? '-'}
- →
- {periodosPorMes[rangeFimIndice]?.label ?? '-'}
-
-
-
-
- Ajuste com o mouse os intervalos exibidos no gráfico.
-
+
- {/if}
- {/if}
+
+ Data final
+
+
+
+
+ Selecione o período para gerar o relatório de programação de férias.
+
+
+
+
+
+
+
+
+ Funcionário
+ (filtroFuncionarioRelatorio = '')}
+ disabled={filtroFuncionarioRelatorio.trim() === ''}
+ >
+ Limpar
+
+
+
+
+ Filtre por nome do funcionário para gerar relatório específico.
+
+
+
+
+
+
+
+
+ Matrícula
+ (filtroMatriculaRelatorio = '')}
+ disabled={filtroMatriculaRelatorio.trim() === ''}
+ >
+ Limpar
+
+
+
+
+ Filtre por matrícula do funcionário para gerar relatório específico.
+
+
+
+
+
+
+
+
+ Status
+ {#if filtroStatusRelatorio !== 'todos'}
+ (filtroStatusRelatorio = 'todos')}
+ >
+ Limpar
+
+ {/if}
+
+
+ Todos
+ Aguardando Aprovação
+ Aprovado
+ Reprovado
+ Data Ajustada
+ Cancelado RH
+
+
+ Filtre por status das solicitações de férias.
+
+
+
+
+
+
+
+
+ Mês de referência
+ (filtroMesRelatorio = '')}
+ disabled={filtroMesRelatorio === ''}
+ >
+ Limpar
+
+
+
+
+ Filtra as solicitações que possuem períodos ativos dentro do mês informado.
+
+
-
-
-
-
+
+
+
+
-
+
-
- Dias Totais Aprovados por Ano de Referência
-
+
Imprimir programação de Férias
- Volume agregado de dias e número de solicitações por ano
+ Escolha o formato desejado para gerar o relatório de programação de férias
-
- {#if solicitacoesPorAno.length === 0}
-
-
- Ainda não há solicitações registradas para exibição.
-
-
- {:else}
-
- {/if}
+
+
+
+ {#if gerandoRelatorio}
+
+ {:else}
+
+
+
+ {/if}
+ Imprimir PDF
+
+
+ {#if gerandoRelatorio}
+
+ {:else}
+
+
+
+ {/if}
+ Exportar Excel
+
+
+ Os relatórios serão gerados com base nos filtros selecionados acima. O PDF será aberto para impressão e o Excel será baixado automaticamente.
+
{/if}
+
+
+ {#if isLoading && !hasError}
+
+ {#each Array.from({ length: 3 }, (_, i) => i) as index (index)}
+
+ {/each}
+
+ {:else}
+
+
+ {/if}
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}
-