Merge remote-tracking branch 'origin' into feat-licitacoes-contratos

This commit is contained in:
2025-11-18 11:18:26 -03:00
29 changed files with 4470 additions and 3550 deletions

View File

@@ -8,6 +8,11 @@
import { createAuthClient } from "better-auth/svelte";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
// O baseURL deve apontar para o frontend (SvelteKit), não para o Convex diretamente
// O Better Auth usa as rotas HTTP do Convex que são acessadas via proxy do SvelteKit
// ou diretamente se configurado. Com o plugin convexClient, o token é gerenciado automaticamente.
export const authClient = createAuthClient({
// baseURL padrão é window.location.origin, que é o correto para SvelteKit
// O Better Auth será acessado via rotas HTTP do Convex registradas em http.ts
plugins: [convexClient()],
});

View File

@@ -29,7 +29,8 @@
aprovado: 'badge-success',
reprovado: 'badge-error',
data_ajustada_aprovada: 'badge-info',
EmFérias: 'badge-info'
EmFérias: 'badge-info',
Cancelado_RH: 'badge-error'
};
return badges[status] || 'badge-neutral';
}
@@ -40,19 +41,20 @@
aprovado: 'Aprovado',
reprovado: 'Reprovado',
data_ajustada_aprovada: 'Data Ajustada e Aprovada',
EmFérias: 'Em Férias'
EmFérias: 'Em Férias',
Cancelado_RH: 'Cancelado RH'
};
return textos[status] || status;
}
async function voltarParaAguardando() {
async function cancelarPorRH() {
try {
processando = true;
erro = '';
await client.mutation(api.ferias.atualizarStatus, {
feriasId: solicitacao._id,
novoStatus: 'aguardando_aprovacao',
novoStatus: 'Cancelado_RH',
usuarioId: usuarioId
});
@@ -150,10 +152,10 @@
</div>
{/if}
<!-- Ação: Voltar para Aguardando Aprovação -->
{#if solicitacao.status !== 'aguardando_aprovacao'}
<!-- Ação: Cancelar por RH -->
{#if solicitacao.status !== 'Cancelado_RH'}
<div class="divider mt-6"></div>
<div class="alert alert-info">
<div class="alert alert-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -164,14 +166,13 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
></path>
</svg>
<div>
<h3 class="font-bold">Alterar Status</h3>
<h3 class="font-bold">Cancelar Férias</h3>
<div class="text-sm">
Ao voltar para "Aguardando Aprovação", a solicitação ficará disponível para aprovação ou
reprovação pelo gestor.
Ao cancelar as férias, o status será alterado para "Cancelado RH" e a solicitação não poderá mais ser processada.
</div>
</div>
</div>
@@ -179,8 +180,8 @@
<div class="card-actions mt-4 justify-end">
<button
type="button"
class="btn btn-warning gap-2"
onclick={voltarParaAguardando}
class="btn btn-error gap-2"
onclick={cancelarPorRH}
disabled={processando}
>
<svg
@@ -194,29 +195,29 @@
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Voltar para Aguardando Aprovação
Cancelar Férias (RH)
</button>
</div>
{:else}
<div class="divider mt-6"></div>
<div class="alert">
<div class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info h-6 w-6 shrink-0"
class="stroke-current h-6 w-6 shrink-0"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Esta solicitação já está aguardando aprovação.</span>
<span>Esta solicitação já foi cancelada pelo RH.</span>
</div>
{/if}

View File

@@ -138,37 +138,43 @@
</div>
<!-- Card Principal -->
<div class="card bg-base-100 border-t-4 border-orange-500 shadow-2xl">
<div class="card-body">
<div class="card bg-base-100 border-t-4 border-primary shadow-2xl">
<div class="card-body p-8">
<!-- Informações do Funcionário -->
<div class="mb-6">
<h3 class="mb-4 flex items-center gap-2 text-xl font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<div class="mb-8">
<h3 class="mb-5 flex items-center gap-3 text-xl font-bold text-primary">
<div class="rounded-lg bg-primary/10 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
Funcionário
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<p class="text-base-content/70 text-sm">Nome</p>
<p class="text-lg font-bold">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<div class="rounded-xl bg-base-200/50 p-4 transition-all hover:bg-base-200">
<p class="mb-2 text-sm font-semibold uppercase tracking-wide text-base-content/60">
Nome
</p>
<p class="text-lg font-bold text-base-content">
{solicitacao.funcionario?.nome || 'N/A'}
</p>
</div>
{#if solicitacao.time}
<div>
<p class="text-base-content/70 text-sm">Time</p>
<div class="rounded-xl bg-base-200/50 p-4 transition-all hover:bg-base-200">
<p class="mb-2 text-sm font-semibold uppercase tracking-wide text-base-content/60">
Time
</p>
<div
class="badge badge-lg font-semibold"
style="background-color: {solicitacao.time.cor}20; border-color: {solicitacao.time
@@ -181,88 +187,96 @@
</div>
</div>
<div class="divider"></div>
<div class="divider my-6"></div>
<!-- Período da Ausência -->
<div class="mb-6">
<h3 class="mb-4 flex items-center gap-2 text-xl font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<div class="mb-8">
<h3 class="mb-5 flex items-center gap-3 text-xl font-bold text-primary">
<div class="rounded-lg bg-primary/10 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
Período da Ausência
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div
class="stat rounded-xl border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950"
class="stat rounded-xl border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10 shadow-md transition-all hover:border-primary/30 hover:shadow-lg"
>
<div class="stat-title">Data Início</div>
<div class="stat-value text-2xl text-orange-600 dark:text-orange-400">
<div class="stat-title text-base-content/70">Data Início</div>
<div class="stat-value text-2xl text-primary">
{new Date(solicitacao.dataInicio).toLocaleDateString('pt-BR')}
</div>
</div>
<div
class="stat rounded-xl border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950"
class="stat rounded-xl border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10 shadow-md transition-all hover:border-primary/30 hover:shadow-lg"
>
<div class="stat-title">Data Fim</div>
<div class="stat-value text-2xl text-orange-600 dark:text-orange-400">
<div class="stat-title text-base-content/70">Data Fim</div>
<div class="stat-value text-2xl text-primary">
{new Date(solicitacao.dataFim).toLocaleDateString('pt-BR')}
</div>
</div>
<div
class="stat rounded-xl border-2 border-orange-500/30 bg-linear-to-br from-orange-50 to-amber-50 dark:from-orange-950 dark:to-amber-950"
class="stat rounded-xl border-2 border-primary/30 bg-gradient-to-br from-primary/10 to-primary/15 shadow-md transition-all hover:border-primary/40 hover:shadow-lg"
>
<div class="stat-title">Total de Dias</div>
<div class="stat-value text-3xl text-orange-600 dark:text-orange-400">
<div class="stat-title text-base-content/70">Total de Dias</div>
<div class="stat-value text-3xl font-bold text-primary">
{totalDias}
</div>
<div class="stat-desc">dias corridos</div>
<div class="stat-desc text-base-content/60">dias corridos</div>
</div>
</div>
</div>
<div class="divider"></div>
<div class="divider my-6"></div>
<!-- Motivo -->
<div class="mb-6">
<h3 class="mb-4 flex items-center gap-2 text-xl font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<div class="mb-8">
<h3 class="mb-5 flex items-center gap-3 text-xl font-bold text-primary">
<div class="rounded-lg bg-primary/10 p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-primary h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
Motivo da Ausência
</h3>
<div class="card bg-base-200">
<div class="card-body">
<p class="whitespace-pre-wrap">{solicitacao.motivo}</p>
<div class="card rounded-xl border-2 border-primary/10 bg-base-200/50 shadow-sm">
<div class="card-body p-5">
<p class="whitespace-pre-wrap leading-relaxed text-base-content">
{solicitacao.motivo}
</p>
</div>
</div>
</div>
<!-- Status Atual -->
<div class="mb-6">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold">Status:</span>
<div class="mb-8 rounded-xl bg-base-200/30 p-4">
<div class="flex items-center gap-3">
<span class="text-sm font-semibold uppercase tracking-wide text-base-content/70"
>Status:</span
>
<div class={`badge badge-lg ${getStatusBadge(solicitacao.status)}`}>
{getStatusTexto(solicitacao.status)}
</div>
@@ -271,7 +285,7 @@
<!-- Erro -->
{#if erro}
<div class="alert alert-error mb-4">
<div class="alert alert-error mb-6 shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
@@ -291,7 +305,7 @@
<!-- Ações -->
{#if solicitacao.status === 'aguardando_aprovacao'}
<div class="card-actions mt-6 justify-end gap-4">
<div class="card-actions mt-8 justify-end gap-4">
<button
type="button"
class="btn btn-error btn-lg gap-2"
@@ -348,14 +362,14 @@
<!-- Modal de Reprovação -->
{#if motivoReprovacao !== undefined}
<div class="mt-4">
<div class="mt-6 rounded-xl border-2 border-error/20 bg-error/5 p-5">
<div class="form-control">
<label class="label" for="motivo-reprovacao">
<span class="label-text font-bold">Motivo da Reprovação</span>
<span class="label-text font-bold text-error">Motivo da Reprovação</span>
</label>
<textarea
id="motivo-reprovacao"
class="textarea textarea-bordered h-24"
class="textarea textarea-bordered h-24 focus:border-error focus:outline-error"
placeholder="Informe o motivo da reprovação..."
bind:value={motivoReprovacao}
></textarea>
@@ -363,7 +377,7 @@
</div>
{/if}
{:else}
<div class="alert alert-info">
<div class="alert alert-info shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -382,10 +396,10 @@
{/if}
<!-- Botão Cancelar -->
<div class="mt-4 text-center">
<div class="mt-6 text-center">
<button
type="button"
class="btn"
class="btn btn-ghost"
onclick={() => {
if (onCancelar) onCancelar();
}}

File diff suppressed because it is too large Load Diff

View File

@@ -20,26 +20,50 @@
let isChecking = $state(true);
let hasAccess = $state(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let hasCheckedOnce = $state(false);
let lastUserState = $state<typeof currentUser | undefined>(undefined);
const currentUser = useQuery(api.auth.getCurrentUser, {});
onMount(() => {
checkAccess();
// Usar $effect para reagir apenas às mudanças na query currentUser
$effect(() => {
// Não verificar novamente se já tem acesso concedido e usuário está autenticado
if (hasAccess && currentUser?.data) {
lastUserState = currentUser;
return;
}
// Evitar loop: só verificar se currentUser realmente mudou
// Comparar dados, não o objeto proxy
const currentData = currentUser?.data;
const lastData = lastUserState?.data;
if (currentData !== lastData || (currentUser === undefined) !== (lastUserState === undefined)) {
lastUserState = currentUser;
checkAccess();
}
});
function checkAccess() {
isChecking = true;
// Limpar timeout anterior se existir
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
// Aguardar um pouco para o authStore carregar do localStorage
setTimeout(() => {
// Verificar autenticação
if (requireAuth && !currentUser?.data) {
const currentPath = window.location.pathname;
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
return;
}
// Se a query ainda está carregando (undefined), aguardar
if (currentUser === undefined) {
isChecking = true;
hasAccess = false;
return;
}
// Marcar que já verificou pelo menos uma vez
hasCheckedOnce = true;
// Se a query retornou dados, verificar autenticação
if (currentUser?.data) {
// Verificar roles
if (allowedRoles.length > 0 && currentUser?.data) {
if (allowedRoles.length > 0) {
const hasRole = allowedRoles.includes(currentUser.data.role?.nome ?? '');
if (!hasRole) {
const currentPath = window.location.pathname;
@@ -49,19 +73,49 @@
}
// Verificar nível
if (
currentUser?.data &&
currentUser.data.role?.nivel &&
currentUser.data.role.nivel > maxLevel
) {
if (currentUser.data.role?.nivel && currentUser.data.role.nivel > maxLevel) {
const currentPath = window.location.pathname;
window.location.href = `${redirectTo}?error=access_denied&route=${encodeURIComponent(currentPath)}`;
return;
}
// Se chegou aqui, permitir acesso
hasAccess = true;
isChecking = false;
}, 100);
return;
}
// Se não tem dados e requer autenticação
if (requireAuth && !currentUser?.data) {
// Se a query já retornou (não está mais undefined), finalizar estado
if (currentUser !== undefined) {
const currentPath = window.location.pathname;
// Evitar redirecionamento em loop - verificar se já está na URL de erro
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has('error')) {
// Só redirecionar se não estiver em loop
if (!hasCheckedOnce || currentUser === null) {
window.location.href = `${redirectTo}?error=auth_required&redirect=${encodeURIComponent(currentPath)}`;
return;
}
}
// Se já tem erro na URL, permitir renderização para mostrar o alerta
isChecking = false;
hasAccess = true;
return;
}
// Se ainda está carregando (undefined), aguardar
isChecking = true;
hasAccess = false;
return;
}
// Se não requer autenticação, permitir acesso
if (!requireAuth) {
hasAccess = true;
isChecking = false;
}
}
</script>

View File

@@ -98,6 +98,7 @@
matricula = '';
senha = '';
erroLogin = '';
carregandoLogin = false;
}
function closeLoginModal() {
@@ -105,6 +106,7 @@
matricula = '';
senha = '';
erroLogin = '';
carregandoLogin = false;
}
function openAboutModal() {
@@ -137,6 +139,7 @@
} else {
erroLogin = 'Erro ao fazer login';
}
carregandoLogin = false;
}
async function handleLogout() {
@@ -203,7 +206,7 @@
<p
class="text-base-content/80 hidden text-xs leading-tight font-medium sm:block lg:text-base"
>
Sistema de Gerenciamento da<br class="lg:hidden" /> Secretaria de Esportes
Sistema de Gerenciamento de Secretaria
</p>
</div>
</div>
@@ -325,7 +328,7 @@
>Contato</a
>
<span class="text-base-content/30"></span>
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
<a href={resolve('/abrir-chamado')} class="link link-hover hover:text-primary transition-colors"
>Suporte</a
>
<span class="text-base-content/30"></span>
@@ -391,101 +394,142 @@
<!-- Modal de Login -->
{#if loginModalStore.showModal}
<dialog class="modal modal-open">
<div class="modal-box bg-base-100 relative max-w-md overflow-hidden">
<div
class="modal-box from-base-100 via-base-100 to-primary/5 relative max-w-md overflow-hidden bg-gradient-to-br shadow-2xl backdrop-blur-sm"
>
<!-- Botão de fechar moderno -->
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
class="btn btn-sm btn-circle btn-ghost absolute top-4 right-4 z-10 hover:bg-error/20 hover:text-error transition-all duration-200"
onclick={closeLoginModal}
aria-label="Fechar modal"
>
<XCircle class="h-5 w-5" strokeWidth={2.5} />
</button>
<div class="p-4">
<div class="mb-6 text-center">
<div class="avatar mb-4">
<div class="bg-primary/10 w-20 rounded-lg p-3">
<img src={logo} alt="Logo" class="h-full w-full object-contain" />
<!-- Decoração de fundo -->
<div
class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-primary/10 blur-3xl"
></div>
<div
class="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-primary/5 blur-3xl"
></div>
<div class="relative z-10 p-8">
<!-- Header com logo e título -->
<div class="mb-8 text-center">
<div class="avatar mb-5 mx-auto">
<div
class="group relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 ring-primary/20 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
>
<div
class="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div>
<img
src={logo}
alt="Logo SGSE"
class="relative z-10 h-full w-full object-contain"
/>
</div>
</div>
<h3 class="text-primary text-3xl font-bold">Login</h3>
<p class="text-base-content/60 mt-2 text-sm">Acesse o sistema com suas credenciais</p>
<h3 class="text-primary mb-2 text-4xl font-bold tracking-tight">Login</h3>
<p class="text-base-content/70 text-sm font-medium">
Acesse o sistema com suas credenciais
</p>
</div>
<!-- Mensagem de erro -->
{#if erroLogin}
<div class="alert alert-error mb-4">
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span>{erroLogin}</span>
<div
class="alert alert-error mb-6 border-error/30 bg-error/10 shadow-lg backdrop-blur-sm"
>
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2.5} />
<span class="font-medium">{erroLogin}</span>
</div>
{/if}
<form class="space-y-4" onsubmit={handleLogin}>
<!-- Formulário -->
<form class="space-y-5" onsubmit={handleLogin}>
<!-- Campo Matrícula/E-mail -->
<div class="form-control">
<label class="label" for="login-matricula">
<span class="label-text font-semibold">Matrícula ou E-mail</span>
<label class="label pb-2" for="login-matricula">
<span class="text-primary label-text text-sm font-semibold"
>Matrícula ou E-mail</span
>
</label>
<input
id="login-matricula"
type="text"
placeholder="Digite sua matrícula ou e-mail"
class="input input-bordered input-primary w-full"
bind:value={matricula}
required
disabled={carregandoLogin}
/>
<div class="relative">
<input
id="login-matricula"
type="text"
placeholder="Digite sua matrícula ou e-mail"
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
bind:value={matricula}
required
disabled={carregandoLogin}
autocomplete="username"
/>
</div>
</div>
<!-- Campo Senha -->
<div class="form-control">
<label class="label" for="login-password">
<span class="label-text font-semibold">Senha</span>
<label class="label pb-2" for="login-password">
<span class="text-primary label-text text-sm font-semibold">Senha</span>
</label>
<input
id="login-password"
type="password"
placeholder="Digite sua senha"
class="input input-bordered input-primary w-full"
bind:value={senha}
required
disabled={carregandoLogin}
/>
<div class="relative">
<input
id="login-password"
type="password"
placeholder="Digite sua senha"
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
bind:value={senha}
required
disabled={carregandoLogin}
autocomplete="current-password"
/>
</div>
</div>
<div class="form-control mt-6">
<button type="submit" class="btn btn-primary w-full" disabled={carregandoLogin}>
<!-- Botão de submit -->
<div class="form-control pt-2">
<button
type="submit"
class="btn btn-primary btn-lg group relative w-full overflow-hidden border-0 bg-gradient-to-r from-primary via-primary to-primary/90 shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
disabled={carregandoLogin}
>
<!-- Efeito de brilho animado -->
<div
class="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
></div>
{#if carregandoLogin}
<span class="loading loading-spinner loading-sm"></span>
Entrando...
<span class="font-semibold">Entrando...</span>
{:else}
<LogIn class="h-5 w-5" strokeWidth={2} />
Entrar
<LogIn class="h-5 w-5 transition-transform duration-300 group-hover:scale-110" strokeWidth={2.5} />
<span class="font-semibold">Entrar</span>
{/if}
</button>
</div>
<div class="mt-4 space-y-2 text-center">
<!-- Links auxiliares -->
<div class="pt-4 space-y-3 text-center">
<a
href={resolve('/abrir-chamado')}
class="link link-primary block text-sm"
class="link link-primary block text-sm font-medium transition-all duration-200 hover:scale-105"
onclick={closeLoginModal}
>
Abrir Chamado
</a>
<a
href={resolve('/esqueci-senha')}
class="link link-secondary block text-sm"
class="link link-secondary block text-sm font-medium transition-all duration-200 hover:scale-105"
onclick={closeLoginModal}
>
Esqueceu sua senha?
</a>
</div>
</form>
<div class="divider text-base-content/40 text-xs">Credenciais de teste</div>
<div class="bg-base-200 rounded-lg p-3 text-xs">
<p class="mb-1 font-semibold">Admin:</p>
<p>
Matrícula: <code class="bg-base-300 rounded px-2 py-1">0000</code>
</p>
<p>
Senha: <code class="bg-base-300 rounded px-2 py-1">Admin@123</code>
</p>
</div>
</div>
</div>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
@@ -500,77 +544,77 @@
{#if showAboutModal}
<dialog class="modal modal-open">
<div
class="modal-box from-base-100 to-base-200 relative max-w-2xl overflow-hidden bg-linear-to-br"
class="modal-box from-base-100 to-base-200 relative max-w-md overflow-hidden bg-gradient-to-br shadow-xl"
>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2 z-10 hover:bg-base-300"
onclick={closeAboutModal}
>
</button>
<div class="space-y-6 py-4 text-center">
<div class="space-y-5 px-6 py-6 text-center">
<!-- Logo e Título -->
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col items-center gap-3">
<div class="avatar">
<div class="w-24 rounded-xl bg-white p-3 shadow-lg">
<div class="w-20 rounded-xl bg-white p-3 shadow-lg ring-2 ring-primary/20">
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
</div>
</div>
<div>
<h3 class="text-primary mb-2 text-3xl font-bold">SGSE</h3>
<p class="text-base-content/80 text-lg font-semibold">
Sistema de Gerenciamento da<br />Secretaria de Esportes
<div class="space-y-1">
<h3 class="text-primary text-2xl font-bold tracking-tight">SGSE</h3>
<p class="text-base-content/70 text-sm font-medium">
Sistema de Gerenciamento de Secretaria
</p>
</div>
</div>
<!-- Divider -->
<div class="divider"></div>
<div class="divider my-1"></div>
<!-- Informações de Versão -->
<div class="bg-primary/10 space-y-3 rounded-xl p-6">
<div class="bg-gradient-to-br from-primary/10 to-primary/5 space-y-2 rounded-xl border border-primary/10 p-4 shadow-sm">
<div class="flex items-center justify-center gap-2">
<Tag class="text-primary h-5 w-5" strokeWidth={2} />
<p class="text-base-content/70 text-sm font-medium">Versão</p>
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
<p class="text-base-content/60 text-xs font-medium uppercase tracking-wide">Versão</p>
</div>
<p class="text-primary text-2xl font-bold">1.0 26_2025</p>
<div class="badge badge-warning badge-lg gap-2">
<Plus class="h-4 w-4" strokeWidth={2} />
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
<Plus class="h-3.5 w-3.5" strokeWidth={2} />
Em Desenvolvimento
</div>
</div>
<!-- Desenvolvido por -->
<div class="space-y-2">
<p class="text-base-content/60 text-sm font-medium">Desenvolvido por</p>
<p class="text-primary text-lg font-bold">Secretaria de Esportes de Pernambuco</p>
<div class="space-y-1.5">
<p class="text-base-content/50 text-xs font-medium uppercase tracking-wide">Desenvolvido por</p>
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
</div>
<!-- Divider -->
<div class="divider"></div>
<div class="divider my-1"></div>
<!-- Informações Adicionais -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="bg-base-200 rounded-lg p-3">
<p class="text-primary font-semibold">Governo</p>
<p class="text-base-content/70 text-xs">Estado de Pernambuco</p>
<div class="grid grid-cols-2 gap-3">
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Governo</p>
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
</div>
<div class="bg-base-200 rounded-lg p-3">
<p class="text-primary font-semibold">Ano</p>
<p class="text-base-content/70 text-xs">2025</p>
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Ano</p>
<p class="text-base-content/60 text-xs font-medium">2025</p>
</div>
</div>
<!-- Botão OK -->
<div class="pt-4">
<div class="pt-3">
<button
type="button"
class="btn btn-primary btn-lg mx-auto w-full max-w-xs shadow-lg transition-all duration-300 hover:shadow-xl"
class="btn btn-primary btn-sm mx-auto w-full max-w-xs shadow-md transition-all duration-200 hover:shadow-lg"
onclick={closeAboutModal}
>
<Check class="h-6 w-6" strokeWidth={2} />
<Check class="h-4 w-4" strokeWidth={2} />
OK
</button>
</div>

View File

@@ -76,38 +76,52 @@ const loading = $derived(props.loading ?? false);
</script>
<form class="space-y-8" onsubmit={handleSubmit}>
<section class="grid gap-6 md:grid-cols-2">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-semibold">Título do chamado</span>
</label>
<input
type="text"
class="input input-bordered input-primary w-full"
placeholder="Ex: Erro ao acessar o módulo de licitações"
bind:value={titulo}
/>
{#if errors.titulo}
<span class="text-error mt-1 text-sm">{errors.titulo}</span>
{/if}
</div>
<!-- Título do Chamado -->
<section class="form-control">
<label class="label">
<span class="label-text font-semibold text-base-content">Título do chamado</span>
</label>
<input
type="text"
class="input input-bordered input-primary w-full"
placeholder="Ex: Erro ao acessar o módulo de licitações"
bind:value={titulo}
/>
{#if errors.titulo}
<span class="text-error mt-1 text-sm">{errors.titulo}</span>
{/if}
</section>
<!-- Tipo de Solicitação e Prioridade -->
<section class="grid gap-6 md:grid-cols-2">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Tipo de solicitação</span>
<span class="label-text font-semibold text-base-content">Tipo de solicitação</span>
</label>
<div class="grid gap-2">
{#each ["chamado", "reclamacao", "elogio", "sugestao"] as opcao}
<label class="btn btn-outline btn-sm justify-start gap-2">
<div class="grid grid-cols-2 gap-2 rounded-xl border border-base-300 bg-base-200/30 p-3">
{#each [
{ value: "chamado", label: "Chamado", icon: "📋" },
{ value: "reclamacao", label: "Reclamação", icon: "⚠️" },
{ value: "elogio", label: "Elogio", icon: "⭐" },
{ value: "sugestao", label: "Sugestão", icon: "💡" }
] as opcao}
<label
class={`flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-2 p-2.5 transition-all ${
tipo === opcao.value
? "border-primary bg-primary/10 shadow-md"
: "border-base-300 bg-base-100 hover:border-primary/50 hover:bg-base-200/50"
}`}
>
<input
type="radio"
name="tipo"
class="radio radio-primary"
value={opcao}
checked={tipo === opcao}
onclick={() => (tipo = opcao as typeof tipo)}
class="radio radio-primary radio-sm shrink-0"
value={opcao.value}
checked={tipo === opcao.value}
onclick={() => (tipo = opcao.value as typeof tipo)}
/>
{opcao.charAt(0).toUpperCase() + opcao.slice(1)}
<span class="text-base shrink-0">{opcao.icon}</span>
<span class="text-sm font-medium flex-1 text-center">{opcao.label}</span>
</label>
{/each}
</div>
@@ -115,39 +129,67 @@ const loading = $derived(props.loading ?? false);
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Prioridade</span>
<span class="label-text font-semibold text-base-content">Prioridade</span>
</label>
<select class="select select-bordered w-full" bind:value={prioridade}>
<option value="baixa">Baixa</option>
<option value="media">Média</option>
<option value="alta">Alta</option>
<option value="critica">Crítica</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Categoria</span>
</label>
<input
type="text"
class="input input-bordered w-full"
placeholder="Ex: Infraestrutura, Sistemas, Acesso"
bind:value={categoria}
/>
{#if errors.categoria}
<span class="text-error mt-1 text-sm">{errors.categoria}</span>
{/if}
<div class="grid grid-cols-2 gap-2 rounded-xl border border-base-300 bg-base-200/30 p-3">
{#each [
{ value: "baixa", label: "Baixa", color: "badge-success" },
{ value: "media", label: "Média", color: "badge-info" },
{ value: "alta", label: "Alta", color: "badge-warning" },
{ value: "critica", label: "Crítica", color: "badge-error" }
] as opcao}
<label
class={`flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-2 p-2.5 transition-all ${
prioridade === opcao.value
? "border-primary bg-primary/10 shadow-md"
: "border-base-300 bg-base-100 hover:border-primary/50 hover:bg-base-200/50"
}`}
>
<input
type="radio"
name="prioridade"
class={`radio radio-sm shrink-0 ${
opcao.value === "baixa" ? "radio-success" :
opcao.value === "media" ? "radio-info" :
opcao.value === "alta" ? "radio-warning" :
"radio-error"
}`}
value={opcao.value}
checked={prioridade === opcao.value}
onclick={() => (prioridade = opcao.value as typeof prioridade)}
/>
<span class={`badge badge-sm ${opcao.color} flex-1 justify-center`}>{opcao.label}</span>
</label>
{/each}
</div>
</div>
</section>
<!-- Categoria -->
<section class="form-control">
<label class="label">
<span class="label-text font-semibold">Descrição detalhada</span>
<span class="label-text font-semibold text-base-content">Categoria</span>
</label>
<input
type="text"
class="input input-bordered w-full"
placeholder="Ex: Infraestrutura, Sistemas, Acesso"
bind:value={categoria}
/>
{#if errors.categoria}
<span class="text-error mt-1 text-sm">{errors.categoria}</span>
{/if}
</section>
<!-- Descrição Detalhada -->
<section class="form-control">
<label class="label">
<span class="label-text font-semibold text-base-content">Descrição detalhada</span>
<span class="label-text-alt text-base-content/50">Obrigatório</span>
</label>
<textarea
class="textarea textarea-bordered textarea-lg min-h-[180px]"
placeholder="Descreva o problema, erro ou sugestão com o máximo de detalhes possível."
placeholder="Descreva o problema, erro ou sugestão com o máximo de detalhes possível..."
bind:value={descricao}
></textarea>
{#if errors.descricao}
@@ -155,7 +197,8 @@ const loading = $derived(props.loading ?? false);
{/if}
</section>
<section class="space-y-4">
<!-- Anexos -->
<section class="space-y-4 rounded-xl border border-base-300 bg-base-200/30 p-4">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-base-content">Anexos (opcional)</p>
@@ -164,6 +207,20 @@ const loading = $derived(props.loading ?? false);
</p>
</div>
<label class="btn btn-outline btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
Selecionar arquivos
<input type="file" class="hidden" multiple accept=".pdf,.png,.jpg,.jpeg" onchange={handleFiles} />
</label>
@@ -196,16 +253,31 @@ const loading = $derived(props.loading ?? false);
{/if}
</section>
<section class="flex flex-wrap gap-3">
<!-- Ações do Formulário -->
<section class="flex flex-wrap gap-3 border-t border-base-300 pt-6">
<button
type="submit"
class="btn btn-primary flex-1 min-w-[200px]"
class="btn btn-primary flex-1 min-w-[200px] shadow-lg"
disabled={loading}
>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
Enviando...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
Registrar chamado
{/if}
</button>
@@ -215,6 +287,20 @@ const loading = $derived(props.loading ?? false);
onclick={resetForm}
disabled={loading}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Limpar
</button>
</section>

View File

@@ -227,12 +227,13 @@
<div class="wizard-ferias-container">
<!-- Progress Bar -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div class="relative flex items-start">
{#each Array(totalPassos) as _, i (i)}
<div class="flex flex-1 items-center">
{@const labels = ['Ano & Saldo', 'Períodos', 'Confirmação']}
<div class="relative z-10 flex flex-1 flex-col items-center">
<!-- Círculo do passo -->
<div
class="relative flex h-12 w-12 items-center justify-center rounded-full font-bold transition-all duration-300"
class="relative z-20 flex h-12 w-12 items-center justify-center rounded-full font-bold transition-all duration-300"
class:bg-primary={passoAtual > i + 1}
class:text-white={passoAtual > i + 1}
class:border-4={passoAtual === i + 1}
@@ -261,10 +262,16 @@
{/if}
</div>
<!-- Label do passo -->
<p class="mt-3 text-center text-sm font-semibold" class:text-primary={passoAtual === i + 1}>
{labels[i]}
</p>
<!-- Linha conectora -->
{#if i < totalPassos - 1}
<div
class="mx-2 h-1 flex-1 transition-all duration-300"
class="absolute left-1/2 top-6 z-10 h-1 transition-all duration-300"
style="width: calc(100% - 1.5rem); margin-left: calc(50% + 0.75rem);"
class:bg-primary={passoAtual > i + 1}
class:bg-base-300={passoAtual <= i + 1}
></div>
@@ -272,19 +279,6 @@
</div>
{/each}
</div>
<!-- Labels dos passos -->
<div class="mt-4 flex justify-between px-1">
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 1}>Ano & Saldo</p>
</div>
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 2}>Períodos</p>
</div>
<div class="flex-1 text-center">
<p class="text-sm font-semibold" class:text-primary={passoAtual === 3}>Confirmação</p>
</div>
</div>
</div>
<!-- Conteúdo dos Passos -->

View File

@@ -896,7 +896,7 @@
doc.setFontSize(8);
doc.setFont('helvetica', 'normal');
doc.setTextColor(128, 128, 128);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, {
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, {
align: 'center'
});
doc.text(`Gerado em: ${agoraStr}`, 105, 290, { align: 'center' });

View File

@@ -197,7 +197,7 @@
doc.setFontSize(8);
doc.setTextColor(128, 128, 128);
doc.text(
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
doc.internal.pageSize.getWidth() / 2,
doc.internal.pageSize.getHeight() - 10,
{ align: 'center' }

View File

@@ -172,7 +172,7 @@ export async function gerarDeclaracaoAcumulacaoCargo(funcionario: Funcionario):
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' });
return doc.output('blob');
}
@@ -260,7 +260,7 @@ export async function gerarDeclaracaoDependentesIR(funcionario: Funcionario): Pr
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' });
return doc.output('blob');
}
@@ -341,7 +341,7 @@ export async function gerarDeclaracaoIdoneidade(funcionario: Funcionario): Promi
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' });
return doc.output('blob');
}
@@ -440,7 +440,7 @@ export async function gerarTermoNepotismo(funcionario: Funcionario): Promise<Blo
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' });
return doc.output('blob');
}
@@ -562,7 +562,7 @@ export async function gerarTermoOpcaoRemuneracao(funcionario: Funcionario): Prom
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text('SGSE - Sistema de Gerenciamento de Secretaria', 105, 285, { align: 'center' });
return doc.output('blob');
}