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:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import WidgetGestaoPontos from '$lib/components/ponto/WidgetGestaoPontos.svelte';
|
||||
const menuItems = [
|
||||
{
|
||||
categoria: "Gestão de Ausências",
|
||||
@@ -115,6 +116,11 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Widget Gestão de Pontos -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
|
||||
<WidgetGestaoPontos />
|
||||
</div>
|
||||
|
||||
<!-- Card de Ajuda -->
|
||||
<div class="alert alert-info shadow-lg mt-8">
|
||||
<svg
|
||||
|
||||
@@ -11,7 +11,28 @@
|
||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import type { FunctionReturnType } from 'convex/server';
|
||||
import { X, Calendar, Users, Clock, CheckCircle2, Eye, FileCheck, CalendarDays, User, Mail, Shield, Briefcase, Hash, CreditCard, Building2, CheckCircle, ListChecks, Info } from 'lucide-svelte';
|
||||
import {
|
||||
X,
|
||||
Calendar,
|
||||
Users,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
FileCheck,
|
||||
CalendarDays,
|
||||
User,
|
||||
Mail,
|
||||
Shield,
|
||||
Briefcase,
|
||||
Hash,
|
||||
CreditCard,
|
||||
Building2,
|
||||
CheckCircle,
|
||||
ListChecks,
|
||||
Info,
|
||||
Fingerprint
|
||||
} from 'lucide-svelte';
|
||||
import RegistroPonto from '$lib/components/ponto/RegistroPonto.svelte';
|
||||
import TicketCard from '$lib/components/chamados/TicketCard.svelte';
|
||||
import TicketTimeline from '$lib/components/chamados/TicketTimeline.svelte';
|
||||
import { chamadosStore } from '$lib/stores/chamados';
|
||||
@@ -45,6 +66,7 @@
|
||||
| 'minhas-ausencias'
|
||||
| 'aprovar-ferias'
|
||||
| 'aprovar-ausencias'
|
||||
| 'meu-ponto'
|
||||
>('meu-perfil');
|
||||
|
||||
let periodoSelecionado = $state<Id<'ferias'> | null>(null);
|
||||
@@ -216,11 +238,7 @@
|
||||
if (!selectedTicketId && chamadosQuery.data.length > 0) {
|
||||
selectedTicketId = chamadosQuery.data[0]._id;
|
||||
}
|
||||
} else if (
|
||||
chamadosQuery !== undefined &&
|
||||
chamadosQuery?.data === null &&
|
||||
!gestorIdDisponivel
|
||||
) {
|
||||
} else if (chamadosQuery !== undefined && chamadosQuery?.data === null && !gestorIdDisponivel) {
|
||||
chamadosEstaveis = [];
|
||||
chamadosStore.setTickets([]);
|
||||
}
|
||||
@@ -703,12 +721,12 @@
|
||||
<!-- Tabs PREMIUM -->
|
||||
<div
|
||||
role="tablist"
|
||||
class="tabs tabs-boxed from-base-200 to-base-300 mb-8 bg-gradient-to-r p-2 shadow-xl rounded-xl border border-base-300"
|
||||
class="tabs tabs-boxed from-base-200 to-base-300 border-base-300 mb-8 rounded-xl border bg-gradient-to-r p-2 shadow-xl"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'meu-perfil' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'meu-perfil' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'meu-perfil')}
|
||||
>
|
||||
<User class="h-5 w-5" strokeWidth={2} />
|
||||
@@ -718,7 +736,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'meus-chamados' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'meus-chamados' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'meus-chamados')}
|
||||
aria-label="Meus Chamados"
|
||||
>
|
||||
@@ -730,7 +748,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'minhas-ferias')}
|
||||
>
|
||||
<Calendar class="h-5 w-5" strokeWidth={2} />
|
||||
@@ -740,7 +758,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'minhas-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'minhas-ausencias')}
|
||||
>
|
||||
<Clock class="h-5 w-5" strokeWidth={2} />
|
||||
@@ -751,7 +769,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-green-600 to-emerald-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ferias' ? 'tab-active scale-105 bg-gradient-to-r from-green-600 to-emerald-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'aprovar-ferias')}
|
||||
>
|
||||
<CheckCircle2 class="h-5 w-5" strokeWidth={2} />
|
||||
@@ -768,7 +786,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg font-semibold transition-all duration-300 gap-2 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'aprovar-ausencias' ? 'tab-active scale-105 bg-gradient-to-r from-orange-600 to-amber-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'aprovar-ausencias')}
|
||||
>
|
||||
<Clock class="h-5 w-5" strokeWidth={2} />
|
||||
@@ -782,6 +800,16 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'meu-ponto' ? 'tab-active scale-105 bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
|
||||
onclick={() => (abaAtiva = 'meu-ponto')}
|
||||
>
|
||||
<Fingerprint class="h-5 w-5" strokeWidth={2} />
|
||||
Meu Ponto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo das Abas -->
|
||||
@@ -915,19 +943,21 @@
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<!-- Informações Pessoais PREMIUM -->
|
||||
<div
|
||||
class="card bg-gradient-to-br from-base-100 to-base-200 hover:shadow-3xl border-t-4 border-purple-500 shadow-2xl transition-shadow overflow-hidden"
|
||||
class="card from-base-100 to-base-200 hover:shadow-3xl overflow-hidden border-t-4 border-purple-500 bg-gradient-to-br shadow-2xl transition-shadow"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg ring-2 ring-purple-500/20">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg ring-2 ring-purple-500/20"
|
||||
>
|
||||
<User class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Informações Pessoais
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Seus dados pessoais e de acesso
|
||||
</p>
|
||||
</div>
|
||||
@@ -935,45 +965,54 @@
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500/20 to-purple-600/20">
|
||||
<User class="text-purple-600 h-5 w-5" strokeWidth={2} />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500/20 to-purple-600/20"
|
||||
>
|
||||
<User class="h-5 w-5 text-purple-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>Nome Completo</span
|
||||
>
|
||||
<p class="text-base-content text-base font-semibold mt-1">
|
||||
<p class="text-base-content mt-1 text-base font-semibold">
|
||||
{currentUser.data?.nome}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20">
|
||||
<Mail class="text-blue-600 h-5 w-5" strokeWidth={2} />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20"
|
||||
>
|
||||
<Mail class="h-5 w-5 text-blue-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>E-mail Institucional</span
|
||||
>
|
||||
<p class="text-base-content text-base font-semibold break-all mt-1">
|
||||
<p class="text-base-content mt-1 text-base font-semibold break-all">
|
||||
{currentUser.data?.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary/20 to-primary/30">
|
||||
<div
|
||||
class="from-primary/20 to-primary/30 flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br"
|
||||
>
|
||||
<Shield class="text-primary h-5 w-5" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>Perfil de Acesso</span
|
||||
>
|
||||
<div class="badge badge-primary badge-lg mt-2 font-bold shadow-sm">
|
||||
@@ -988,19 +1027,21 @@
|
||||
<!-- Dados Funcionais PREMIUM -->
|
||||
{#if funcionario}
|
||||
<div
|
||||
class="card bg-gradient-to-br from-base-100 to-base-200 hover:shadow-3xl border-t-4 border-blue-500 shadow-2xl transition-shadow overflow-hidden"
|
||||
class="card from-base-100 to-base-200 hover:shadow-3xl overflow-hidden border-t-4 border-blue-500 bg-gradient-to-br shadow-2xl transition-shadow"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg ring-2 ring-blue-500/20">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg ring-2 ring-blue-500/20"
|
||||
>
|
||||
<Briefcase class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Dados Funcionais
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Informações profissionais e organizacionais
|
||||
</p>
|
||||
</div>
|
||||
@@ -1008,43 +1049,56 @@
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20">
|
||||
<Hash class="text-blue-600 h-5 w-5" strokeWidth={2} />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/20"
|
||||
>
|
||||
<Hash class="h-5 w-5 text-blue-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>Matrícula</span
|
||||
>
|
||||
<p class="text-base-content text-base font-semibold mt-1">
|
||||
<p class="text-base-content mt-1 text-base font-semibold">
|
||||
{funcionario.matricula || 'Não informada'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-green-500/20 to-green-600/20">
|
||||
<CreditCard class="text-green-600 h-5 w-5" strokeWidth={2} />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-green-500/20 to-green-600/20"
|
||||
>
|
||||
<CreditCard class="h-5 w-5 text-green-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide">CPF</span>
|
||||
<p class="text-base-content text-base font-semibold mt-1">
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>CPF</span
|
||||
>
|
||||
<p class="text-base-content mt-1 text-base font-semibold">
|
||||
{funcionario.cpf}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500/20 to-orange-600/20">
|
||||
<Building2 class="text-orange-600 h-5 w-5" strokeWidth={2} />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500/20 to-orange-600/20"
|
||||
>
|
||||
<Building2 class="h-5 w-5 text-orange-600" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide">Time</span>
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>Time</span
|
||||
>
|
||||
{#if meuTime}
|
||||
<div class="mt-2">
|
||||
<div
|
||||
@@ -1064,13 +1118,16 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hover:bg-base-200/60 flex items-start gap-3 rounded-lg border border-base-300/50 p-4 transition-all shadow-sm hover:shadow-md"
|
||||
class="hover:bg-base-200/60 border-base-300/50 flex items-start gap-3 rounded-lg border p-4 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-success/20 to-success/30">
|
||||
<div
|
||||
class="from-success/20 to-success/30 flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br"
|
||||
>
|
||||
<CheckCircle class="text-success h-5 w-5" strokeWidth={2} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="label-text text-base-content/60 text-xs font-semibold uppercase tracking-wide"
|
||||
<span
|
||||
class="label-text text-base-content/60 text-xs font-semibold tracking-wide uppercase"
|
||||
>Status Atual</span
|
||||
>
|
||||
{#if funcionario.statusFerias === 'em_ferias'}
|
||||
@@ -1078,7 +1135,9 @@
|
||||
🏖️ Em Férias
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-success badge-lg mt-2 font-bold shadow-sm">✅ Ativo</div>
|
||||
<div class="badge badge-success badge-lg mt-2 font-bold shadow-sm">
|
||||
✅ Ativo
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1091,12 +1150,10 @@
|
||||
<!-- Times Gerenciados PREMIUM -->
|
||||
{#if ehGestor}
|
||||
<div
|
||||
class="card border-t-4 border-warning bg-gradient-to-br from-warning/10 via-base-100 to-warning/5 shadow-2xl"
|
||||
class="card border-warning from-warning/10 via-base-100 to-warning/5 border-t-4 bg-gradient-to-br shadow-2xl"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2
|
||||
class="card-title mb-6 flex items-center gap-2 text-2xl text-warning"
|
||||
>
|
||||
<h2 class="card-title text-warning mb-6 flex items-center gap-2 text-2xl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-7 w-7"
|
||||
@@ -1609,18 +1666,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Lista de Solicitações -->
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-primary shadow-2xl overflow-hidden">
|
||||
<div
|
||||
class="card from-base-100 to-base-200 border-primary overflow-hidden border-t-4 bg-gradient-to-br shadow-2xl"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-primary/80 shadow-lg ring-2 ring-primary/20">
|
||||
<div
|
||||
class="from-primary to-primary/80 ring-primary/20 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg ring-2"
|
||||
>
|
||||
<ListChecks class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Minhas Solicitações
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Histórico de solicitações de férias
|
||||
</p>
|
||||
</div>
|
||||
@@ -1632,32 +1693,42 @@
|
||||
</div>
|
||||
|
||||
{#if solicitacoesFiltradas.length === 0}
|
||||
<div class="alert alert-info shadow-lg border border-info/20">
|
||||
<div class="alert alert-info border-info/20 border shadow-lg">
|
||||
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||
<span class="font-semibold">Nenhuma solicitação encontrada com os filtros aplicados.</span>
|
||||
<span class="font-semibold"
|
||||
>Nenhuma solicitação encontrada com os filtros aplicados.</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
|
||||
<table class="table table-zebra">
|
||||
<div class="border-base-300 overflow-x-auto rounded-lg border shadow-inner">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr class="bg-gradient-to-r from-base-200 to-base-300">
|
||||
<th class="font-bold text-base-content">Ano</th>
|
||||
<th class="font-bold text-base-content">Período</th>
|
||||
<th class="font-bold text-base-content">Dias</th>
|
||||
<th class="font-bold text-base-content">Status</th>
|
||||
<th class="font-bold text-base-content">Solicitado em</th>
|
||||
<tr class="from-base-200 to-base-300 bg-gradient-to-r">
|
||||
<th class="text-base-content font-bold">Ano</th>
|
||||
<th class="text-base-content font-bold">Período</th>
|
||||
<th class="text-base-content font-bold">Dias</th>
|
||||
<th class="text-base-content font-bold">Status</th>
|
||||
<th class="text-base-content font-bold">Solicitado em</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each solicitacoesFiltradas as periodo (periodo._id)}
|
||||
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
|
||||
<td class="font-semibold text-base-content/80">{periodo.anoReferencia}</td>
|
||||
<td class="font-medium text-base-content/70">
|
||||
<tr
|
||||
class="hover:bg-base-200/50 border-base-300 border-b transition-all duration-200"
|
||||
>
|
||||
<td class="text-base-content/80 font-semibold">{periodo.anoReferencia}</td
|
||||
>
|
||||
<td class="text-base-content/70 font-medium">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
|
||||
<span>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
|
||||
periodo.dataFim
|
||||
)}</span>
|
||||
<CalendarDays
|
||||
class="text-base-content/50 h-3.5 w-3.5"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span
|
||||
>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
|
||||
periodo.dataFim
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -1666,11 +1737,13 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class={`badge badge-sm font-semibold shadow-sm ${getStatusBadge(periodo.status)}`}>
|
||||
<div
|
||||
class={`badge badge-sm font-semibold shadow-sm ${getStatusBadge(periodo.status)}`}
|
||||
>
|
||||
{getStatusTexto(periodo.status)}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs text-base-content/60 font-medium"
|
||||
<td class="text-base-content/60 text-xs font-medium"
|
||||
>{new Date(periodo._creationTime).toLocaleDateString('pt-BR')}</td
|
||||
>
|
||||
</tr>
|
||||
@@ -1973,18 +2046,22 @@
|
||||
</div>
|
||||
{:else if abaAtiva === 'aprovar-ferias'}
|
||||
<!-- Aprovar Férias (Gestores) PREMIUM -->
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-green-500 shadow-2xl overflow-hidden">
|
||||
<div
|
||||
class="card from-base-100 to-base-200 overflow-hidden border-t-4 border-green-500 bg-gradient-to-br shadow-2xl"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600 shadow-lg ring-2 ring-green-500/20">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-green-600 shadow-lg ring-2 ring-green-500/20"
|
||||
>
|
||||
<Users class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Solicitações da Equipe
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Gerencie as solicitações de férias da sua equipe
|
||||
</p>
|
||||
</div>
|
||||
@@ -1996,29 +2073,31 @@
|
||||
</div>
|
||||
|
||||
{#if solicitacoesSubordinados.length === 0}
|
||||
<div class="alert alert-success shadow-lg border border-success/20">
|
||||
<div class="alert alert-success border-success/20 border shadow-lg">
|
||||
<CheckCircle2 class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||
<span class="font-semibold">Nenhuma solicitação pendente no momento.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
|
||||
<table class="table table-zebra">
|
||||
<div class="border-base-300 overflow-x-auto rounded-lg border shadow-inner">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr class="bg-gradient-to-r from-base-200 to-base-300">
|
||||
<th class="font-bold text-base-content">Funcionário</th>
|
||||
<th class="font-bold text-base-content">Time</th>
|
||||
<th class="font-bold text-base-content">Ano</th>
|
||||
<th class="font-bold text-base-content">Período</th>
|
||||
<th class="font-bold text-base-content">Dias</th>
|
||||
<th class="font-bold text-base-content">Status</th>
|
||||
<th class="font-bold text-base-content text-center">Ações</th>
|
||||
<tr class="from-base-200 to-base-300 bg-gradient-to-r">
|
||||
<th class="text-base-content font-bold">Funcionário</th>
|
||||
<th class="text-base-content font-bold">Time</th>
|
||||
<th class="text-base-content font-bold">Ano</th>
|
||||
<th class="text-base-content font-bold">Período</th>
|
||||
<th class="text-base-content font-bold">Dias</th>
|
||||
<th class="text-base-content font-bold">Status</th>
|
||||
<th class="text-base-content text-center font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each solicitacoesSubordinados as periodo (periodo._id)}
|
||||
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
|
||||
<tr
|
||||
class="hover:bg-base-200/50 border-base-300 border-b transition-all duration-200"
|
||||
>
|
||||
<td>
|
||||
<div class="font-semibold text-base-content">
|
||||
<div class="text-base-content font-semibold">
|
||||
{periodo.funcionario?.nome}
|
||||
</div>
|
||||
</td>
|
||||
@@ -2033,13 +2112,18 @@
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="font-semibold text-base-content/80">{periodo.anoReferencia}</td>
|
||||
<td class="font-medium text-base-content/70">
|
||||
<td class="text-base-content/80 font-semibold">{periodo.anoReferencia}</td>
|
||||
<td class="text-base-content/70 font-medium">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
|
||||
<span>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
|
||||
periodo.dataFim
|
||||
)}</span>
|
||||
<CalendarDays
|
||||
class="text-base-content/50 h-3.5 w-3.5"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span
|
||||
>{formatarDataString(periodo.dataInicio)} - {formatarDataString(
|
||||
periodo.dataFim
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -2068,7 +2152,7 @@
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300 transition-all duration-200"
|
||||
class="btn btn-ghost btn-sm hover:bg-base-300 gap-2 transition-all duration-200"
|
||||
onclick={() => selecionarPeriodo(periodo._id)}
|
||||
>
|
||||
<Eye class="h-4 w-4" strokeWidth={2} />
|
||||
@@ -2087,18 +2171,22 @@
|
||||
</div>
|
||||
{:else if abaAtiva === 'aprovar-ausencias'}
|
||||
<!-- Aprovar Ausências (Gestores) -->
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 border-t-4 border-orange-500 shadow-2xl overflow-hidden">
|
||||
<div
|
||||
class="card from-base-100 to-base-200 overflow-hidden border-t-4 border-orange-500 bg-gradient-to-br shadow-2xl"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 shadow-lg ring-2 ring-orange-500/20">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 shadow-lg ring-2 ring-orange-500/20"
|
||||
>
|
||||
<Clock class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content flex items-center gap-2">
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Solicitações de Ausências da Equipe
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60 mt-0.5">
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Gerencie as solicitações de ausências da sua equipe
|
||||
</p>
|
||||
</div>
|
||||
@@ -2110,28 +2198,30 @@
|
||||
</div>
|
||||
|
||||
{#if ausenciasSubordinados.length === 0}
|
||||
<div class="alert alert-success shadow-lg border border-success/20">
|
||||
<div class="alert alert-success border-success/20 border shadow-lg">
|
||||
<CheckCircle2 class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||
<span class="font-semibold">Nenhuma solicitação pendente no momento.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-base-300 shadow-inner">
|
||||
<table class="table table-zebra">
|
||||
<div class="border-base-300 overflow-x-auto rounded-lg border shadow-inner">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr class="bg-gradient-to-r from-base-200 to-base-300">
|
||||
<th class="font-bold text-base-content">Funcionário</th>
|
||||
<th class="font-bold text-base-content">Time</th>
|
||||
<th class="font-bold text-base-content">Período</th>
|
||||
<th class="font-bold text-base-content">Dias</th>
|
||||
<th class="font-bold text-base-content">Status</th>
|
||||
<th class="font-bold text-base-content text-center">Ações</th>
|
||||
<tr class="from-base-200 to-base-300 bg-gradient-to-r">
|
||||
<th class="text-base-content font-bold">Funcionário</th>
|
||||
<th class="text-base-content font-bold">Time</th>
|
||||
<th class="text-base-content font-bold">Período</th>
|
||||
<th class="text-base-content font-bold">Dias</th>
|
||||
<th class="text-base-content font-bold">Status</th>
|
||||
<th class="text-base-content text-center font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each ausenciasSubordinados as ausencia (ausencia._id)}
|
||||
<tr class="hover:bg-base-200/50 transition-all duration-200 border-b border-base-300">
|
||||
<tr
|
||||
class="hover:bg-base-200/50 border-base-300 border-b transition-all duration-200"
|
||||
>
|
||||
<td>
|
||||
<div class="font-semibold text-base-content">
|
||||
<div class="text-base-content font-semibold">
|
||||
{ausencia.funcionario?.nome || 'N/A'}
|
||||
</div>
|
||||
</td>
|
||||
@@ -2147,9 +2237,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="font-medium text-base-content/70">
|
||||
<td class="text-base-content/70 font-medium">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<CalendarDays class="h-3.5 w-3.5 text-base-content/50" strokeWidth={2} />
|
||||
<CalendarDays
|
||||
class="text-base-content/50 h-3.5 w-3.5"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span>
|
||||
{new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até
|
||||
{new Date(ausencia.dataFim).toLocaleDateString('pt-BR')}
|
||||
@@ -2186,7 +2279,7 @@
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300 transition-all duration-200"
|
||||
class="btn btn-ghost btn-sm hover:bg-base-300 gap-2 transition-all duration-200"
|
||||
onclick={() => (solicitacaoAusenciaAprovar = ausencia._id)}
|
||||
>
|
||||
<Eye class="h-4 w-4" strokeWidth={2} />
|
||||
@@ -2465,6 +2558,34 @@
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
{#if abaAtiva === 'meu-ponto'}
|
||||
<!-- Meu Ponto -->
|
||||
<div
|
||||
class="card from-base-100 to-base-200 overflow-hidden border-t-4 border-blue-500 bg-gradient-to-br shadow-2xl"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg ring-2 ring-blue-500/20"
|
||||
>
|
||||
<Fingerprint class="h-6 w-6 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
|
||||
Meu Ponto
|
||||
</h2>
|
||||
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||
Registre sua entrada, saída e intervalos de trabalho
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RegistroPonto />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
CheckCircle2,
|
||||
Info,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
} from "lucide-svelte";
|
||||
import type { Component } from "svelte";
|
||||
|
||||
@@ -119,6 +120,22 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
categoria: "Controle de Ponto",
|
||||
descricao: "Gerencie registros de ponto dos funcionários",
|
||||
Icon: Clock,
|
||||
gradient: "from-cyan-500/10 to-cyan-600/20",
|
||||
accentColor: "text-cyan-600",
|
||||
bgIcon: "bg-cyan-500/20",
|
||||
opcoes: [
|
||||
{
|
||||
nome: "Registro de Pontos",
|
||||
descricao: "Visualizar e gerenciar registros de ponto",
|
||||
href: "/recursos-humanos/registro-pontos",
|
||||
Icon: Clock,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
<script lang="ts">
|
||||
import { onMount } 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 } from 'lucide-svelte';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { formatarHoraPonto, getTipoRegistroLabel } from '$lib/utils/ponto';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Estados
|
||||
let dataInicio = $state(new Date().toISOString().split('T')[0]!);
|
||||
let dataFim = $state(new Date().toISOString().split('T')[0]!);
|
||||
let funcionarioIdFiltro = $state<Id<'funcionarios'> | ''>('');
|
||||
let carregando = $state(false);
|
||||
|
||||
// Queries
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
const registrosQuery = useQuery(api.pontos.listarRegistrosPeriodo, {
|
||||
funcionarioId: funcionarioIdFiltro || undefined,
|
||||
dataInicio,
|
||||
dataFim,
|
||||
});
|
||||
const estatisticasQuery = useQuery(api.pontos.obterEstatisticas, {
|
||||
dataInicio,
|
||||
dataFim,
|
||||
});
|
||||
|
||||
const funcionarios = $derived(funcionariosQuery?.data || []);
|
||||
const registros = $derived(registrosQuery?.data || []);
|
||||
const estatisticas = $derived(estatisticasQuery?.data);
|
||||
|
||||
// Agrupar registros por funcionário e data
|
||||
const registrosAgrupados = $derived.by(() => {
|
||||
const agrupados: Record<
|
||||
string,
|
||||
{
|
||||
funcionario: { nome: string; matricula?: string; descricaoCargo?: string } | null;
|
||||
registros: typeof registros;
|
||||
}
|
||||
> = {};
|
||||
|
||||
for (const registro of registros) {
|
||||
const key = registro.funcionarioId;
|
||||
if (!agrupados[key]) {
|
||||
agrupados[key] = {
|
||||
funcionario: registro.funcionario,
|
||||
registros: [],
|
||||
};
|
||||
}
|
||||
agrupados[key]!.registros.push(registro);
|
||||
}
|
||||
|
||||
return Object.values(agrupados);
|
||||
});
|
||||
|
||||
async function imprimirFichaPonto(funcionarioId: Id<'funcionarios'>) {
|
||||
const registrosFuncionario = registros.filter((r) => r.funcionarioId === funcionarioId);
|
||||
if (registrosFuncionario.length === 0) {
|
||||
alert('Nenhum registro encontrado para este funcionário no período selecionado');
|
||||
return;
|
||||
}
|
||||
|
||||
const funcionario = funcionarios.find((f) => f._id === funcionarioId);
|
||||
if (!funcionario) {
|
||||
alert('Funcionário não encontrado');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Logo
|
||||
let yPosition = 20;
|
||||
try {
|
||||
const logoImg = new Image();
|
||||
logoImg.src = logoGovPE;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
logoImg.onload = () => resolve();
|
||||
logoImg.onerror = () => reject();
|
||||
setTimeout(() => reject(), 3000);
|
||||
});
|
||||
|
||||
const logoWidth = 25;
|
||||
const aspectRatio = logoImg.height / logoImg.width;
|
||||
const logoHeight = logoWidth * aspectRatio;
|
||||
|
||||
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
|
||||
yPosition = Math.max(20, 10 + logoHeight / 2);
|
||||
} catch (err) {
|
||||
console.warn('Não foi possível carregar a logo:', err);
|
||||
}
|
||||
|
||||
// Cabeçalho
|
||||
doc.setFontSize(16);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.text('FICHA DE PONTO', 105, yPosition, { align: 'center' });
|
||||
|
||||
yPosition += 10;
|
||||
|
||||
// Dados do Funcionário
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
yPosition += 8;
|
||||
doc.setFontSize(10);
|
||||
|
||||
if (funcionario.matricula) {
|
||||
doc.text(`Matrícula: ${funcionario.matricula}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
}
|
||||
doc.text(`Nome: ${funcionario.nome}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
if (funcionario.descricaoCargo) {
|
||||
doc.text(`Cargo/Função: ${funcionario.descricaoCargo}`, 15, yPosition);
|
||||
yPosition += 6;
|
||||
}
|
||||
|
||||
yPosition += 5;
|
||||
doc.text(`Período: ${dataInicio} a ${dataFim}`, 15, yPosition);
|
||||
yPosition += 10;
|
||||
|
||||
// Tabela de registros
|
||||
const tableData = registrosFuncionario.map((r) => [
|
||||
r.data,
|
||||
getTipoRegistroLabel(r.tipo),
|
||||
formatarHoraPonto(r.hora, r.minuto),
|
||||
r.dentroDoPrazo ? 'Sim' : 'Não',
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Data', 'Tipo', 'Horário', 'Dentro do Prazo']],
|
||||
body: tableData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
// Rodapé
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.text(
|
||||
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
|
||||
// Salvar
|
||||
const nomeArquivo = `ficha-ponto-${funcionario.matricula || funcionario.nome}-${dataInicio}-${dataFim}.pdf`;
|
||||
doc.save(nomeArquivo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar ficha de ponto. Tente novamente.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<!-- 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">Registro de Pontos</h1>
|
||||
<p class="text-base-content/60 mt-1">Gerencie e visualize os registros de ponto dos funcionários</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
<Filter class="h-5 w-5" />
|
||||
Filtros
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="data-inicio">
|
||||
<span class="label-text font-medium">Data Início</span>
|
||||
</label>
|
||||
<input
|
||||
id="data-inicio"
|
||||
type="date"
|
||||
bind:value={dataInicio}
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="data-fim">
|
||||
<span class="label-text font-medium">Data Fim</span>
|
||||
</label>
|
||||
<input
|
||||
id="data-fim"
|
||||
type="date"
|
||||
bind:value={dataFim}
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="funcionario">
|
||||
<span class="label-text font-medium">Funcionário</span>
|
||||
</label>
|
||||
<select
|
||||
id="funcionario"
|
||||
bind:value={funcionarioIdFiltro}
|
||||
class="select select-bordered"
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{#each funcionarios as funcionario}
|
||||
<option value={funcionario._id}>{funcionario.nome}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estatísticas -->
|
||||
{#if estatisticas}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-primary">
|
||||
<BarChart3 class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="stat-title">Total de Registros</div>
|
||||
<div class="stat-value text-primary">{estatisticas.totalRegistros}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-success">
|
||||
<CheckCircle2 class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="stat-title">Dentro do Prazo</div>
|
||||
<div class="stat-value text-success">{estatisticas.dentroDoPrazo}</div>
|
||||
<div class="stat-desc">
|
||||
{estatisticas.totalRegistros > 0
|
||||
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-error">
|
||||
<XCircle class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="stat-title">Fora do Prazo</div>
|
||||
<div class="stat-value text-error">{estatisticas.foraDoPrazo}</div>
|
||||
<div class="stat-desc">
|
||||
{estatisticas.totalRegistros > 0
|
||||
? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-100 shadow-lg rounded-lg">
|
||||
<div class="stat-figure text-info">
|
||||
<Users class="h-8 w-8" />
|
||||
</div>
|
||||
<div class="stat-title">Funcionários</div>
|
||||
<div class="stat-value text-info">{estatisticas.totalFuncionarios}</div>
|
||||
<div class="stat-desc">
|
||||
{estatisticas.funcionariosDentroPrazo} dentro do prazo, {estatisticas.funcionariosForaPrazo} fora
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Lista de Registros -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Registros</h2>
|
||||
|
||||
{#if registrosAgrupados.length === 0}
|
||||
<div class="alert alert-info">
|
||||
<span>Nenhum registro encontrado para o período selecionado</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each registrosAgrupados as grupo}
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">
|
||||
{grupo.funcionario?.nome || 'Funcionário não encontrado'}
|
||||
</h3>
|
||||
{#if grupo.funcionario?.matricula}
|
||||
<p class="text-sm text-base-content/70">
|
||||
Matrícula: {grupo.funcionario.matricula}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={() => imprimirFichaPonto(grupo.registros[0]!.funcionarioId)}
|
||||
>
|
||||
<Printer class="h-4 w-4" />
|
||||
Imprimir Ficha
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Tipo</th>
|
||||
<th>Horário</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each grupo.registros as registro}
|
||||
<tr>
|
||||
<td>{registro.data}</td>
|
||||
<td>{getTipoRegistroLabel(registro.tipo)}</td>
|
||||
<td>{formatarHoraPonto(registro.hora, registro.minuto)}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {registro.dentroDoPrazo ? 'badge-success' : 'badge-error'}"
|
||||
>
|
||||
{registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import WidgetGestaoPontos from '$lib/components/ponto/WidgetGestaoPontos.svelte';
|
||||
const menuItems = [
|
||||
{
|
||||
categoria: "Gestão de Ausências",
|
||||
@@ -113,6 +114,11 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Widget Gestão de Pontos -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
|
||||
<WidgetGestaoPontos />
|
||||
</div>
|
||||
|
||||
<!-- Card de Ajuda -->
|
||||
<div class="alert alert-info shadow-lg mt-8">
|
||||
<svg
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user