Registro de Pontos

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

{#if estatisticas}

Total de Registros

{estatisticas.totalRegistros}

Funcionários

{estatisticas.totalFuncionarios}

{estatisticas.totalRegistros > 0 ? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) : 0}% dentro do prazo Ativo
{/if}
{#if estatisticas}

Total de Registros

{estatisticas.totalRegistros}

Dentro do Prazo

{estatisticas.dentroDoPrazo}

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

Fora do Prazo

{estatisticas.foraDoPrazo}

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

Funcionários

{estatisticas.totalFuncionarios}

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

{/if}

Visão Geral das Estatísticas

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

Erro ao carregar estatísticas

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

Nenhuma estatística disponível

{:else} {/if}

Registros de Ponto

Gerencie e visualize os registros de ponto dos funcionários

Filtros de Busca
{ const target = e.target as HTMLInputElement; const valorComMascara = maskDate(target.value); dataInicioExibicao = valorComMascara; // Converter para formato backend quando completo if (validateDate(valorComMascara)) { dataInicioInterno = formatarDataParaBackendWrapper(valorComMascara); // Atualizar o input date oculto const dateInput = document.getElementById( 'data-inicio-date' ) as HTMLInputElement; if (dateInput) { dateInput.value = dataInicioInterno; } } }} onblur={() => { // Validar e corrigir ao sair do campo if (dataInicioExibicao && !validateDate(dataInicioExibicao)) { toast.error('Data de início inválida. Use o formato dd/mm/aaaa'); dataInicioExibicao = formatarDataParaExibicao(dataInicioInterno); } else if (dataInicioExibicao && validateDate(dataInicioExibicao)) { dataInicioInterno = formatarDataParaBackendWrapper(dataInicioExibicao); } }} /> { const target = e.target as HTMLInputElement; if (target.value) { dataInicioInterno = target.value; dataInicioExibicao = formatarDataParaExibicao(target.value); } }} />
{ const target = e.target as HTMLInputElement; const valorComMascara = maskDate(target.value); dataFimExibicao = valorComMascara; // Converter para formato backend quando completo if (validateDate(valorComMascara)) { dataFimInterno = formatarDataParaBackendWrapper(valorComMascara); // Atualizar o input date oculto const dateInput = document.getElementById('data-fim-date') as HTMLInputElement; if (dateInput) { dateInput.value = dataFimInterno; } } }} onblur={() => { // Validar e corrigir ao sair do campo if (dataFimExibicao && !validateDate(dataFimExibicao)) { toast.error('Data de fim inválida. Use o formato dd/mm/aaaa'); dataFimExibicao = formatarDataParaExibicao(dataFimInterno); } else if (dataFimExibicao && validateDate(dataFimExibicao)) { dataFimInterno = formatarDataParaBackendWrapper(dataFimExibicao); } }} /> { const target = e.target as HTMLInputElement; if (target.value) { dataFimInterno = target.value; dataFimExibicao = formatarDataParaExibicao(target.value); } }} />
{#if funcionarioIdFiltro || dataInicio || dataFim || statusFiltro !== 'todos' || localizacaoFiltro !== 'todos'}
Filtros ativos: {#if funcionarioIdFiltro && funcionarioSelecionadoNome}
{funcionarioSelecionadoNome}
{/if} {#if dataInicio}
De: {formatarDataDDMMAAAA(dataInicio)}
{/if} {#if dataFim}
Até: {formatarDataDDMMAAAA(dataFim)}
{/if} {#if statusFiltro !== 'todos'}
Status: {statusFiltro === 'dentro' ? 'Dentro do Prazo' : 'Fora do Prazo'}
{/if} {#if localizacaoFiltro !== 'todos'}
Local: {localizacaoFiltro === 'dentro' ? 'Dentro do Raio' : 'Fora do Raio'}
{/if}
{registrosFiltrados.length} registro(s)
{/if}
{#if registrosQuery === undefined || registrosQuery?.isLoading}
Carregando registros... Aguarde um momento
{:else if registrosQuery?.error}

Erro ao carregar registros

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

Nenhum registro encontrado

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

{#if funcionarioIdFiltro && funcionarioSelecionadoNome}

Funcionário: {funcionarioSelecionadoNome}

{/if}

Tente ajustar os filtros para encontrar registros.

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

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

Total de registros: {registros.length}
{:else}
{#each registrosAgrupados as grupo (grupo.funcionarioId)}

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

{#if grupo.funcionario?.matricula}

Matrícula: {grupo.funcionario.matricula}

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

{grupo.funcionario.descricaoCargo}

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

Banco de Horas

{formatarSaldoHoras(saldoAcumulado)}

{/if} {/key}
{#each Object.values(grupo.registrosPorData) as grupoData, dataIndex (grupoData.data)} {@const totalRegistros = grupoData.registros.length} {@const dataFormatada = formatarDataDDMMAAAA(grupoData.data)} {@const saldosParciais = calcularSaldosParciais( grupoData.registros.map((r) => ({ tipo: r.tipo, hora: r.hora, minuto: r.minuto, _id: r._id })) )} {@const isUltimoDia = dataIndex === Object.values(grupo.registrosPorData).length - 1} {#each grupoData.registros as registro, index (registro._id)} {@const saldoParcial = saldosParciais.get(index)} {#if index === 0} {/if} {/each} {/each}
Data Tipo Horário Saldo Parcial Saldo Diário Localização Status Ações
{dataFormatada} {config ? getTipoRegistroLabel(registro.tipo, { nomeEntrada: config.nomeEntrada, nomeSaidaAlmoco: config.nomeSaidaAlmoco, nomeRetornoAlmoco: config.nomeRetornoAlmoco, nomeSaida: config.nomeSaida }) : getTipoRegistroLabel(registro.tipo)} {formatarHoraPonto(registro.hora, registro.minuto)} {#if saldoParcial} Par {saldoParcial.parNumero}: +{saldoParcial.horas}h {saldoParcial.minutos}min {:else} - {/if} {#if grupoData.saldoDiarioComparativo} {:else if grupoData.saldoDiario} {:else} - {/if} {registro.dentroDoPrazo ? '✓ Dentro do Prazo' : '✗ Fora do Prazo'}
{/each}
{/if}
{#if mostrarModalImpressao && funcionarioParaImprimir} { mostrarModalImpressao = false; funcionarioParaImprimir = ''; }} onGenerate={gerarPDFComSelecaoWrapper} /> {/if} {#if mostrarModalDetalhes && registroDetalhesId} {@const registroDetalhesQuery = useQuery( api.pontos.obterRegistro, registroDetalhesId ? { registroId: registroDetalhesId } : 'skip' )} {@const registroDetalhes = registroDetalhesQuery?.data} e.target === e.currentTarget && fecharModalDetalhes()} > {/if}