refactor: update ErrorModal and RegistroPonto components for improved UI and functionality

- Refactored ErrorModal to use a div-based layout with enhanced animations and accessibility features.
- Updated RegistroPonto to include a new loading state and improved modal handling for webcam capture.
- Enhanced styling for better visual consistency and user experience across modals and registration cards.
- Introduced comparative balance calculations in RegistroPonto for better visibility of time discrepancies.
This commit is contained in:
2025-11-23 13:13:24 -03:00
parent db2daacdad
commit 35e7c10ed0
5 changed files with 660 additions and 221 deletions

View File

@@ -7,6 +7,7 @@
import { formatarHoraPonto, getTipoRegistroLabel, formatarDataDDMMAAAA } from '$lib/utils/ponto';
import LocalizacaoIcon from '$lib/components/ponto/LocalizacaoIcon.svelte';
import SaldoDiarioBadge from '$lib/components/ponto/SaldoDiarioBadge.svelte';
import SaldoDiarioComparativoBadge from '$lib/components/ponto/SaldoDiarioComparativoBadge.svelte';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
@@ -278,6 +279,7 @@
// Agrupar registros por funcionário e data
const registrosAgrupados = $derived.by(() => {
const configData = config;
const agrupados: Record<
string,
{
@@ -289,6 +291,7 @@
data: string;
registros: Array<typeof registros[number]>;
saldoDiario?: { saldoMinutos: number; horas: number; minutos: number; positivo: boolean };
saldoDiarioComparativo?: { trabalhadoMinutos: number; esperadoMinutos: number; diferencaMinutos: number };
}
>;
}
@@ -380,13 +383,50 @@
return a.minuto - b.minuto;
});
// Usar saldo diário da query se disponível, senão calcular
const primeiroRegistro = grupoData.registros[0];
if (primeiroRegistro && 'saldoDiario' in primeiroRegistro && primeiroRegistro.saldoDiario) {
grupoData.saldoDiario = primeiroRegistro.saldoDiario;
// Calcular saldo diário comparativo usando a soma dos saldos parciais
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);
// Par 1: entrada -> saida_almoco
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;
// Calcular diferença em relação à carga horária diária total configurada
const diferencaMinutos = totalTrabalhado - cargaHorariaDiariaEsperadaMinutos;
// Armazenar saldo comparativo
grupoData.saldoDiarioComparativo = {
trabalhadoMinutos: totalTrabalhado,
esperadoMinutos: cargaHorariaDiariaEsperadaMinutos,
diferencaMinutos: diferencaMinutos
};
} else {
// Calcular saldo diário como diferença entre saída e entrada
grupoData.saldoDiario = calcularSaldoDiario(grupoData.registros);
// 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);
}
}
}
}
@@ -1463,6 +1503,27 @@
esperadoMinutos: saldoDiarioTotalEsperadoMinutos
};
// 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 [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
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 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<string>();
// Criar linhas da tabela
for (let i = 0; i < todosRegistros.length; i++) {
const reg = todosRegistros[i];
@@ -1496,23 +1557,63 @@
// Verificar se há saldo esperado (par incompleto)
const saldoEsperado = saldosEsperadosPorPar.get(i);
if (saldoEsperado) {
// Par incompleto: mostrar saldo comparativo em vermelho
linha._saldoVermelho = true;
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
// 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 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 / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: saldoEsperado.tamanhoPar
});
} else if (indexReal >= 0) {
const saldoPar = saldosComparativosPorPar.get(indexReal);
if (saldoPar) {
const sinalDiferenca = saldoPar.diferencaMinutos >= 0 ? '+' : '-';
// Aplicar cor vermelha se diferença for negativa
if (saldoPar.diferencaMinutos < 0) {
linha._saldoVermelho = true;
// Decrementar saldo acumulado apenas uma vez por par
const chavePar = `${reg.tipo}-${saldoPar.parIndex}`;
if (!paresProcessadosParaSaldo.has(chavePar)) {
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 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 / ${sinalDiferenca}${saldoPar.diferencaHoras}h ${saldoPar.diferencaMinutosResto}min`,
content: `${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: saldoPar.tamanhoPar
});
} else {
@@ -1537,54 +1638,122 @@
// Verificar se há saldo esperado para este par
const saldoEsperado = saldosEsperadosPorPar.get(i);
if (saldoEsperado) {
// Par incompleto ou completamente não marcado: mostrar saldo em vermelho
linha._saldoVermelho = true;
const sinalDiferenca = saldoEsperado.diferencaMinutos >= 0 ? '+' : '-';
// 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 {
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 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
}
// Se par completamente não marcado (trabalhado = 0), mostrar apenas diferença negativa
if (saldoEsperado.trabalhadoMinutos === 0) {
linha.push({
content: `+0h 0min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: saldoEsperado.tamanhoPar
});
} else {
linha.push({
content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`,
content: `${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: saldoEsperado.tamanhoPar
});
}
} else if (regsReais.length === 0) {
// Dia sem registros: calcular saldo esperado completo com diferença negativa
// Dia sem registros: calcular saldo esperado completo e decrementar saldo acumulado
if (reg.tipo === 'entrada') {
// Par 1 completo esperado
const [horaEntrada, minutoEntrada] = config.horarioEntrada.split(':').map(Number);
const [horaSaidaAlmoco, minutoSaidaAlmoco] = config.horarioSaidaAlmoco.split(':').map(Number);
const minutosEntrada = horaEntrada * 60 + minutoEntrada;
const minutosSaida = horaSaidaAlmoco * 60 + minutoSaidaAlmoco;
let saldoMinutos = minutosSaida - minutosEntrada;
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
const horas = Math.floor(saldoMinutos / 60);
const minutos = saldoMinutos % 60;
linha._saldoVermelho = true;
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
// 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 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 / -${horas}h ${minutos}min`,
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: 2 // entrada + saida_almoco
});
} else if (reg.tipo === 'retorno_almoco') {
// Par 2 completo esperado
const [horaRetornoAlmoco, minutoRetornoAlmoco] = config.horarioRetornoAlmoco.split(':').map(Number);
const [horaSaida, minutoSaida] = config.horarioSaida.split(':').map(Number);
const minutosEntrada = horaRetornoAlmoco * 60 + minutoRetornoAlmoco;
const minutosSaida = horaSaida * 60 + minutoSaida;
let saldoMinutos = minutosSaida - minutosEntrada;
if (saldoMinutos < 0) saldoMinutos += 24 * 60;
const horas = Math.floor(saldoMinutos / 60);
const minutos = saldoMinutos % 60;
linha._saldoVermelho = true;
// Para dia sem registros, mostrar 0h trabalhado e diferença negativa
// 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 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 / -${horas}h ${minutos}min`,
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: 2 // retorno_almoco + saida
});
} else {
@@ -1599,13 +1768,34 @@
);
if (saidaEsperadaExiste) {
// Par completamente não marcado: calcular saldo negativo
// Par completamente não marcado: calcular saldo negativo e decrementar saldo acumulado
const saldoEsperadoCompleto = saldosEsperadosPorPar.get(i);
if (saldoEsperadoCompleto) {
linha._saldoVermelho = true;
const sinalDiferenca = saldoEsperadoCompleto.diferencaMinutos >= 0 ? '+' : '-';
// Decrementar saldo acumulado apenas uma vez por par
const chavePar = `${reg.tipo}-nao-marcado-${i}`;
if (!paresProcessadosParaSaldo.has(chavePar)) {
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;
// 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 / ${sinalDiferenca}${saldoEsperadoCompleto.diferencaHoras}h ${saldoEsperadoCompleto.diferencaMinutosResto}min`,
content: `0h 0min parcial | Saldo: ${sinalSaldo}${saldoAcumuladoHoras}h ${saldoAcumuladoMinutosResto}min`,
rowSpan: saldoEsperadoCompleto.tamanhoPar
});
} else {
@@ -1664,11 +1854,22 @@
}
}
// Aplicar cor vermelha na coluna de saldo diário quando marcado
if (data.row.raw && (data.row.raw as any)._saldoVermelho) {
// Coluna de saldo diário (índice 3 se saldoDiario estiver ativo)
// Aplicar cor baseada no saldo acumulado
if (data.row.raw) {
const rowData = data.row.raw as any;
const indiceSaldoDiario = sections.saldoDiario ? 3 : -1;
if (data.column.index === indiceSaldoDiario) {
data.cell.styles.textColor = [200, 0, 0];
if (rowData._saldoNegativo) {
// Saldo negativo: cor vermelha
data.cell.styles.textColor = [200, 0, 0];
} else if (rowData._saldoPositivo) {
// Saldo positivo: cor verde
data.cell.styles.textColor = [0, 128, 0];
} else if (rowData._saldoVermelho) {
// Fallback para compatibilidade: cor vermelha
data.cell.styles.textColor = [200, 0, 0];
}
}
}
}
@@ -3467,23 +3668,23 @@
<div class="mb-6 pb-4 border-b border-base-300">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-3">
<div class="flex items-center gap-3 mb-2">
<div class="p-2 bg-primary/10 rounded-lg">
<Users class="h-5 w-5 text-primary" strokeWidth={2.5} />
</div>
<div>
<h3 class="font-bold text-xl text-base-content">
<h3 class="font-bold text-lg text-base-content">
{grupo.funcionario?.nome || 'Funcionário não encontrado'}
</h3>
{#if grupo.funcionario?.matricula}
<p class="text-sm text-base-content/70 mt-1">
Matrícula: <span class="font-semibold">{grupo.funcionario.matricula}</span>
<span class="font-medium">Matrícula:</span> <span class="font-semibold">{grupo.funcionario.matricula}</span>
</p>
{/if}
</div>
</div>
{#if grupo.funcionario?.descricaoCargo}
<p class="text-sm text-base-content/60 ml-11">
<p class="text-sm text-base-content/60 ml-11 font-medium">
{grupo.funcionario.descricaoCargo}
</p>
{/if}
@@ -3533,51 +3734,60 @@
</div>
<div class="overflow-x-auto max-h-[600px] overflow-y-auto border border-base-300 rounded-xl shadow-inner bg-base-100/50">
<table class="table table-zebra">
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300 to-base-200">
<table class="table table-zebra w-full">
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300/95 to-base-200/95 backdrop-blur-sm">
<tr>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Data</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Tipo</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Horário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Parcial</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Saldo Diário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Localização</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Status</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Ações</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Data</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Tipo</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Horário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Saldo Parcial</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Saldo Diário</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Localização</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Status</th>
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400 text-sm">Ações</th>
</tr>
</thead>
<tbody>
{#each Object.values(grupo.registrosPorData) as grupoData}
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex}
{@const totalRegistros = grupoData.registros.length}
{@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)}
{@const saldosParciais = calcularSaldosParciais(grupoData.registros)}
{@const isUltimoDia = dataIndex === Object.values(grupo.registrosPorData).length - 1}
{#each grupoData.registros as registro, index}
{@const saldoParcial = saldosParciais.get(index)}
<tr>
<td class="whitespace-nowrap">{dataFormatada}</td>
<tr class="hover:bg-base-200/50 transition-colors {dataIndex % 2 === 0 ? 'bg-base-100/40' : 'bg-base-50/60'} {!isUltimoDia && index === totalRegistros - 1 ? 'border-b-4 border-base-300' : ''}">
<td class="whitespace-nowrap font-semibold text-sm">{dataFormatada}</td>
<td class="whitespace-nowrap">
{config
? getTipoRegistroLabel(registro.tipo, {
nomeEntrada: config.nomeEntrada,
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
nomeSaida: config.nomeSaida,
})
: getTipoRegistroLabel(registro.tipo)}
<span class="badge badge-outline badge-sm font-medium text-xs">
{config
? getTipoRegistroLabel(registro.tipo, {
nomeEntrada: config.nomeEntrada,
nomeSaidaAlmoco: config.nomeSaidaAlmoco,
nomeRetornoAlmoco: config.nomeRetornoAlmoco,
nomeSaida: config.nomeSaida,
})
: getTipoRegistroLabel(registro.tipo)}
</span>
</td>
<td class="whitespace-nowrap">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
<td class="whitespace-nowrap font-mono text-sm font-medium">{formatarHoraPonto(registro.hora, registro.minuto)}</td>
<td class="whitespace-nowrap">
{#if saldoParcial}
<span class="badge badge-info badge-sm font-semibold">
<span class="badge badge-info badge-sm font-semibold shadow-sm text-xs">
Par {saldoParcial.parNumero}: +{saldoParcial.horas}h {saldoParcial.minutos}min
</span>
{:else}
<span class="text-base-content/40">-</span>
<span class="text-base-content/30 text-sm">-</span>
{/if}
</td>
{#if index === 0}
<td class="whitespace-nowrap" rowspan={totalRegistros}>
<SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
{#if grupoData.saldoDiarioComparativo}
<SaldoDiarioComparativoBadge saldo={grupoData.saldoDiarioComparativo} size="md" />
{:else if grupoData.saldoDiario}
<SaldoDiarioBadge saldo={grupoData.saldoDiario} size="md" />
{:else}
<span class="badge badge-ghost badge-lg">-</span>
{/if}
</td>
{/if}
<td class="whitespace-nowrap">
@@ -3585,19 +3795,19 @@
</td>
<td class="whitespace-nowrap">
<span
class="badge badge-lg font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
class="badge badge-sm font-semibold {registro.dentroDoPrazo ? 'badge-success shadow-sm' : 'badge-error shadow-sm'}"
>
{registro.dentroDoPrazo ? '✓ Dentro do Prazo' : '✗ Fora do Prazo'}
</span>
</td>
<td class="whitespace-nowrap">
<button
class="btn btn-sm btn-outline btn-primary gap-2 hover:btn-primary hover:shadow-md transition-all"
class="btn btn-sm btn-outline btn-primary gap-2 hover:btn-primary hover:shadow-md transition-all text-xs"
onclick={() => abrirModalDetalhes(registro._id)}
title="Ver Detalhes"
>
<FileText class="h-4 w-4" />
Detalhes
<FileText class="h-3 w-3" />
<span class="text-xs">Detalhes</span>
</button>
</td>
</tr>