feat: integrate point management features into the dashboard

- Added a new "Meu Ponto" section for users to register their work hours, breaks, and attendance.
- Introduced a "Controle de Ponto" category in the Recursos Humanos section for managing employee time records.
- Enhanced the backend schema to support point registration and configuration settings.
- Updated various components to improve UI consistency and user experience across the dashboard.
This commit is contained in:
2025-11-18 11:44:12 -03:00
parent 52123a33b3
commit f0c6e4468f
22 changed files with 3604 additions and 128 deletions

View File

@@ -258,6 +258,24 @@
palette: 'secondary',
icon: 'envelope'
},
{
title: 'Configurações de Ponto',
description:
'Configure os horários de trabalho, intervalos e tolerâncias para o sistema de controle de ponto.',
ctaLabel: 'Configurar Ponto',
href: '/(dashboard)/ti/configuracoes-ponto',
palette: 'primary',
icon: 'clock'
},
{
title: 'Configurações de Relógio',
description:
'Configure a sincronização de tempo com servidor NTP ou use o relógio do PC como fallback.',
ctaLabel: 'Configurar Relógio',
href: '/(dashboard)/ti/configuracoes-relogio',
palette: 'info',
icon: 'clock'
},
{
title: 'Monitoramento de Emails',
description:

View File

@@ -0,0 +1,191 @@
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Save, CheckCircle2 } from 'lucide-svelte';
const client = useConvexClient();
const configQuery = useQuery(api.configuracaoPonto.obterConfiguracao, {});
let horarioEntrada = $state('08:00');
let horarioSaidaAlmoco = $state('12:00');
let horarioRetornoAlmoco = $state('13:00');
let horarioSaida = $state('17:00');
let toleranciaMinutos = $state(15);
let processando = $state(false);
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
$effect(() => {
if (configQuery?.data) {
horarioEntrada = configQuery.data.horarioEntrada;
horarioSaidaAlmoco = configQuery.data.horarioSaidaAlmoco;
horarioRetornoAlmoco = configQuery.data.horarioRetornoAlmoco;
horarioSaida = configQuery.data.horarioSaida;
toleranciaMinutos = configQuery.data.toleranciaMinutos;
}
});
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
async function salvarConfiguracao() {
// Validação básica
if (!horarioEntrada || !horarioSaidaAlmoco || !horarioRetornoAlmoco || !horarioSaida) {
mostrarMensagem('error', 'Preencha todos os horários');
return;
}
if (toleranciaMinutos < 0 || toleranciaMinutos > 60) {
mostrarMensagem('error', 'Tolerância deve estar entre 0 e 60 minutos');
return;
}
processando = true;
try {
await client.mutation(api.configuracaoPonto.salvarConfiguracao, {
horarioEntrada,
horarioSaidaAlmoco,
horarioRetornoAlmoco,
horarioSaida,
toleranciaMinutos,
});
mostrarMensagem('success', 'Configuração salva com sucesso!');
} catch (error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao salvar configuração');
} finally {
processando = false;
}
}
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Clock class="h-8 w-8 text-primary" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações de Ponto</h1>
<p class="text-base-content/60 mt-1">Configure os horários de trabalho e tolerâncias</p>
</div>
</div>
</div>
<!-- Mensagens -->
{#if mensagem}
<div
class="alert mb-6"
class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === 'error'}
>
<CheckCircle2 class="h-6 w-6" />
<span>{mensagem.texto}</span>
</div>
{/if}
<!-- Formulário -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Horários de Trabalho</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Entrada -->
<div class="form-control">
<label class="label" for="horario-entrada">
<span class="label-text font-medium">Horário de Entrada *</span>
</label>
<input
id="horario-entrada"
type="time"
bind:value={horarioEntrada}
class="input input-bordered"
/>
</div>
<!-- Saída para Almoço -->
<div class="form-control">
<label class="label" for="horario-saida-almoco">
<span class="label-text font-medium">Saída para Almoço *</span>
</label>
<input
id="horario-saida-almoco"
type="time"
bind:value={horarioSaidaAlmoco}
class="input input-bordered"
/>
</div>
<!-- Retorno do Almoço -->
<div class="form-control">
<label class="label" for="horario-retorno-almoco">
<span class="label-text font-medium">Retorno do Almoço *</span>
</label>
<input
id="horario-retorno-almoco"
type="time"
bind:value={horarioRetornoAlmoco}
class="input input-bordered"
/>
</div>
<!-- Saída -->
<div class="form-control">
<label class="label" for="horario-saida">
<span class="label-text font-medium">Horário de Saída *</span>
</label>
<input
id="horario-saida"
type="time"
bind:value={horarioSaida}
class="input input-bordered"
/>
</div>
</div>
<div class="divider"></div>
<!-- Tolerância -->
<div class="form-control">
<label class="label" for="tolerancia">
<span class="label-text font-medium">Tolerância (minutos) *</span>
</label>
<input
id="tolerancia"
type="number"
bind:value={toleranciaMinutos}
min="0"
max="60"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt"
>Tempo de tolerância para registros antes ou depois do horário configurado</span
>
</div>
</div>
<!-- Ações -->
<div class="card-actions justify-end mt-6">
<button
class="btn btn-primary"
onclick={salvarConfiguracao}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Save class="h-5 w-5" />
{/if}
Salvar Configuração
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,250 @@
<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Save, CheckCircle2, AlertCircle, RefreshCw } from 'lucide-svelte';
const client = useConvexClient();
const configQuery = useQuery(api.configuracaoRelogio.obterConfiguracao, {});
let servidorNTP = $state('pool.ntp.org');
let portaNTP = $state(123);
let usarServidorExterno = $state(false);
let fallbackParaPC = $state(true);
let processando = $state(false);
let testando = $state(false);
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
$effect(() => {
if (configQuery?.data) {
servidorNTP = configQuery.data.servidorNTP || 'pool.ntp.org';
portaNTP = configQuery.data.portaNTP || 123;
usarServidorExterno = configQuery.data.usarServidorExterno || false;
fallbackParaPC = configQuery.data.fallbackParaPC !== undefined ? configQuery.data.fallbackParaPC : true;
}
});
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
async function salvarConfiguracao() {
if (usarServidorExterno) {
if (!servidorNTP || servidorNTP.trim() === '') {
mostrarMensagem('error', 'Servidor NTP é obrigatório quando usar servidor externo');
return;
}
if (portaNTP < 1 || portaNTP > 65535) {
mostrarMensagem('error', 'Porta NTP deve estar entre 1 e 65535');
return;
}
}
processando = true;
try {
await client.mutation(api.configuracaoRelogio.salvarConfiguracao, {
servidorNTP: usarServidorExterno ? servidorNTP : undefined,
portaNTP: usarServidorExterno ? portaNTP : undefined,
usarServidorExterno,
fallbackParaPC,
});
mostrarMensagem('success', 'Configuração salva com sucesso!');
} catch (error) {
console.error('Erro ao salvar configuração:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao salvar configuração');
} finally {
processando = false;
}
}
async function testarSincronizacao() {
testando = true;
mensagem = null;
try {
const resultado = await client.action(api.configuracaoRelogio.sincronizarTempo, {});
if (resultado.sucesso) {
mostrarMensagem(
'success',
resultado.usandoServidorExterno
? `Sincronização bem-sucedida! Offset: ${resultado.offsetSegundos}s`
: 'Usando relógio do PC (servidor externo não disponível)'
);
} else {
mostrarMensagem('error', 'Falha na sincronização');
}
} catch (error) {
console.error('Erro ao testar sincronização:', error);
mostrarMensagem('error', error instanceof Error ? error.message : 'Erro ao testar sincronização');
} finally {
testando = false;
}
}
</script>
<div class="container mx-auto px-4 py-6 max-w-4xl">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<div class="p-3 bg-primary/10 rounded-xl">
<Clock class="h-8 w-8 text-primary" strokeWidth={2} />
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Configurações de Relógio</h1>
<p class="text-base-content/60 mt-1">Configure a sincronização de tempo do sistema</p>
</div>
</div>
</div>
<!-- Mensagens -->
{#if mensagem}
<div
class="alert mb-6"
class:alert-success={mensagem.tipo === 'success'}
class:alert-error={mensagem.tipo === 'error'}
>
{#if mensagem.tipo === 'success'}
<CheckCircle2 class="h-6 w-6" />
{:else}
<AlertCircle class="h-6 w-6" />
{/if}
<span>{mensagem.texto}</span>
</div>
{/if}
<!-- Formulário -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Sincronização de Tempo</h2>
<!-- Usar Servidor Externo -->
<div class="form-control mb-4">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
bind:checked={usarServidorExterno}
class="checkbox checkbox-primary"
/>
<span class="label-text font-medium">Usar servidor NTP externo</span>
</label>
<div class="label">
<span class="label-text-alt"
>Sincronizar com servidor de tempo externo (NTP) em vez de usar o relógio do PC</span
>
</div>
</div>
{#if usarServidorExterno}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- Servidor NTP -->
<div class="form-control">
<label class="label" for="servidor-ntp">
<span class="label-text font-medium">Servidor NTP *</span>
</label>
<input
id="servidor-ntp"
type="text"
bind:value={servidorNTP}
placeholder="pool.ntp.org"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Ex: pool.ntp.org, time.google.com, time.windows.com</span>
</div>
</div>
<!-- Porta NTP -->
<div class="form-control">
<label class="label" for="porta-ntp">
<span class="label-text font-medium">Porta NTP *</span>
</label>
<input
id="porta-ntp"
type="number"
bind:value={portaNTP}
placeholder="123"
min="1"
max="65535"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Porta padrão: 123</span>
</div>
</div>
</div>
{/if}
<!-- Fallback para PC -->
<div class="form-control mb-4">
<label class="label cursor-pointer gap-3">
<input
type="checkbox"
bind:checked={fallbackParaPC}
class="checkbox checkbox-primary"
/>
<span class="label-text font-medium">Usar relógio do PC se servidor externo falhar</span>
</label>
<div class="label">
<span class="label-text-alt"
>Se marcado, o sistema usará o relógio do PC caso não consiga sincronizar com o servidor
externo</span
>
</div>
</div>
<!-- Ações -->
<div class="card-actions justify-end mt-6 gap-3">
{#if usarServidorExterno}
<button
class="btn btn-outline btn-info"
onclick={testarSincronizacao}
disabled={testando || processando}
>
{#if testando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<RefreshCw class="h-5 w-5" />
{/if}
Testar Sincronização
</button>
{/if}
<button
class="btn btn-primary"
onclick={salvarConfiguracao}
disabled={processando || testando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Save class="h-5 w-5" />
{/if}
Salvar Configuração
</button>
</div>
</div>
</div>
<!-- Informações -->
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title mb-4">Informações</h2>
<div class="alert alert-info">
<AlertCircle class="h-6 w-6" />
<div>
<p>
<strong>Nota:</strong> O sistema usa uma API HTTP para sincronização de tempo como
aproximação do protocolo NTP. Para sincronização NTP real, seria necessário uma biblioteca
específica.
</p>
<p class="text-sm mt-1">
Servidores NTP recomendados: pool.ntp.org, time.google.com, time.windows.com
</p>
</div>
</div>
</div>
</div>
</div>