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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user