Files
sgse-app/apps/web/src/routes/(dashboard)/ti/configuracoes-ponto/+page.svelte
deyvisonwanderley d6aaa15cf4 feat: enhance point registration and location validation features
- Refactored the RegistroPonto component to improve the layout and user experience, including a new section for displaying standard hours.
- Updated RelogioSincronizado to include GMT offset adjustments for accurate time display.
- Introduced new location validation logic in the backend to ensure point registrations are within allowed geofenced areas.
- Enhanced the device information schema to capture additional GPS data, improving the reliability of location checks.
- Added new endpoints for managing allowed marking addresses, facilitating better control over where points can be registered.
2025-11-21 05:12:27 -03:00

296 lines
8.5 KiB
Svelte

<script lang="ts">
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { Clock, Save, CheckCircle2, MapPin } from 'lucide-svelte';
import { resolve } from '$app/paths';
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 nomeEntrada = $state('Entrada 1');
let nomeSaidaAlmoco = $state('Saída 1');
let nomeRetornoAlmoco = $state('Entrada 2');
let nomeSaida = $state('Saída 2');
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;
nomeEntrada = configQuery.data.nomeEntrada || 'Entrada 1';
nomeSaidaAlmoco = configQuery.data.nomeSaidaAlmoco || 'Saída 1';
nomeRetornoAlmoco = configQuery.data.nomeRetornoAlmoco || 'Entrada 2';
nomeSaida = configQuery.data.nomeSaida || 'Saída 2';
}
});
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;
}
// Validação dos nomes
if (!nomeEntrada.trim() || !nomeSaidaAlmoco.trim() || !nomeRetornoAlmoco.trim() || !nomeSaida.trim()) {
mostrarMensagem('error', 'Preencha todos os nomes dos registros');
return;
}
processando = true;
try {
await client.mutation(api.configuracaoPonto.salvarConfiguracao, {
horarioEntrada,
horarioSaidaAlmoco,
horarioRetornoAlmoco,
horarioSaida,
toleranciaMinutos,
nomeEntrada: nomeEntrada.trim(),
nomeSaidaAlmoco: nomeSaidaAlmoco.trim(),
nomeRetornoAlmoco: nomeRetornoAlmoco.trim(),
nomeSaida: nomeSaida.trim(),
});
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>
<a
href={resolve('/ti/configuracoes-ponto/enderecos')}
class="btn btn-secondary gap-2"
>
<MapPin class="h-5 w-5" />
Endereços de Marcação
</a>
</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>
<div class="divider"></div>
<!-- Nomes Personalizados dos Registros -->
<h2 class="card-title mb-4">Nomes dos Registros</h2>
<p class="text-sm text-base-content/70 mb-4">
Personalize os nomes exibidos para cada tipo de registro de ponto
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Nome Entrada -->
<div class="form-control">
<label class="label" for="nome-entrada">
<span class="label-text font-medium">Nome do Registro de Entrada *</span>
</label>
<input
id="nome-entrada"
type="text"
bind:value={nomeEntrada}
placeholder="Ex: Entrada 1"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Nome exibido para o primeiro registro do dia</span>
</div>
</div>
<!-- Nome Saída Almoço -->
<div class="form-control">
<label class="label" for="nome-saida-almoco">
<span class="label-text font-medium">Nome do Registro de Saída para Almoço *</span>
</label>
<input
id="nome-saida-almoco"
type="text"
bind:value={nomeSaidaAlmoco}
placeholder="Ex: Saída 1"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Nome exibido para a saída para almoço</span>
</div>
</div>
<!-- Nome Retorno Almoço -->
<div class="form-control">
<label class="label" for="nome-retorno-almoco">
<span class="label-text font-medium">Nome do Registro de Retorno do Almoço *</span>
</label>
<input
id="nome-retorno-almoco"
type="text"
bind:value={nomeRetornoAlmoco}
placeholder="Ex: Entrada 2"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Nome exibido para o retorno do almoço</span>
</div>
</div>
<!-- Nome Saída -->
<div class="form-control">
<label class="label" for="nome-saida">
<span class="label-text font-medium">Nome do Registro de Saída *</span>
</label>
<input
id="nome-saida"
type="text"
bind:value={nomeSaida}
placeholder="Ex: Saída 2"
class="input input-bordered"
/>
<div class="label">
<span class="label-text-alt">Nome exibido para a saída final do dia</span>
</div>
</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>