feat: implement vacation management system with request approval, notification handling, and employee training tracking; enhance UI components for improved user experience

This commit is contained in:
2025-10-29 22:05:29 -03:00
parent f219340cd8
commit 16bcd2ac25
21 changed files with 3910 additions and 617 deletions

View File

@@ -79,6 +79,34 @@
},
],
},
{
categoria: "Gestão de Férias e Licenças",
descricao: "Controle de férias, atestados e licenças",
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="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>`,
gradient: "from-purple-500/10 to-purple-600/20",
accentColor: "text-purple-600",
bgIcon: "bg-purple-500/20",
opcoes: [
{
nome: "Gestão de Férias",
descricao: "Controlar períodos de férias",
href: "/recursos-humanos/ferias",
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="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>`,
},
{
nome: "Atestados & Licenças",
descricao: "Registrar atestados e licenças",
href: "/recursos-humanos/atestados-licencas",
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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>`,
},
],
},
];
</script>

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import { goto } from "$app/navigation";
</script>
<main class="container mx-auto px-4 py-6 max-w-7xl">
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
<li>Atestados & Licenças</li>
</ul>
</div>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-purple-500/20 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-primary">Atestados & Licenças</h1>
<p class="text-base-content/70">Registro de atestados médicos e licenças</p>
</div>
</div>
<button class="btn btn-ghost gap-2" onclick={() => goto("/recursos-humanos")}>
<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>
<!-- Alert de desenvolvimento -->
<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">Módulo em Desenvolvimento</h3>
<div class="text-sm">Esta funcionalidade está em desenvolvimento e estará disponível em breve.</div>
</div>
</div>
<!-- Preview do que virá -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6 opacity-60">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Registrar Atestado</h2>
<p class="text-sm text-base-content/70">Cadastre atestados médicos</p>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-disabled">Em breve</button>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Registrar Licença</h2>
<p class="text-sm text-base-content/70">Cadastre licenças e afastamentos</p>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-disabled">Em breve</button>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Histórico</h2>
<p class="text-sm text-base-content/70">Consulte histórico de atestados e licenças</p>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-disabled">Em breve</button>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Estatísticas</h2>
<p class="text-sm text-base-content/70">Visualize estatísticas e relatórios</p>
<div class="card-actions justify-end mt-4">
<button class="btn btn-sm btn-disabled">Em breve</button>
</div>
</div>
</div>
</div>
</main>

View File

@@ -0,0 +1,285 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
// Buscar todas as solicitações (RH vê tudo)
const todasSolicitacoesQuery = useQuery(api.ferias.listarTodas, {});
const todosFuncionariosQuery = useQuery(api.funcionarios.getAll, {});
let filtroStatus = $state<string>("todos");
let filtroTime = $state<string>("todos");
let filtroBusca = $state("");
const solicitacoes = $derived(todasSolicitacoesQuery?.data || []);
const funcionarios = $derived(todosFuncionariosQuery?.data || []);
// Filtrar solicitações
const solicitacoesFiltradas = $derived(
solicitacoes.filter((s: any) => {
// Filtro de status
if (filtroStatus !== "todos" && s.status !== filtroStatus) return false;
// Filtro de time
if (filtroTime !== "todos" && s.time?._id !== filtroTime) return false;
// Filtro de busca
if (filtroBusca && !s.funcionario?.nome.toLowerCase().includes(filtroBusca.toLowerCase())) {
return false;
}
return true;
})
);
// Estatísticas
const stats = $derived({
total: solicitacoes.length,
aguardando: solicitacoes.filter((s: any) => s.status === "aguardando_aprovacao").length,
aprovadas: solicitacoes.filter((s: any) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length,
reprovadas: solicitacoes.filter((s: any) => s.status === "reprovado").length,
emFerias: funcionarios.filter((f: any) => f.statusFerias === "em_ferias").length,
});
// Times únicos para filtro
const timesDisponiveis = $derived(
Array.from(new Set(solicitacoes.map((s: any) => s.time).filter(Boolean)))
);
function getStatusBadge(status: string) {
const badges: Record<string, string> = {
aguardando_aprovacao: "badge-warning",
aprovado: "badge-success",
reprovado: "badge-error",
data_ajustada_aprovada: "badge-info",
};
return badges[status] || "badge-neutral";
}
function getStatusTexto(status: string) {
const textos: Record<string, string> = {
aguardando_aprovacao: "Aguardando",
aprovado: "Aprovado",
reprovado: "Reprovado",
data_ajustada_aprovada: "Ajustado",
};
return textos[status] || status;
}
function formatarData(dataISO: string) {
return new Date(dataISO).toLocaleDateString("pt-BR");
}
</script>
<main class="container mx-auto px-4 py-6 max-w-7xl">
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
<li>Gestão de Férias</li>
</ul>
</div>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-purple-500/20 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-primary">Dashboard de Férias</h1>
<p class="text-base-content/70">Visão geral de todas as solicitações e funcionários</p>
</div>
</div>
<button class="btn btn-ghost gap-2" onclick={() => goto("/recursos-humanos")}>
<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>
<!-- Estatísticas -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<div class="stat bg-base-100 shadow-lg rounded-box border border-base-300">
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="stat-title">Total</div>
<div class="stat-value text-primary">{stats.total}</div>
<div class="stat-desc">Solicitações</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-warning/30">
<div class="stat-figure text-warning">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Aguardando</div>
<div class="stat-value text-warning">{stats.aguardando}</div>
<div class="stat-desc">Pendentes</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-success/30">
<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">Aprovadas</div>
<div class="stat-value text-success">{stats.aprovadas}</div>
<div class="stat-desc">Deferidas</div>
</div>
<div class="stat bg-base-100 shadow-lg rounded-box border border-error/30">
<div class="stat-figure text-error">
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Reprovadas</div>
<div class="stat-value text-error">{stats.reprovadas}</div>
<div class="stat-desc">Indeferidas</div>
</div>
<div class="stat bg-gradient-to-br from-purple-500/10 to-purple-600/20 shadow-lg rounded-box border-2 border-purple-500/30">
<div class="stat-figure text-purple-600">
<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="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="stat-title">Em Férias</div>
<div class="stat-value text-purple-600">{stats.emFerias}</div>
<div class="stat-desc">Agora</div>
</div>
</div>
<!-- Filtros -->
<div class="card bg-base-100 shadow-lg mb-6">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Filtros</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Busca -->
<div class="form-control">
<label class="label" for="busca">
<span class="label-text">Buscar Funcionário</span>
</label>
<input
id="busca"
type="text"
placeholder="Digite o nome..."
class="input input-bordered"
bind:value={filtroBusca}
/>
</div>
<!-- Filtro Status -->
<div class="form-control">
<label class="label" for="status">
<span class="label-text">Status</span>
</label>
<select id="status" class="select select-bordered" bind:value={filtroStatus}>
<option value="todos">Todos</option>
<option value="aguardando_aprovacao">Aguardando Aprovação</option>
<option value="aprovado">Aprovado</option>
<option value="reprovado">Reprovado</option>
<option value="data_ajustada_aprovada">Data Ajustada</option>
</select>
</div>
<!-- Filtro Time -->
<div class="form-control">
<label class="label" for="time">
<span class="label-text">Time</span>
</label>
<select id="time" class="select select-bordered" bind:value={filtroTime}>
<option value="todos">Todos os Times</option>
{#each timesDisponiveis as time}
{#if time}
<option value={time._id}>{time.nome}</option>
{/if}
{/each}
</select>
</div>
</div>
</div>
</div>
<!-- Lista de Solicitações -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h2 class="card-title text-lg mb-4">
Solicitações ({solicitacoesFiltradas.length})
</h2>
{#if solicitacoesFiltradas.length === 0}
<div class="alert">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info 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>
<span>Nenhuma solicitação encontrada com os filtros aplicados.</span>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Funcionário</th>
<th>Time</th>
<th>Ano</th>
<th>Períodos</th>
<th>Total Dias</th>
<th>Status</th>
<th>Solicitado em</th>
</tr>
</thead>
<tbody>
{#each solicitacoesFiltradas as solicitacao}
<tr>
<td>
<div class="flex items-center gap-3">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-10">
<span class="text-xs">{solicitacao.funcionario?.nome.substring(0, 2).toUpperCase()}</span>
</div>
</div>
<div>
<div class="font-bold">{solicitacao.funcionario?.nome}</div>
<div class="text-xs opacity-50">{solicitacao.funcionario?.matricula || "S/N"}</div>
</div>
</div>
</td>
<td>
{#if solicitacao.time}
<div class="badge badge-outline" style="border-color: {solicitacao.time.cor}">
{solicitacao.time.nome}
</div>
{:else}
<span class="text-base-content/50 text-xs">Sem time</span>
{/if}
</td>
<td>{solicitacao.anoReferencia}</td>
<td>{solicitacao.periodos.length} período(s)</td>
<td class="font-bold">{solicitacao.periodos.reduce((acc: number, p: any) => acc + p.diasCorridos, 0)} dias</td>
<td>
<div class={`badge ${getStatusBadge(solicitacao.status)}`}>
{getStatusTexto(solicitacao.status)}
</div>
</td>
<td class="text-xs">{new Date(solicitacao._creationTime).toLocaleDateString("pt-BR")}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
</main>

View File

@@ -10,8 +10,6 @@
let list: Array<any> = [];
let filtered: Array<any> = [];
let selectedId: string | null = null;
let deletingId: string | null = null;
let toDelete: { id: string; nome: string } | null = null;
let openMenuId: string | null = null;
let funcionarioParaImprimir: any = null;
@@ -42,15 +40,6 @@
if (selectedId) goto(`/recursos-humanos/funcionarios/${selectedId}/editar`);
}
function openDeleteModal(id: string, nome: string) {
toDelete = { id, nome };
(document.getElementById("delete_modal_func") as HTMLDialogElement)?.showModal();
}
function closeDeleteModal() {
toDelete = null;
(document.getElementById("delete_modal_func") as HTMLDialogElement)?.close();
}
async function openPrintModal(funcionarioId: string) {
try {
const data = await client.query(api.funcionarios.getFichaCompleta, {
@@ -62,17 +51,6 @@
alert("Erro ao carregar dados para impressão");
}
}
async function confirmDelete() {
if (!toDelete) return;
try {
deletingId = toDelete.id;
await client.mutation(api.funcionarios.remove, { id: toDelete.id } as any);
closeDeleteModal();
await load();
} finally {
deletingId = null;
}
}
function navCadastro() { goto("/recursos-humanos/funcionarios/cadastro"); }
@@ -231,7 +209,6 @@
<li><a href={`/recursos-humanos/funcionarios/${f._id}/editar`}>Editar</a></li>
<li><a href={`/recursos-humanos/funcionarios/${f._id}/documentos`}>Ver Documentos</a></li>
<li><button onclick={() => openPrintModal(f._id)}>Imprimir Ficha</button></li>
<li class="border-t mt-1 pt-1"><button class="text-error" onclick={() => openDeleteModal(f._id, f.nome)}>Excluir</button></li>
</ul>
</div>
</td>
@@ -249,36 +226,6 @@
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>
<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>
<!-- Modal de Impressão -->
{#if funcionarioParaImprimir}
<PrintModal

View File

@@ -17,9 +17,11 @@
let funcionario = $state<any>(null);
let simbolo = $state<any>(null);
let cursos = $state<any[]>([]);
let documentosUrls = $state<Record<string, string | null>>({});
let loading = $state(true);
let showPrintModal = $state(false);
let showPrintFinanceiro = $state(false);
async function load() {
try {
@@ -35,6 +37,7 @@
funcionario = data;
simbolo = data.simbolo;
cursos = data.cursos || [];
// Carregar URLs dos documentos
try {
@@ -126,12 +129,87 @@
</svg>
Imprimir Ficha
</button>
<button class="btn btn-info gap-2" onclick={() => showPrintFinanceiro = true}>
<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 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>
Imprimir Dados Financeiros
</button>
</div>
</div>
</div>
<!-- Dados Financeiros - Destaque -->
{#if simbolo}
<div class="card bg-gradient-to-br from-success/10 to-success/20 shadow-xl mb-6 border border-success/30">
<div class="card-body">
<h3 class="card-title text-xl border-b pb-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-success" 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>
Dados Financeiros
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="stat bg-base-100/50 rounded-lg p-4">
<div class="stat-title text-xs">Símbolo</div>
<div class="stat-value text-2xl">{simbolo.nome}</div>
<div class="stat-desc text-xs">{simbolo.tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}</div>
</div>
{#if funcionario.simboloTipo === 'cargo_comissionado'}
<div class="stat bg-base-100/50 rounded-lg p-4">
<div class="stat-title text-xs">Vencimento</div>
<div class="stat-value text-2xl text-info">R$ {simbolo.vencValor}</div>
<div class="stat-desc text-xs">Valor base</div>
</div>
<div class="stat bg-base-100/50 rounded-lg p-4">
<div class="stat-title text-xs">Representação</div>
<div class="stat-value text-2xl text-warning">R$ {simbolo.repValor}</div>
<div class="stat-desc text-xs">Adicional</div>
</div>
{/if}
<div class="stat bg-success/20 rounded-lg p-4 border-2 border-success/40">
<div class="stat-title text-xs font-bold">Total</div>
<div class="stat-value text-3xl text-success">R$ {simbolo.valor}</div>
<div class="stat-desc text-xs">Remuneração total</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Status de Férias -->
<div class="card bg-gradient-to-br from-purple-500/10 to-purple-600/20 shadow-xl mb-6 border border-purple-500/30">
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-3 bg-purple-500/20 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 class="font-bold text-lg">Status Atual</h3>
<div class="flex items-center gap-2 mt-1">
{#if funcionario.statusFerias === "em_ferias"}
<div class="badge badge-warning badge-lg">🏖️ Em Férias</div>
{:else}
<div class="badge badge-success badge-lg">✅ Ativo</div>
{/if}
</div>
</div>
</div>
<a href="/recursos-humanos/ferias" class="btn btn-primary btn-sm gap-2">
<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="M12 4v16m8-8H4" />
</svg>
Gerenciar Férias
</a>
</div>
</div>
</div>
<!-- Grid de Cards -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Coluna 1: Dados Pessoais -->
<div class="space-y-6">
<!-- Informações Pessoais -->
@@ -196,8 +274,45 @@
{/if}
</div>
<!-- Coluna 2: Documentos e Formação -->
<!-- Coluna 2: Cargo, Formação e Cursos -->
<div class="space-y-6">
<!-- Cargo e Vínculo -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg border-b pb-2 mb-3">Cargo e Vínculo</h3>
<div class="space-y-2 text-sm">
<div><span class="font-semibold">Tipo:</span> {funcionario.simboloTipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}</div>
{#if simbolo}
<div><span class="font-semibold">Símbolo:</span> {simbolo.nome}</div>
<div class="text-xs text-base-content/70">{simbolo.descricao}</div>
{/if}
{#if funcionario.descricaoCargo}
<div class="mt-2"><span class="font-semibold">Descrição:</span> {funcionario.descricaoCargo}</div>
{/if}
{#if funcionario.admissaoData}
<div class="mt-2"><span class="font-semibold">Data Admissão:</span> {funcionario.admissaoData}</div>
{/if}
{#if funcionario.nomeacaoPortaria}
<div><span class="font-semibold">Portaria:</span> {funcionario.nomeacaoPortaria}</div>
{/if}
{#if funcionario.nomeacaoData}
<div><span class="font-semibold">Data Nomeação:</span> {funcionario.nomeacaoData}</div>
{/if}
{#if funcionario.nomeacaoDOE}
<div><span class="font-semibold">DOE:</span> {funcionario.nomeacaoDOE}</div>
{/if}
{#if funcionario.pertenceOrgaoPublico}
<div class="mt-2"><span class="font-semibold">Pertence Órgão Público:</span> Sim</div>
{#if funcionario.orgaoOrigem}
<div><span class="font-semibold">Órgão Origem:</span> {funcionario.orgaoOrigem}</div>
{/if}
{/if}
{#if funcionario.aposentado && funcionario.aposentado !== 'nao'}
<div><span class="font-semibold">Aposentado:</span> {getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)}</div>
{/if}
</div>
</div>
</div>
<!-- Documentos Pessoais -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
@@ -253,6 +368,48 @@
</div>
{/if}
<!-- Cursos e Treinamentos -->
{#if cursos && cursos.length > 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg border-b pb-2 mb-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="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Cursos e Treinamentos
</h3>
<div class="space-y-3">
{#each cursos as curso}
<div class="flex items-start gap-3 p-3 bg-base-200 rounded-lg">
<div class="flex-1">
<p class="font-semibold text-sm">{curso.descricao}</p>
<p class="text-xs text-base-content/70 mt-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 inline mr-1" 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>
{curso.data}
</p>
</div>
{#if curso.certificadoUrl}
<a
href={curso.certificadoUrl}
target="_blank"
rel="noopener noreferrer"
class="btn btn-xs btn-primary gap-1"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Certificado
</a>
{/if}
</div>
{/each}
</div>
</div>
</div>
{/if}
<!-- Saúde -->
{#if funcionario.grupoSanguineo || funcionario.fatorRH}
<div class="card bg-base-100 shadow-xl">
@@ -280,47 +437,6 @@
</div>
</div>
</div>
</div>
<!-- Coluna 3: Cargo e Bancário -->
<div class="space-y-6">
<!-- Cargo e Vínculo -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="card-title text-lg border-b pb-2 mb-3">Cargo e Vínculo</h3>
<div class="space-y-2 text-sm">
<div><span class="font-semibold">Tipo:</span> {funcionario.simboloTipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}</div>
{#if simbolo}
<div><span class="font-semibold">Símbolo:</span> {simbolo.nome}</div>
<div class="text-xs text-base-content/70">{simbolo.descricao}</div>
{/if}
{#if funcionario.descricaoCargo}
<div class="mt-2"><span class="font-semibold">Descrição:</span> {funcionario.descricaoCargo}</div>
{/if}
{#if funcionario.admissaoData}
<div class="mt-2"><span class="font-semibold">Data Admissão:</span> {funcionario.admissaoData}</div>
{/if}
{#if funcionario.nomeacaoPortaria}
<div><span class="font-semibold">Portaria:</span> {funcionario.nomeacaoPortaria}</div>
{/if}
{#if funcionario.nomeacaoData}
<div><span class="font-semibold">Data Nomeação:</span> {funcionario.nomeacaoData}</div>
{/if}
{#if funcionario.nomeacaoDOE}
<div><span class="font-semibold">DOE:</span> {funcionario.nomeacaoDOE}</div>
{/if}
{#if funcionario.pertenceOrgaoPublico}
<div class="mt-2"><span class="font-semibold">Pertence Órgão Público:</span> Sim</div>
{#if funcionario.orgaoOrigem}
<div><span class="font-semibold">Órgão Origem:</span> {funcionario.orgaoOrigem}</div>
{/if}
{/if}
{#if funcionario.aposentado && funcionario.aposentado !== 'nao'}
<div><span class="font-semibold">Aposentado:</span> {getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)}</div>
{/if}
</div>
</div>
</div>
<!-- Endereço -->
<div class="card bg-base-100 shadow-xl">
@@ -431,4 +547,103 @@
onClose={() => showPrintModal = false}
/>
{/if}
<!-- Modal de Impressão Dados Financeiros -->
{#if showPrintFinanceiro && simbolo}
<dialog class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-2xl mb-6 border-b pb-3">Dados Financeiros - {funcionario.nome}</h3>
<div class="space-y-4 print:space-y-2" id="dados-financeiros-print">
<!-- Informações Básicas -->
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm font-semibold text-base-content/70">Nome</p>
<p class="text-lg">{funcionario.nome}</p>
</div>
<div>
<p class="text-sm font-semibold text-base-content/70">Matrícula</p>
<p class="text-lg">{funcionario.matricula || 'N/A'}</p>
</div>
<div>
<p class="text-sm font-semibold text-base-content/70">CPF</p>
<p class="text-lg">{maskCPF(funcionario.cpf)}</p>
</div>
<div>
<p class="text-sm font-semibold text-base-content/70">Data Admissão</p>
<p class="text-lg">{funcionario.admissaoData || 'N/A'}</p>
</div>
</div>
<div class="divider"></div>
<!-- Dados Financeiros -->
<div>
<h4 class="font-bold text-lg mb-3">Remuneração</h4>
<div class="space-y-2">
<div class="flex justify-between p-2 bg-base-200 rounded">
<span class="font-semibold">Símbolo:</span>
<span>{simbolo.nome}</span>
</div>
<div class="flex justify-between p-2 bg-base-200 rounded">
<span class="font-semibold">Tipo:</span>
<span>{simbolo.tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}</span>
</div>
{#if funcionario.simboloTipo === 'cargo_comissionado'}
<div class="flex justify-between p-2 bg-info/10 rounded">
<span class="font-semibold">Vencimento:</span>
<span class="text-info font-bold">R$ {simbolo.vencValor}</span>
</div>
<div class="flex justify-between p-2 bg-warning/10 rounded">
<span class="font-semibold">Representação:</span>
<span class="text-warning font-bold">R$ {simbolo.repValor}</span>
</div>
{/if}
<div class="flex justify-between p-3 bg-success/20 rounded border-2 border-success/40">
<span class="font-bold text-lg">TOTAL:</span>
<span class="text-success font-bold text-2xl">R$ {simbolo.valor}</span>
</div>
</div>
</div>
{#if funcionario.contaBradescoNumero}
<div class="divider"></div>
<div>
<h4 class="font-bold text-lg mb-3">Dados Bancários</h4>
<div class="space-y-2">
<div class="flex justify-between p-2 bg-base-200 rounded">
<span class="font-semibold">Banco:</span>
<span>Bradesco</span>
</div>
<div class="flex justify-between p-2 bg-base-200 rounded">
<span class="font-semibold">Agência:</span>
<span>{funcionario.contaBradescoAgencia || 'N/A'}</span>
</div>
<div class="flex justify-between p-2 bg-base-200 rounded">
<span class="font-semibold">Conta:</span>
<span>{funcionario.contaBradescoNumero}{funcionario.contaBradescoDV ? `-${funcionario.contaBradescoDV}` : ''}</span>
</div>
</div>
</div>
{/if}
</div>
<div class="modal-action">
<button
class="btn btn-primary gap-2"
onclick={() => window.print()}
>
<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 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Imprimir
</button>
<button class="btn btn-ghost" onclick={() => showPrintFinanceiro = false}>Fechar</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="button" onclick={() => showPrintFinanceiro = false} aria-label="Fechar modal">Fechar</button>
</form>
</dialog>
{/if}
{/if}

View File

@@ -92,6 +92,25 @@
// Documentos (Storage IDs)
let documentosStorage: Record<string, string | undefined> = $state({});
// Cursos e Treinamentos
interface Curso {
_id?: string;
id: string;
descricao: string;
data: string;
certificadoId?: string;
arquivo?: File;
marcadoParaExcluir?: boolean;
}
let cursos = $state<Curso[]>([]);
let mostrarFormularioCurso = $state(false);
let cursoAtual = $state<Curso>({
id: crypto.randomUUID(),
descricao: "",
data: "",
});
async function loadSimbolos() {
const list = await client.query(api.simbolos.getAll, {} as any);
@@ -170,6 +189,22 @@
documentosStorage[doc.campo] = storageId;
}
});
// Carregar cursos
try {
const cursosData = await client.query(api.cursos.listarPorFuncionario, {
funcionarioId: funcionarioId as any,
});
cursos = cursosData.map((c: any) => ({
_id: c._id,
id: c._id,
descricao: c.descricao,
data: c.data,
certificadoId: c.certificadoId,
}));
} catch (error) {
console.error("Erro ao carregar cursos:", error);
}
} catch (error) {
console.error("Erro ao carregar funcionário:", error);
notice = { kind: "error", text: "Erro ao carregar dados do funcionário" };
@@ -193,6 +228,51 @@
uf = data.uf || "";
} catch {}
}
// Funções de Cursos
function adicionarCurso() {
if (!cursoAtual.descricao.trim() || !cursoAtual.data.trim()) {
notice = { kind: "error", text: "Preencha a descrição e data do curso" };
return;
}
if (cursos.filter(c => !c.marcadoParaExcluir).length >= 7) {
notice = { kind: "error", text: "Máximo de 7 cursos permitidos" };
return;
}
cursos.push({ ...cursoAtual });
cursoAtual = {
id: crypto.randomUUID(),
descricao: "",
data: "",
};
mostrarFormularioCurso = false;
}
function removerCurso(id: string) {
const curso = cursos.find(c => c.id === id);
if (curso && curso._id) {
// Marcar para excluir se já existe no banco
curso.marcadoParaExcluir = true;
} else {
// Remover diretamente se é novo
cursos = cursos.filter(c => c.id !== id);
}
}
async function uploadCertificado(file: File): Promise<string> {
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await result.json();
return storageId;
}
async function handleDocumentoUpload(campo: string, file: File) {
try {
@@ -299,6 +379,45 @@
};
await client.mutation(api.funcionarios.update, { id: funcionarioId as any, ...payload as any });
// Salvar cursos
try {
// Excluir cursos marcados
for (const curso of cursos.filter(c => c.marcadoParaExcluir && c._id)) {
await client.mutation(api.cursos.excluir, { id: curso._id as any });
}
// Adicionar/atualizar cursos
for (const curso of cursos.filter(c => !c.marcadoParaExcluir)) {
let certificadoId = curso.certificadoId;
// Upload de certificado se houver arquivo novo
if (curso.arquivo) {
certificadoId = await uploadCertificado(curso.arquivo);
}
if (curso._id) {
// Atualizar curso existente
await client.mutation(api.cursos.atualizar, {
id: curso._id as any,
descricao: curso.descricao,
data: curso.data,
certificadoId: certificadoId as any,
});
} else {
// Criar novo curso
await client.mutation(api.cursos.criar, {
funcionarioId: funcionarioId as any,
descricao: curso.descricao,
data: curso.data,
certificadoId: certificadoId as any,
});
}
}
} catch (error) {
console.error("Erro ao salvar cursos:", error);
}
notice = { kind: "success", text: "Funcionário atualizado com sucesso!" };
setTimeout(() => goto("/recursos-humanos/funcionarios"), 600);
} catch (e: any) {
@@ -1254,6 +1373,122 @@
</div>
</div>
<!-- Card 7.5: Cursos e Treinamentos -->
<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="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Cursos e Treinamentos
</h2>
<p class="text-sm text-base-content/70">
Gerencie cursos e treinamentos do funcionário (até 7 cursos)
</p>
{#if cursos.filter(c => !c.marcadoParaExcluir).length > 0}
<div class="space-y-2">
<h3 class="font-semibold text-sm">Cursos cadastrados ({cursos.filter(c => !c.marcadoParaExcluir).length}/7)</h3>
{#each cursos.filter(c => !c.marcadoParaExcluir) as curso}
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg">
<div class="flex-1">
<p class="font-semibold text-sm">{curso.descricao}</p>
<p class="text-xs text-base-content/70">{curso.data}</p>
{#if curso.certificadoId}
<p class="text-xs text-success">✓ Com certificado</p>
{/if}
</div>
<button
type="button"
class="btn btn-sm btn-error btn-square"
aria-label="Remover curso"
onclick={() => removerCurso(curso.id)}
>
<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>
{/each}
</div>
{/if}
{#if cursos.filter(c => !c.marcadoParaExcluir).length < 7}
<div class="collapse collapse-arrow border border-base-300 bg-base-200">
<input type="checkbox" bind:checked={mostrarFormularioCurso} />
<div class="collapse-title font-medium">
Adicionar Curso/Treinamento
</div>
<div class="collapse-content">
<div class="space-y-3 pt-2">
<div class="form-control">
<label class="label" for="curso-descricao-edit">
<span class="label-text font-medium">Descrição do Curso</span>
</label>
<input
id="curso-descricao-edit"
type="text"
class="input input-bordered w-full"
bind:value={cursoAtual.descricao}
placeholder="Ex: Gestão de Projetos"
/>
</div>
<div class="form-control">
<label class="label" for="curso-data-edit">
<span class="label-text font-medium">Data de Conclusão</span>
</label>
<input
id="curso-data-edit"
type="text"
class="input input-bordered w-full"
bind:value={cursoAtual.data}
placeholder="Ex: 01/2024"
onchange={(e) => cursoAtual.data = maskDate(e.currentTarget.value)}
/>
</div>
<div class="form-control">
<label class="label" for="curso-certificado-edit">
<span class="label-text font-medium">Certificado/Diploma (opcional)</span>
</label>
<input
id="curso-certificado-edit"
type="file"
class="file-input file-input-bordered w-full"
accept=".pdf,.jpg,.jpeg,.png"
onchange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) cursoAtual.arquivo = file;
}}
/>
</div>
<button
type="button"
class="btn btn-primary btn-sm gap-2 mt-2"
onclick={adicionarCurso}
>
<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="M12 4v16m8-8H4" />
</svg>
Adicionar à Lista
</button>
</div>
</div>
</div>
{:else}
<div class="alert alert-info">
<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>
<span>Limite de 7 cursos atingido</span>
</div>
{/if}
</div>
</div>
<!-- Card 8: Ações -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">

View File

@@ -89,6 +89,46 @@
// Documentos (Storage IDs)
let documentosStorage: Record<string, string | undefined> = $state({});
// Cursos e Treinamentos
let cursos = $state<Array<{
id: string;
descricao: string;
data: string;
certificadoId?: string;
}>>([]);
let mostrarFormularioCurso = $state(false);
let cursoAtual = $state({ descricao: "", data: "", arquivo: null as File | null });
function adicionarCurso() {
if (!cursoAtual.descricao.trim() || !cursoAtual.data.trim()) {
alert("Preencha a descrição e a data do curso");
return;
}
cursos.push({
id: crypto.randomUUID(),
descricao: cursoAtual.descricao,
data: cursoAtual.data,
certificadoId: undefined
});
cursoAtual = { descricao: "", data: "", arquivo: null };
}
function removerCurso(id: string) {
cursos = cursos.filter(c => c.id !== id);
}
async function uploadCertificado(file: File): Promise<string> {
const storageId = await client.mutation(api.documentos.generateUploadUrl, {});
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
const response = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const result = await response.json();
return result.storageId;
}
async function loadSimbolos() {
const list = await client.query(api.simbolos.getAll, {} as any);
simbolos = list.map((s: any) => ({
@@ -140,7 +180,7 @@
async function handleSubmit() {
// Validação básica
if (!nome || !matricula || !cpf || !rg || !nascimento || !email || !telefone) {
if (!nome || !cpf || !rg || !nascimento || !email || !telefone) {
notice = { kind: "error", text: "Preencha todos os campos obrigatórios" };
return;
}
@@ -165,7 +205,7 @@
const payload = {
nome,
matricula,
matricula: matricula.trim() || undefined,
cpf: onlyDigits(cpf),
rg: onlyDigits(rg),
nascimento,
@@ -229,7 +269,28 @@
),
};
await client.mutation(api.funcionarios.create, payload as any);
const novoFuncionarioId = await client.mutation(api.funcionarios.create, payload as any);
// Salvar cursos, se houver
for (const curso of cursos) {
let certificadoId = curso.certificadoId;
// Se houver arquivo para upload, fazer o upload
if (cursoAtual.arquivo && curso.id === cursos[cursos.length - 1].id) {
try {
certificadoId = await uploadCertificado(cursoAtual.arquivo);
} catch (err) {
console.error("Erro ao fazer upload do certificado:", err);
}
}
await client.mutation(api.cursos.criar, {
funcionarioId: novoFuncionarioId,
descricao: curso.descricao,
data: curso.data,
certificadoId: certificadoId as any,
});
}
notice = { kind: "success", text: "Funcionário cadastrado com sucesso!" };
setTimeout(() => goto("/recursos-humanos/funcionarios"), 600);
} catch (e: any) {
@@ -327,14 +388,14 @@
<!-- Matrícula -->
<div class="form-control">
<label class="label" for="matricula">
<span class="label-text font-medium">Matrícula <span class="text-error">*</span></span>
<span class="label-text font-medium">Matrícula <span class="text-base-content/50 text-xs">(opcional)</span></span>
</label>
<input
id="matricula"
type="text"
class="input input-bordered w-full"
bind:value={matricula}
required
placeholder="Deixe em branco se não tiver"
/>
</div>
@@ -768,6 +829,121 @@
</div>
</div>
<!-- Card 3.5: Cursos e Treinamentos -->
<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="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Cursos e Treinamentos
</h2>
<p class="text-sm text-base-content/70">
Adicione até 7 cursos ou treinamentos realizados pelo funcionário (opcional)
</p>
<!-- Lista de cursos adicionados -->
{#if cursos.length > 0}
<div class="space-y-2">
<h3 class="font-semibold text-sm">Cursos adicionados ({cursos.length}/7)</h3>
{#each cursos as curso}
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg">
<div class="flex-1">
<p class="font-semibold text-sm">{curso.descricao}</p>
<p class="text-xs text-base-content/70">{curso.data}</p>
</div>
<button
type="button"
class="btn btn-sm btn-error btn-square"
aria-label="Remover curso"
onclick={() => removerCurso(curso.id)}
>
<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>
{/each}
</div>
{/if}
<!-- Formulário para adicionar curso -->
{#if cursos.length < 7}
<div class="collapse collapse-arrow border border-base-300 bg-base-200">
<input type="checkbox" bind:checked={mostrarFormularioCurso} />
<div class="collapse-title font-medium">
Adicionar Curso/Treinamento
</div>
<div class="collapse-content">
<div class="space-y-3 pt-2">
<div class="form-control">
<label class="label" for="curso-descricao">
<span class="label-text font-medium">Descrição do Curso</span>
</label>
<input
id="curso-descricao"
type="text"
class="input input-bordered w-full"
bind:value={cursoAtual.descricao}
placeholder="Ex: Gestão de Projetos"
/>
</div>
<div class="form-control">
<label class="label" for="curso-data">
<span class="label-text font-medium">Data de Conclusão</span>
</label>
<input
id="curso-data"
type="text"
class="input input-bordered w-full"
bind:value={cursoAtual.data}
placeholder="Ex: 01/2024"
onchange={(e) => cursoAtual.data = maskDate(e.currentTarget.value)}
/>
</div>
<div class="form-control">
<label class="label" for="curso-certificado">
<span class="label-text font-medium">Certificado/Diploma (opcional)</span>
</label>
<input
id="curso-certificado"
type="file"
class="file-input file-input-bordered w-full"
accept=".pdf,.jpg,.jpeg,.png"
onchange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) cursoAtual.arquivo = file;
}}
/>
</div>
<button
type="button"
class="btn btn-primary btn-sm gap-2 mt-2"
onclick={adicionarCurso}
>
<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="M12 4v16m8-8H4" />
</svg>
Adicionar à Lista
</button>
</div>
</div>
</div>
{:else}
<div class="alert alert-info">
<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>
<span>Limite de 7 cursos atingido</span>
</div>
{/if}
</div>
</div>
<!-- Card 4: Endereço e Contato -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body space-y-4">