feat: enhance employee and symbol management with new features, improved UI components, and backend schema updates
This commit is contained in:
@@ -1,39 +1,253 @@
|
||||
<script>
|
||||
import { resolve } from "$app/paths";
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
|
||||
// Buscar estatísticas para exibir nos cards
|
||||
const statsQuery = useQuery(api.dashboard.getStats, {});
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
categoria: "Gestão de Funcionários",
|
||||
descricao: "Gerencie o cadastro e informações dos funcionários",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>`,
|
||||
gradient: "from-blue-500/10 to-blue-600/20",
|
||||
accentColor: "text-blue-600",
|
||||
bgIcon: "bg-blue-500/20",
|
||||
opcoes: [
|
||||
{
|
||||
nome: "Cadastrar Funcionário",
|
||||
descricao: "Adicionar novo funcionário ao sistema",
|
||||
href: "/recursos-humanos/funcionarios/cadastro",
|
||||
icon: `<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="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
nome: "Listar Funcionários",
|
||||
descricao: "Visualizar e editar cadastros",
|
||||
href: "/recursos-humanos/funcionarios",
|
||||
icon: `<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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
nome: "Excluir Cadastro",
|
||||
descricao: "Remover funcionário do sistema",
|
||||
href: "/recursos-humanos/funcionarios/excluir",
|
||||
icon: `<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
nome: "Relatórios",
|
||||
descricao: "Visualizar estatísticas e gráficos",
|
||||
href: "/recursos-humanos/funcionarios/relatorios",
|
||||
icon: `<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
categoria: "Gestão de Símbolos",
|
||||
descricao: "Gerencie cargos comissionados e funções gratificadas",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>`,
|
||||
gradient: "from-green-500/10 to-green-600/20",
|
||||
accentColor: "text-green-600",
|
||||
bgIcon: "bg-green-500/20",
|
||||
opcoes: [
|
||||
{
|
||||
nome: "Cadastrar Símbolo",
|
||||
descricao: "Adicionar novo cargo ou função",
|
||||
href: "/recursos-humanos/simbolos/cadastro",
|
||||
icon: `<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 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
nome: "Listar Símbolos",
|
||||
descricao: "Visualizar e editar símbolos",
|
||||
href: "/recursos-humanos/simbolos",
|
||||
icon: `<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="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-3xl font-bold text-brand-dark">Recursos Humanos</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<h3 class="text-lg font-bold text-brand-dark col-span-4">Funcionários</h3>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/funcionarios/cadastro")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100"
|
||||
>Cadastrar Funcionários</a
|
||||
>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/funcionarios")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100">Editar Cadastro</a
|
||||
>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/funcionarios/excluir")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100">Excluir Cadastro</a
|
||||
>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/funcionarios/relatorios")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100">Relatórios</a
|
||||
>
|
||||
|
||||
<h3 class="text-lg font-bold text-brand-dark col-span-4">Simbolos</h3>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/simbolos/cadastro")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100"
|
||||
>Cadastrar Simbolos</a
|
||||
>
|
||||
<a
|
||||
href={resolve("/recursos-humanos/simbolos")}
|
||||
class="p-4 rounded-xl border hover:shadow bgbase-100">Listar Simbolos</a
|
||||
>
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-4xl font-bold text-primary mb-2">Recursos Humanos</h1>
|
||||
<p class="text-lg text-base-content/70">
|
||||
Gerencie funcionários, símbolos e visualize relatórios do departamento
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estatísticas Rápidas -->
|
||||
{#if statsQuery.data}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="stats shadow-lg bg-gradient-to-br from-primary/10 to-primary/20">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Total</div>
|
||||
<div class="stat-value text-primary">{statsQuery.data.totalFuncionarios}</div>
|
||||
<div class="stat-desc">Funcionários cadastrados</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow-lg bg-gradient-to-br from-success/10 to-success/20">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Ativos</div>
|
||||
<div class="stat-value text-success">{statsQuery.data.funcionariosAtivos}</div>
|
||||
<div class="stat-desc">Funcionários ativos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow-lg bg-gradient-to-br from-secondary/10 to-secondary/20">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Símbolos</div>
|
||||
<div class="stat-value text-secondary">{statsQuery.data.totalSimbolos}</div>
|
||||
<div class="stat-desc">Cargos e funções</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow-lg bg-gradient-to-br from-accent/10 to-accent/20">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">CC / FG</div>
|
||||
<div class="stat-value text-accent">{statsQuery.data.cargoComissionado} / {statsQuery.data.funcaoGratificada}</div>
|
||||
<div class="stat-desc">Distribuição</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Menu de Opções -->
|
||||
<div class="space-y-8">
|
||||
{#each menuItems as categoria}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||
<div class="card-body">
|
||||
<!-- Cabeçalho da Categoria -->
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="p-4 {categoria.bgIcon} rounded-2xl">
|
||||
<div class="{categoria.accentColor}">
|
||||
{@html categoria.icon}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="card-title text-2xl mb-2 {categoria.accentColor}">
|
||||
{categoria.categoria}
|
||||
</h2>
|
||||
<p class="text-base-content/70">{categoria.descricao}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Opções -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{#each categoria.opcoes as opcao}
|
||||
<a
|
||||
href={opcao.href}
|
||||
class="group relative overflow-hidden rounded-xl border-2 border-base-300 bg-gradient-to-br {categoria.gradient} p-6 hover:border-primary hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="p-3 bg-base-100 rounded-lg group-hover:bg-primary group-hover:text-white transition-colors duration-300">
|
||||
<div class="{categoria.accentColor} group-hover:text-white">
|
||||
{@html opcao.icon}
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-base-content/30 group-hover:text-primary transition-colors duration-300"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-base-content mb-2 group-hover:text-primary transition-colors duration-300">
|
||||
{opcao.nome}
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/70 flex-1">
|
||||
{opcao.descricao}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Card de Ajuda -->
|
||||
<div class="alert alert-info shadow-lg mt-8">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Precisa de ajuda?</h3>
|
||||
<div class="text-sm">
|
||||
Entre em contato com o suporte técnico ou consulte a documentação do sistema para mais informações sobre as funcionalidades de Recursos Humanos.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.stats {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,51 +70,135 @@
|
||||
$: needsScroll = filtered.length > 8;
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 pb-32">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-3xl font-bold text-brand-dark">Funcionários</h2>
|
||||
<div class="space-x-2 flex items-center">
|
||||
<button class="btn btn-primary" onclick={navCadastro}>Novo Funcionário</button>
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>Funcionários</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Funcionários Cadastrados</h1>
|
||||
<p class="text-base-content/70">Gerencie os funcionários da secretaria</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-lg gap-2" onclick={navCadastro}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Novo Funcionário
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-4 gap-3 items-end">
|
||||
<div class="form-control">
|
||||
<label class="label" for="func_nome"><span class="label-text">Nome</span></label>
|
||||
<input id="func_nome" class="input input-bordered" bind:value={filtroNome} oninput={applyFilters} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="func_cpf"><span class="label-text">CPF</span></label>
|
||||
<input id="func_cpf" class="input input-bordered" bind:value={filtroCPF} oninput={applyFilters} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="func_matricula"><span class="label-text">Matrícula</span></label>
|
||||
<input id="func_matricula" class="input input-bordered" bind:value={filtroMatricula} oninput={applyFilters} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="func_tipo"><span class="label-text">Símbolo Tipo</span></label>
|
||||
<select id="func_tipo" class="select select-bordered" bind:value={filtroTipo} oninput={applyFilters}>
|
||||
<option value="">Todos</option>
|
||||
<option value="cargo_comissionado">Cargo comissionado</option>
|
||||
<option value="funcao_gratificada">Função gratificada</option>
|
||||
</select>
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<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="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros de Pesquisa
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="func_nome">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<input
|
||||
id="func_nome"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
placeholder="Buscar por nome..."
|
||||
bind:value={filtroNome}
|
||||
oninput={applyFilters}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="func_cpf">
|
||||
<span class="label-text font-semibold">CPF</span>
|
||||
</label>
|
||||
<input
|
||||
id="func_cpf"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
placeholder="000.000.000-00"
|
||||
bind:value={filtroCPF}
|
||||
oninput={applyFilters}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="func_matricula">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
id="func_matricula"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
placeholder="Buscar por matrícula..."
|
||||
bind:value={filtroMatricula}
|
||||
oninput={applyFilters}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="func_tipo">
|
||||
<span class="label-text font-semibold">Símbolo Tipo</span>
|
||||
</label>
|
||||
<select id="func_tipo" class="select select-bordered focus:select-primary w-full" bind:value={filtroTipo} oninput={applyFilters}>
|
||||
<option value="">Todos os tipos</option>
|
||||
<option value="cargo_comissionado">Cargo Comissionado</option>
|
||||
<option value="funcao_gratificada">Função Gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if filtroNome || filtroCPF || filtroMatricula || filtroTipo}
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm gap-2"
|
||||
onclick={() => {
|
||||
filtroNome = "";
|
||||
filtroCPF = "";
|
||||
filtroMatricula = "";
|
||||
filtroTipo = "";
|
||||
applyFilters();
|
||||
}}
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto bg-base-100 rounded-lg shadow-sm mb-2" class:overflow-y-auto={needsScroll} style={needsScroll ? "max-height: calc(100vh - 180px);" : "overflow-y: visible;"}>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>CPF</th>
|
||||
<th>Matrícula</th>
|
||||
<th>Tipo</th>
|
||||
<th>Cidade</th>
|
||||
<th>UF</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Tabela de Funcionários -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-y-auto" style="max-height: {filtered.length > 8 ? '600px' : 'none'};">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="sticky top-0 bg-base-200 z-10">
|
||||
<tr>
|
||||
<th class="font-bold">Nome</th>
|
||||
<th class="font-bold">CPF</th>
|
||||
<th class="font-bold">Matrícula</th>
|
||||
<th class="font-bold">Tipo</th>
|
||||
<th class="font-bold">Cidade</th>
|
||||
<th class="font-bold">UF</th>
|
||||
<th class="text-right font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filtered as f}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{f.nome}</td>
|
||||
@@ -136,10 +220,19 @@
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre resultados -->
|
||||
<div class="mt-4 text-sm text-base-content/70 text-center">
|
||||
Exibindo {filtered.length} de {list.length} funcionário(s)
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
<dialog id="delete_modal_func" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Confirmar Exclusão</h3>
|
||||
@@ -168,5 +261,5 @@
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -190,19 +190,59 @@
|
||||
load();
|
||||
</script>
|
||||
|
||||
<form class="max-w-3xl mx-auto p-4" onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
{#if notice}
|
||||
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<main class="container mx-auto px-4 py-4 max-w-5xl">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li><a href="/recursos-humanos/funcionarios" class="text-primary hover:underline">Funcionários</a></li>
|
||||
<li>Editar</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="card-title text-3xl">Editar Funcionários</h2>
|
||||
<p class="opacity-70">Atualize os dados do funcionário.</p>
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-yellow-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Editar Funcionário</h1>
|
||||
<p class="text-base-content/70">Atualize os dados do funcionário</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<form
|
||||
class="space-y-6"
|
||||
onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}
|
||||
>
|
||||
<!-- Card: Informações Pessoais -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Informações Pessoais
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
|
||||
@@ -222,6 +262,19 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Endereço -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Endereço
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<form.Field name="cep" validators={{ onChange: schema.shape.cep }}>
|
||||
@@ -258,6 +311,18 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Documentos -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
||||
</svg>
|
||||
Documentos
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="cpf" validators={{ onChange: schema.shape.cpf }}>
|
||||
@@ -277,6 +342,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Datas -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="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>
|
||||
Datas
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="nascimento" validators={{ onChange: schema.shape.nascimento }}>
|
||||
@@ -296,6 +373,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Contato -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Contato
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="email" validators={{ onChange: schema.shape.email }}>
|
||||
@@ -315,7 +404,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Cargo/Função -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Cargo/Função
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="simboloTipo" validators={{ onChange: schema.shape.simboloTipo }}>
|
||||
@@ -344,21 +444,47 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card-actions justify-end pt-2">
|
||||
<button type="button" class="btn btn-ghost" disabled={isSubmitting} onclick={() => goto("/recursos-humanos/funcionarios")}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" class:loading={isSubmitting} disabled={isSubmitting || !canSubmit}>
|
||||
Salvar Alterações
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Card: Ações -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-lg"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto("/recursos-humanos/funcionarios")}
|
||||
>
|
||||
<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>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Salvando...
|
||||
{: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="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Salvar Alterações
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
@@ -233,22 +233,59 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="max-w-3xl mx-auto p-4"
|
||||
onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
{#if notice}
|
||||
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<main class="container mx-auto px-4 py-4 max-w-5xl">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li><a href="/recursos-humanos/funcionarios" class="text-primary hover:underline">Funcionários</a></li>
|
||||
<li>Cadastrar</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="card-title text-3xl">Cadastro de Funcionários</h2>
|
||||
<p class="opacity-70">Preencha os campos abaixo para cadastrar um novo funcionário.</p>
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Cadastro de Funcionário</h1>
|
||||
<p class="text-base-content/70">Preencha os campos abaixo para cadastrar um novo funcionário</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<form
|
||||
class="space-y-6"
|
||||
onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}
|
||||
>
|
||||
<!-- Card: Informações Pessoais -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Informações Pessoais
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
|
||||
@@ -268,6 +305,19 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Endereço -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Endereço
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<form.Field name="cep" validators={{ onChange: schema.shape.cep }}>
|
||||
@@ -304,6 +354,18 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Documentos -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
||||
</svg>
|
||||
Documentos
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="cpf" validators={{ onChange: schema.shape.cpf }}>
|
||||
@@ -323,6 +385,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Datas -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="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>
|
||||
Datas
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="nascimento" validators={{ onChange: schema.shape.nascimento }}>
|
||||
@@ -342,6 +416,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Contato -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Contato
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="email" validators={{ onChange: schema.shape.email }}>
|
||||
@@ -361,7 +447,18 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card: Cargo/Função -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
<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="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Cargo/Função
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field name="simboloTipo" validators={{ onChange: schema.shape.simboloTipo }}>
|
||||
@@ -390,21 +487,47 @@
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card-actions justify-end pt-2">
|
||||
<button type="button" class="btn btn-ghost" disabled={isSubmitting} onclick={() => goto("/recursos-humanos/funcionarios")}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" class:loading={isSubmitting} disabled={isSubmitting || !canSubmit}>
|
||||
Cadastrar Funcionário
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Card: Ações -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-lg"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto("/recursos-humanos/funcionarios")}
|
||||
>
|
||||
<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>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Cadastrando...
|
||||
{: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="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Cadastrar Funcionário
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let list: Array<any> = [];
|
||||
let filtro = "";
|
||||
let notice: { kind: "success" | "error"; text: string } | null = null;
|
||||
let toDelete: { id: string; nome: string } | null = null;
|
||||
let deletingId: string | null = null;
|
||||
let list: Array<any> = $state([]);
|
||||
let filtro = $state("");
|
||||
let notice: { kind: "success" | "error"; text: string } | null = $state(null);
|
||||
let toDelete: { id: string; nome: string; cpf: string; matricula: string } | null = $state(null);
|
||||
let deletingId: string | null = $state(null);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -20,8 +20,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteModal(id: string, nome: string) {
|
||||
toDelete = { id, nome };
|
||||
function openDeleteModal(id: string, nome: string, cpf: string, matricula: string) {
|
||||
toDelete = { id, nome, cpf, matricula };
|
||||
(document.getElementById("delete_modal_func_excluir") as HTMLDialogElement)?.showModal();
|
||||
}
|
||||
|
||||
@@ -36,92 +36,295 @@
|
||||
deletingId = toDelete.id;
|
||||
await client.mutation(api.funcionarios.remove, { id: toDelete.id } as any);
|
||||
closeDeleteModal();
|
||||
notice = { kind: "success", text: "Funcionário excluído com sucesso." };
|
||||
notice = { kind: "success", text: `Funcionário "${toDelete.nome}" excluído com sucesso!` };
|
||||
await load();
|
||||
|
||||
// Auto-fechar mensagem de sucesso após 5 segundos
|
||||
setTimeout(() => {
|
||||
notice = null;
|
||||
}, 5000);
|
||||
} catch (e) {
|
||||
notice = { kind: "error", text: "Erro ao excluir cadastro." };
|
||||
notice = { kind: "error", text: "Erro ao excluir cadastro. Tente novamente." };
|
||||
} finally {
|
||||
deletingId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function back() { goto("/recursos-humanos/funcionarios"); }
|
||||
function limparFiltro() {
|
||||
filtro = "";
|
||||
}
|
||||
|
||||
onMount(() => { void load(); });
|
||||
function back() {
|
||||
goto("/recursos-humanos/funcionarios");
|
||||
}
|
||||
|
||||
// Computed para lista filtrada
|
||||
const filtered = $derived(
|
||||
list.filter((f) => {
|
||||
const q = (filtro || "").toLowerCase();
|
||||
return !q ||
|
||||
(f.nome || "").toLowerCase().includes(q) ||
|
||||
(f.cpf || "").includes(q) ||
|
||||
(f.matricula || "").toLowerCase().includes(q);
|
||||
})
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
{#if notice}
|
||||
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold">Excluir Funcionários</h2>
|
||||
<button class="btn" onclick={back}>Voltar</button>
|
||||
</div>
|
||||
|
||||
<div class="form-control max-w-sm">
|
||||
<label class="label" for="func_excluir_busca"><span class="label-text">Buscar por nome/CPF/matrícula</span></label>
|
||||
<input id="func_excluir_busca" class="input input-bordered" bind:value={filtro} />
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>CPF</th>
|
||||
<th>Matrícula</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each list.filter((f) => {
|
||||
const q = (filtro || "").toLowerCase();
|
||||
return !q || (f.nome || "").toLowerCase().includes(q) || (f.cpf || "").includes(q) || (f.matricula || "").toLowerCase().includes(q);
|
||||
}) as f}
|
||||
<tr>
|
||||
<td>{f.nome}</td>
|
||||
<td>{f.cpf}</td>
|
||||
<td>{f.matricula}</td>
|
||||
<td>
|
||||
<button class="btn btn-error btn-sm" onclick={() => openDeleteModal(f._id, f.nome)}>Excluir</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<dialog id="delete_modal_func_excluir" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Confirmar Exclusão</h3>
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/></svg>
|
||||
<span>Esta ação não pode ser desfeita!</span>
|
||||
</div>
|
||||
{#if toDelete}
|
||||
<p class="py-2">Tem certeza que deseja excluir o funcionário <strong class="text-error">{toDelete.nome}</strong>?</p>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-2">
|
||||
<button class="btn btn-ghost" onclick={closeDeleteModal} type="button">Cancelar</button>
|
||||
<button class="btn btn-error" onclick={confirmDelete} disabled={deletingId !== null} type="button">
|
||||
{#if deletingId}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Excluindo...
|
||||
{:else}
|
||||
Confirmar Exclusão
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/" class="text-primary hover:text-primary-focus">
|
||||
<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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/recursos-humanos" class="text-primary hover:text-primary-focus">Recursos Humanos</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/recursos-humanos/funcionarios" class="text-primary hover:text-primary-focus">Funcionários</a>
|
||||
</li>
|
||||
<li class="font-semibold">Excluir Funcionários</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Header com ícone e descrição -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-3 bg-error/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-base-content">Excluir Funcionários</h1>
|
||||
<p class="text-base-content/60 mt-1">Selecione o funcionário que deseja remover do sistema</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-ghost gap-2"
|
||||
onclick={back}
|
||||
>
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Voltar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerta de sucesso/erro -->
|
||||
{#if notice}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
{#if notice.kind === "success"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="font-semibold">{notice.text}</span>
|
||||
<button class="btn btn-sm btn-ghost" onclick={() => notice = null} aria-label="Fechar alerta">
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Card de Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title text-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros de Busca
|
||||
</h2>
|
||||
{#if filtro}
|
||||
<button class="btn btn-sm btn-ghost gap-2" onclick={limparFiltro}>
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for="func_excluir_busca">
|
||||
<span class="label-text font-semibold">Buscar por Nome, CPF ou Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
id="func_excluir_busca"
|
||||
type="text"
|
||||
placeholder="Digite para filtrar..."
|
||||
class="input input-bordered input-primary w-full focus:input-primary"
|
||||
bind:value={filtro}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
{filtered.length} funcionário{filtered.length !== 1 ? 's' : ''} encontrado{filtered.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela de Funcionários -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
Lista de Funcionários
|
||||
</h2>
|
||||
|
||||
{#if list.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="p-4 bg-base-200 rounded-full mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-base-content/70">Nenhum funcionário cadastrado</p>
|
||||
<p class="text-sm text-base-content/50 mt-2">Cadastre funcionários para gerenciá-los aqui</p>
|
||||
</div>
|
||||
{:else if filtered.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div class="p-4 bg-base-200 rounded-full mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-base-content/70">Nenhum resultado encontrado</p>
|
||||
<p class="text-sm text-base-content/50 mt-2">Tente ajustar os filtros de busca</p>
|
||||
<button class="btn btn-primary btn-sm mt-4" onclick={limparFiltro}>Limpar Filtros</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead class="bg-base-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="text-base">Nome</th>
|
||||
<th class="text-base">CPF</th>
|
||||
<th class="text-base">Matrícula</th>
|
||||
<th class="text-base text-center">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filtered as f}
|
||||
<tr class="hover">
|
||||
<td class="font-semibold">{f.nome}</td>
|
||||
<td>{f.cpf}</td>
|
||||
<td><span class="badge badge-ghost">{f.matricula}</span></td>
|
||||
<td class="text-center">
|
||||
<button
|
||||
class="btn btn-error btn-sm gap-2"
|
||||
onclick={() => openDeleteModal(f._id, f.nome, f.cpf, f.matricula)}
|
||||
>
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Excluir
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
<dialog id="delete_modal_func_excluir" class="modal">
|
||||
<div class="modal-box max-w-md">
|
||||
<h3 class="font-bold text-2xl mb-4 flex items-center gap-2 text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
|
||||
</svg>
|
||||
Confirmar Exclusão
|
||||
</h3>
|
||||
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-bold">Atenção!</span>
|
||||
<p class="text-sm">Esta ação não pode ser desfeita!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if toDelete}
|
||||
<div class="bg-base-200 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-base-content/70 mb-3">Você está prestes a excluir o seguinte funcionário:</p>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" 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>
|
||||
<strong class="text-error text-lg">{toDelete.nome}</strong>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="text-base-content/60">CPF:</span>
|
||||
<span class="font-semibold">{toDelete.cpf}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="text-base-content/60">Matrícula:</span>
|
||||
<span class="badge badge-ghost">{toDelete.matricula}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-base-content/70 mb-6">
|
||||
Tem certeza que deseja continuar?
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="modal-action justify-between">
|
||||
<button
|
||||
class="btn btn-ghost gap-2"
|
||||
onclick={closeDeleteModal}
|
||||
disabled={deletingId !== null}
|
||||
>
|
||||
<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>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error gap-2"
|
||||
onclick={confirmDelete}
|
||||
disabled={deletingId !== null}
|
||||
>
|
||||
{#if deletingId}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Excluindo...
|
||||
{: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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Confirmar Exclusão
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
|
||||
@@ -29,130 +29,248 @@
|
||||
}
|
||||
});
|
||||
|
||||
let chartWidth = 800;
|
||||
let chartHeight = 320;
|
||||
const padding = { top: 20, right: 20, bottom: 80, left: 70 };
|
||||
let chartWidth = 900;
|
||||
let chartHeight = 400;
|
||||
const padding = { top: 40, right: 30, bottom: 100, left: 80 };
|
||||
|
||||
function getMax<T>(arr: Array<T>, sel: (t: T) => number): number {
|
||||
let m = 0;
|
||||
for (const a of arr) m = Math.max(m, sel(a));
|
||||
return m;
|
||||
}
|
||||
|
||||
function scaleY(v: number, max: number): number {
|
||||
if (max <= 0) return 0;
|
||||
const innerH = chartHeight - padding.top - padding.bottom;
|
||||
return (v / max) * innerH;
|
||||
}
|
||||
function barX(i: number, n: number): number {
|
||||
|
||||
function getX(i: number, n: number): number {
|
||||
const innerW = chartWidth - padding.left - padding.right;
|
||||
return padding.left + (innerW / n) * i + 10;
|
||||
}
|
||||
function barW(n: number): number {
|
||||
const innerW = chartWidth - padding.left - padding.right;
|
||||
return Math.max(8, innerW / n - 20);
|
||||
return padding.left + (innerW / (n - 1)) * i;
|
||||
}
|
||||
|
||||
// Sem tooltip flutuante; valores serão exibidos de forma fixa no gráfico
|
||||
function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string {
|
||||
if (data.length === 0) return "";
|
||||
const n = data.length;
|
||||
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = getX(i, n);
|
||||
const y = chartHeight - padding.bottom - scaleY(getValue(data[i]), max);
|
||||
path += ` L ${x} ${y}`;
|
||||
}
|
||||
|
||||
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
|
||||
path += " Z";
|
||||
return path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 pb-4">
|
||||
<div class="container mx-auto px-4 py-6 space-y-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/recursos-humanos">Recursos Humanos</a></li>
|
||||
<li><a href="/recursos-humanos/funcionarios">Funcionários</a></li>
|
||||
<li class="font-semibold">Relatórios</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="p-3 bg-primary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-base-content">Relatórios de Funcionários</h1>
|
||||
<p class="text-base-content/60">Análise de distribuição de salários e funcionários por símbolo</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if notice}
|
||||
<div class="alert" class:alert-error={notice.kind === "error"} class:alert-success={notice.kind === "success"}>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-3xl font-bold text-brand-dark">Relatórios</h2>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-6">
|
||||
<!-- Gráfico 1: Símbolo x Salário (Valor) -->
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart -->
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Símbolo x Salário (Valor total)</h3>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de barras: salário por símbolo">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-base-content">Distribuição de Salários por Símbolo</h3>
|
||||
<p class="text-sm text-base-content/60">Valores dos símbolos cadastrados no sistema</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto bg-base-100 rounded-lg p-4">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo">
|
||||
{#if rows.length === 0}
|
||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
||||
{:else}
|
||||
{@const max = getMax(rows, (r) => r.valor)}
|
||||
<!-- Eixos -->
|
||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" />
|
||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" />
|
||||
|
||||
<!-- Grid lines -->
|
||||
{#each [0,1,2,3,4,5] as t}
|
||||
{@const val = Math.round((max/5) * t)}
|
||||
{@const y = chartHeight - padding.bottom - scaleY(val, max)}
|
||||
<line x1={padding.left - 4} y1={y} x2={padding.left} y2={y} stroke="currentColor" stroke-opacity="0.3" />
|
||||
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" />
|
||||
<text x={padding.left - 8} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text>
|
||||
{/each}
|
||||
<!-- Eixo X (nomes) -->
|
||||
|
||||
<!-- Eixos -->
|
||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
|
||||
<!-- Area fill (camada) -->
|
||||
<path
|
||||
d={createAreaPath(rows, (r) => r.valor, max)}
|
||||
fill="url(#gradient-salary)"
|
||||
opacity="0.7"
|
||||
/>
|
||||
|
||||
<!-- Line -->
|
||||
<polyline
|
||||
points={rows.map((r, i) => {
|
||||
const x = getX(i, rows.length);
|
||||
const y = chartHeight - padding.bottom - scaleY(r.valor, max);
|
||||
return `${x},${y}`;
|
||||
}).join(' ')}
|
||||
fill="none"
|
||||
stroke="rgb(59, 130, 246)"
|
||||
stroke-width="3"
|
||||
/>
|
||||
|
||||
<!-- Data points -->
|
||||
{#each rows as r, i}
|
||||
<text x={barX(i, rows.length) + barW(rows.length) / 2} y={chartHeight - padding.bottom + 28} text-anchor="middle" class="text-xs">
|
||||
{r.nome}
|
||||
</text>
|
||||
{/each}
|
||||
<!-- Barras -->
|
||||
{#each rows as r, i}
|
||||
<rect
|
||||
x={barX(i, rows.length)}
|
||||
y={chartHeight - padding.bottom - scaleY(r.valor, max)}
|
||||
width={barW(rows.length)}
|
||||
height={scaleY(r.valor, max)}
|
||||
class="fill-primary/80"
|
||||
/>
|
||||
<!-- Valor fixo acima da barra -->
|
||||
<text x={barX(i, rows.length) + barW(rows.length)/2} y={chartHeight - padding.bottom - scaleY(r.valor, max) - 6} text-anchor="middle" class="text-[10px] font-semibold">
|
||||
{@const x = getX(i, rows.length)}
|
||||
{@const y = chartHeight - padding.bottom - scaleY(r.valor, max)}
|
||||
<circle cx={x} cy={y} r="5" fill="rgb(59, 130, 246)" stroke="white" stroke-width="2" />
|
||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-primary">
|
||||
{`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
<!-- Eixo X labels -->
|
||||
{#each rows as r, i}
|
||||
{@const x = getX(i, rows.length)}
|
||||
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70">
|
||||
<div class="flex items-center justify-center text-center">
|
||||
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;">
|
||||
{r.nome}
|
||||
</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
{/each}
|
||||
|
||||
<!-- Gradient definition -->
|
||||
<defs>
|
||||
<linearGradient id="gradient-salary" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.8" />
|
||||
<stop offset="100%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo -->
|
||||
<div class="card bg-base-100 shadow overflow-hidden">
|
||||
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart -->
|
||||
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Quantidade de Funcionários por Símbolo</h3>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de barras: quantidade por símbolo">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-secondary/10 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3>
|
||||
<p class="text-sm text-base-content/60">Quantidade de funcionários alocados em cada símbolo</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto bg-base-100 rounded-lg p-4">
|
||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo">
|
||||
{#if rows.length === 0}
|
||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
||||
{:else}
|
||||
{@const maxC = getMax(rows, (r) => r.count)}
|
||||
<!-- Eixos -->
|
||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" />
|
||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" />
|
||||
|
||||
<!-- Grid lines -->
|
||||
{#each [0,1,2,3,4,5] as t}
|
||||
{@const val = Math.round((maxC/5) * t)}
|
||||
{@const y = chartHeight - padding.bottom - scaleY(val, Math.max(1, maxC))}
|
||||
<line x1={padding.left - 4} y1={y} x2={padding.left} y2={y} stroke="currentColor" stroke-opacity="0.3" />
|
||||
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" />
|
||||
<text x={padding.left - 6} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{val}</text>
|
||||
{/each}
|
||||
|
||||
<!-- Eixos -->
|
||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
||||
|
||||
<!-- Area fill (camada) -->
|
||||
<path
|
||||
d={createAreaPath(rows, (r) => r.count, Math.max(1, maxC))}
|
||||
fill="url(#gradient-count)"
|
||||
opacity="0.7"
|
||||
/>
|
||||
|
||||
<!-- Line -->
|
||||
<polyline
|
||||
points={rows.map((r, i) => {
|
||||
const x = getX(i, rows.length);
|
||||
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
|
||||
return `${x},${y}`;
|
||||
}).join(' ')}
|
||||
fill="none"
|
||||
stroke="rgb(236, 72, 153)"
|
||||
stroke-width="3"
|
||||
/>
|
||||
|
||||
<!-- Data points -->
|
||||
{#each rows as r, i}
|
||||
<text x={barX(i, rows.length) + barW(rows.length) / 2} y={chartHeight - padding.bottom + 28} text-anchor="middle" class="text-xs">
|
||||
{r.nome}
|
||||
</text>
|
||||
{/each}
|
||||
{#each rows as r, i}
|
||||
<rect
|
||||
x={barX(i, rows.length)}
|
||||
y={chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))}
|
||||
width={barW(rows.length)}
|
||||
height={scaleY(r.count, Math.max(1, maxC))}
|
||||
class="fill-secondary/80"
|
||||
/>
|
||||
<text x={barX(i, rows.length) + barW(rows.length)/2} y={chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC)) - 6} text-anchor="middle" class="text-[10px] font-semibold">
|
||||
{@const x = getX(i, rows.length)}
|
||||
{@const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))}
|
||||
<circle cx={x} cy={y} r="5" fill="rgb(236, 72, 153)" stroke="white" stroke-width="2" />
|
||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-secondary">
|
||||
{r.count}
|
||||
</text>
|
||||
{/each}
|
||||
|
||||
<!-- Eixo X labels -->
|
||||
{#each rows as r, i}
|
||||
{@const x = getX(i, rows.length)}
|
||||
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70">
|
||||
<div class="flex items-center justify-center text-center">
|
||||
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;">
|
||||
{r.nome}
|
||||
</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
{/each}
|
||||
|
||||
<!-- Gradient definition -->
|
||||
<defs>
|
||||
<linearGradient id="gradient-count" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.8" />
|
||||
<stop offset="100%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -73,47 +73,112 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 pb-32">
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>Símbolos</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-green-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Símbolos Cadastrados</h1>
|
||||
<p class="text-base-content/70">Gerencie cargos comissionados e funções gratificadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/recursos-humanos/simbolos/cadastro" class="btn btn-primary btn-lg gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Novo Símbolo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-3xl font-bold text-brand-dark">Símbolos</h2>
|
||||
<a href="/recursos-humanos/simbolos/cadastro" class="btn btn-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Novo Símbolo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-3 items-end">
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_nome"><span class="label-text">Nome</span></label>
|
||||
<input id="symbol_nome" class="input input-bordered" bind:value={filtroNome} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_tipo"><span class="label-text">Tipo</span></label>
|
||||
<select id="symbol_tipo" class="select select-bordered" bind:value={filtroTipo}>
|
||||
<option value="">Todos</option>
|
||||
<option value="cargo_comissionado">Cargo comissionado</option>
|
||||
<option value="funcao_gratificada">Função gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_desc"><span class="label-text">Descrição</span></label>
|
||||
<input id="symbol_desc" class="input input-bordered" bind:value={filtroDescricao} />
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<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="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros de Pesquisa
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_nome">
|
||||
<span class="label-text font-semibold">Nome do Símbolo</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_nome"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar por nome..."
|
||||
bind:value={filtroNome}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_tipo">
|
||||
<span class="label-text font-semibold">Tipo</span>
|
||||
</label>
|
||||
<select id="symbol_tipo" class="select select-bordered focus:select-primary" bind:value={filtroTipo}>
|
||||
<option value="">Todos os tipos</option>
|
||||
<option value="cargo_comissionado">Cargo Comissionado</option>
|
||||
<option value="funcao_gratificada">Função Gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_desc">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_desc"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar na descrição..."
|
||||
bind:value={filtroDescricao}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if filtroNome || filtroTipo || filtroDescricao}
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm gap-2"
|
||||
onclick={() => {
|
||||
filtroNome = "";
|
||||
filtroTipo = "";
|
||||
filtroDescricao = "";
|
||||
}}
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,38 +187,42 @@
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto bg-base-100 rounded-lg shadow-sm mb-2" class:overflow-y-auto={needsScroll} style={needsScroll ? "max-height: calc(100vh - 180px);" : "overflow-y: visible;"}>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Tipo</th>
|
||||
<th>Valor Referência</th>
|
||||
<th>Valor Vencimento</th>
|
||||
<th>Valor Total</th>
|
||||
<th>Descrição</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if filtered.length > 0}
|
||||
{#each filtered as simbolo}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{simbolo.nome}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
class:badge-primary={simbolo.tipo === "cargo_comissionado"}
|
||||
class:badge-secondary={simbolo.tipo === "funcao_gratificada"}
|
||||
>
|
||||
{getTipoLabel(simbolo.tipo)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{simbolo.repValor ? formatMoney(simbolo.repValor) : "—"}</td>
|
||||
<td>{simbolo.vencValor ? formatMoney(simbolo.vencValor) : "—"}</td>
|
||||
<td class="font-semibold">{formatMoney(simbolo.valor)}</td>
|
||||
<td class="max-w-xs truncate">{simbolo.descricao}</td>
|
||||
<td class="text-right">
|
||||
<!-- Tabela de Símbolos -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-y-auto" style="max-height: {filtered.length > 8 ? '600px' : 'none'};">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="sticky top-0 bg-base-200 z-10">
|
||||
<tr>
|
||||
<th class="font-bold">Nome</th>
|
||||
<th class="font-bold">Tipo</th>
|
||||
<th class="font-bold">Valor Referência</th>
|
||||
<th class="font-bold">Valor Vencimento</th>
|
||||
<th class="font-bold">Valor Total</th>
|
||||
<th class="font-bold">Descrição</th>
|
||||
<th class="text-right font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if filtered.length > 0}
|
||||
{#each filtered as simbolo}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{simbolo.nome}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
class:badge-primary={simbolo.tipo === "cargo_comissionado"}
|
||||
class:badge-secondary={simbolo.tipo === "funcao_gratificada"}
|
||||
>
|
||||
{getTipoLabel(simbolo.tipo)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{simbolo.repValor ? formatMoney(simbolo.repValor) : "—"}</td>
|
||||
<td>{simbolo.vencValor ? formatMoney(simbolo.vencValor) : "—"}</td>
|
||||
<td class="font-semibold">{formatMoney(simbolo.valor)}</td>
|
||||
<td class="max-w-xs truncate">{simbolo.descricao}</td>
|
||||
<td class="text-right">
|
||||
<div class="dropdown dropdown-end" class:dropdown-open={openMenuId === simbolo._id}>
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick={() => toggleMenu(simbolo._id)}>
|
||||
<svg
|
||||
@@ -201,20 +270,28 @@
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center opacity-70">Nenhum símbolo encontrado com os filtros atuais.</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center opacity-70 py-8">Nenhum símbolo encontrado com os filtros atuais.</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre resultados -->
|
||||
<div class="mt-4 text-sm text-base-content/70 text-center">
|
||||
Exibindo {filtered.length} de {list.length} símbolo(s)
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
<dialog id="delete_modal" class="modal">
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
}
|
||||
|
||||
let notice = $state<{ kind: "success" | "error"; text: string } | null>(null);
|
||||
|
||||
function getTotalPreview(): string {
|
||||
if (tipo !== "cargo_comissionado") return "";
|
||||
const r = unmaskCurrencyToDotDecimal(form.getFieldValue("refValor"));
|
||||
@@ -77,303 +78,400 @@
|
||||
valor: !isCargo ? unmaskCurrencyToDotDecimal(value.valor) : undefined,
|
||||
};
|
||||
|
||||
const res = await client.mutation(api.simbolos.create, payload);
|
||||
|
||||
if (res) {
|
||||
formApi.reset();
|
||||
notice = { kind: "success", text: "Símbolo cadastrado com sucesso." };
|
||||
setTimeout(() => goto("/recursos-humanos/simbolos"), 600);
|
||||
} else {
|
||||
console.log("erro ao registrar cliente");
|
||||
notice = { kind: "error", text: "Erro ao cadastrar símbolo." };
|
||||
try {
|
||||
const res = await client.mutation(api.simbolos.create, payload);
|
||||
if (res) {
|
||||
formApi.reset();
|
||||
notice = { kind: "success", text: "Símbolo cadastrado com sucesso!" };
|
||||
setTimeout(() => goto("/recursos-humanos/simbolos"), 1500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
notice = { kind: "error", text: error.message || "Erro ao cadastrar símbolo." };
|
||||
}
|
||||
},
|
||||
defaultValues,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="max-w-3xl mx-auto p-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
{#if notice}
|
||||
<div
|
||||
class="alert"
|
||||
class:alert-success={notice.kind === "success"}
|
||||
class:alert-error={notice.kind === "error"}
|
||||
>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<h2 class="card-title text-3xl">Cadastro de Símbolos</h2>
|
||||
<p class="opacity-70">
|
||||
Preencha os campos abaixo para cadastrar um novo símbolo.
|
||||
</p>
|
||||
<main class="container mx-auto px-4 py-4 max-w-4xl">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li><a href="/recursos-humanos/simbolos" class="text-primary hover:underline">Símbolos</a></li>
|
||||
<li>Cadastrar</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-green-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Cadastro de Símbolo</h1>
|
||||
<p class="text-base-content/70">Preencha os campos abaixo para cadastrar um novo cargo ou função</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
<span class="label-text font-medium"
|
||||
>Símbolo <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
value={state.value}
|
||||
placeholder="Ex.: DAS-1"
|
||||
class="input input-bordered w-full"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = target.value;
|
||||
handleChange(value);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt opacity-60"
|
||||
>Informe o nome identificador do símbolo.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
name="descricao"
|
||||
validators={{ onChange: schema.shape.descricao }}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="descricao">
|
||||
<span class="label-text font-medium"
|
||||
>Descrição <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
value={state.value}
|
||||
placeholder="Ex.: Cargo de Apoio 1"
|
||||
class="input input-bordered w-full"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = target.value;
|
||||
handleChange(value);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt opacity-60"
|
||||
>Descreva brevemente o símbolo.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
name="tipo"
|
||||
validators={{
|
||||
onChange: ({ value }) => (value ? undefined : "Obrigatório"),
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="tipo">
|
||||
<span class="label-text font-medium"
|
||||
>Tipo <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
{name}
|
||||
class="select select-bordered w-full"
|
||||
bind:value={tipo}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const value = target.value;
|
||||
handleChange(value);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
>
|
||||
<option value="cargo_comissionado">Cargo comissionado</option>
|
||||
<option value="funcao_gratificada">Função gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
{#if tipo === "cargo_comissionado"}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form.Field
|
||||
name="vencValor"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "cargo_comissionado" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="vencValor">
|
||||
<span class="label-text font-medium"
|
||||
>Valor de Vencimento <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
value={state.value}
|
||||
placeholder="Ex.: 1200,00"
|
||||
class="input input-bordered w-full"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt opacity-60"
|
||||
>Valor efetivo de vencimento.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
name="refValor"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "cargo_comissionado" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="refValor">
|
||||
<span class="label-text font-medium"
|
||||
>Valor de Referência <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
value={state.value}
|
||||
placeholder="Ex.: 1000,00"
|
||||
class="input input-bordered w-full"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt opacity-60"
|
||||
>Valor base de referência.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
{#if getTotalPreview()}
|
||||
<div class="alert bg-base-200">
|
||||
<span>Total previsto: R$ {getTotalPreview()}</span>
|
||||
</div>
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div
|
||||
class="alert mb-6 shadow-lg"
|
||||
class:alert-success={notice.kind === "success"}
|
||||
class:alert-error={notice.kind === "error"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
{:else}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<form
|
||||
class="space-y-6"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title text-xl border-b pb-3">Informações Básicas</h2>
|
||||
|
||||
<!-- Nome do Símbolo -->
|
||||
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="nome">
|
||||
<span class="label-text font-semibold">
|
||||
Nome do Símbolo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
id="nome"
|
||||
value={state.value}
|
||||
placeholder="Ex.: DAS-1, CAA-2, FDA-3"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Informe o código identificador do símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Descrição -->
|
||||
<form.Field name="descricao" validators={{ onChange: schema.shape.descricao }}>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="descricao">
|
||||
<span class="label-text font-semibold">
|
||||
Descrição <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
{name}
|
||||
id="descricao"
|
||||
value={state.value}
|
||||
placeholder="Ex.: Cargo de Direção e Assessoramento Superior - Nível 1"
|
||||
class="textarea textarea-bordered w-full h-24 focus:textarea-primary"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Descreva detalhadamente o símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Tipo -->
|
||||
<form.Field
|
||||
name="valor"
|
||||
name="tipo"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "funcao_gratificada" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
onChange: ({ value }) => (value ? undefined : "Obrigatório"),
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="valor">
|
||||
<span class="label-text font-medium"
|
||||
>Valor <span class="text-error">*</span></span
|
||||
>
|
||||
<label class="label" for="tipo">
|
||||
<span class="label-text font-semibold">
|
||||
Tipo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
<select
|
||||
{name}
|
||||
value={state.value}
|
||||
placeholder="Ex.: 1.500,00"
|
||||
class="input input-bordered w-full"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
id="tipo"
|
||||
class="select select-bordered w-full focus:select-primary"
|
||||
bind:value={tipo}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
const target = e.target as HTMLSelectElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt opacity-60"
|
||||
>Informe o valor da função gratificada.</span
|
||||
>
|
||||
</div>
|
||||
>
|
||||
<option value="cargo_comissionado">Cargo Comissionado (CC)</option>
|
||||
<option value="funcao_gratificada">Função Gratificada (FG)</option>
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Selecione se é um cargo comissionado ou função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
{/if}
|
||||
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
canSubmit: state.canSubmit,
|
||||
isSubmitting: state.isSubmitting,
|
||||
})}
|
||||
>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card-actions justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto("/recursos-humanos/simbolos")}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
class:loading={isSubmitting}
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
<span>Cadastrar Símbolo</span>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Card de Valores -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
Valores Financeiros
|
||||
<span class="badge badge-primary badge-lg ml-2">
|
||||
{tipo === "cargo_comissionado" ? "Cargo Comissionado" : "Função Gratificada"}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{#if tipo === "cargo_comissionado"}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Valor de Vencimento -->
|
||||
<form.Field
|
||||
name="vencValor"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "cargo_comissionado" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="vencValor">
|
||||
<span class="label-text font-semibold">
|
||||
Valor de Vencimento <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="vencValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor base de vencimento
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Valor de Referência -->
|
||||
<form.Field
|
||||
name="refValor"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "cargo_comissionado" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="refValor">
|
||||
<span class="label-text font-semibold">
|
||||
Valor de Referência <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="refValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor de referência do cargo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<!-- Preview do Total -->
|
||||
{#if getTotalPreview()}
|
||||
<div class="alert alert-info shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<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"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Valor Total Calculado</h3>
|
||||
<div class="text-2xl font-bold mt-1">R$ {getTotalPreview()}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Valor da Função Gratificada -->
|
||||
<form.Field
|
||||
name="valor"
|
||||
validators={{
|
||||
onChange: ({ value }) =>
|
||||
form.getFieldValue("tipo") === "funcao_gratificada" && !value
|
||||
? "Obrigatório"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{#snippet children({ name, state, handleChange })}
|
||||
<div class="form-control">
|
||||
<label class="label" for="valor">
|
||||
<span class="label-text font-semibold">
|
||||
Valor da Função Gratificada <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="valor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor mensal da função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botões de Ação -->
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
canSubmit: state.canSubmit,
|
||||
isSubmitting: state.isSubmitting,
|
||||
})}
|
||||
>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-lg"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto("/recursos-humanos/simbolos")}
|
||||
>
|
||||
<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>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Cadastrando...
|
||||
{: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="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Cadastrar Símbolo
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user