feat: integrate jsPDF and jsPDF-autotable for document generation; enhance employee management with print functionality and improved data handling in employee forms
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { goto } from "$app/navigation";
|
||||
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
|
||||
import PrintModal from "$lib/components/PrintModal.svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
@@ -12,6 +13,7 @@
|
||||
let deletingId: string | null = null;
|
||||
let toDelete: { id: string; nome: string } | null = null;
|
||||
let openMenuId: string | null = null;
|
||||
let funcionarioParaImprimir: any = null;
|
||||
|
||||
let filtroNome = "";
|
||||
let filtroCPF = "";
|
||||
@@ -48,6 +50,18 @@
|
||||
toDelete = null;
|
||||
(document.getElementById("delete_modal_func") as HTMLDialogElement)?.close();
|
||||
}
|
||||
|
||||
async function openPrintModal(funcionarioId: string) {
|
||||
try {
|
||||
const data = await client.query(api.funcionarios.getFichaCompleta, {
|
||||
id: funcionarioId as any
|
||||
});
|
||||
funcionarioParaImprimir = data;
|
||||
} catch (err) {
|
||||
console.error("Erro ao carregar funcionário:", err);
|
||||
alert("Erro ao carregar dados para impressão");
|
||||
}
|
||||
}
|
||||
async function confirmDelete() {
|
||||
if (!toDelete) return;
|
||||
try {
|
||||
@@ -213,8 +227,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/></svg>
|
||||
</button>
|
||||
<ul class="dropdown-content menu bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg border border-base-300">
|
||||
<li><a href={`/recursos-humanos/funcionarios/${f._id}`}>Ver Detalhes</a></li>
|
||||
<li><a href={`/recursos-humanos/funcionarios/${f._id}/editar`}>Editar</a></li>
|
||||
<li><button class="text-error" onclick={() => openDeleteModal(f._id, f.nome)}>Excluir</button></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>
|
||||
@@ -261,5 +278,12 @@
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</main>
|
||||
|
||||
<!-- Modal de Impressão -->
|
||||
{#if funcionarioParaImprimir}
|
||||
<PrintModal
|
||||
funcionario={funcionarioParaImprimir}
|
||||
onClose={() => funcionarioParaImprimir = null}
|
||||
/>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import { maskCPF, maskCEP, maskPhone } from "$lib/utils/masks";
|
||||
import { documentos, getDocumentoDefinicao } from "$lib/utils/documentos";
|
||||
import {
|
||||
SEXO_OPTIONS, ESTADO_CIVIL_OPTIONS, GRAU_INSTRUCAO_OPTIONS,
|
||||
GRUPO_SANGUINEO_OPTIONS, FATOR_RH_OPTIONS, APOSENTADO_OPTIONS
|
||||
} from "$lib/utils/constants";
|
||||
import PrintModal from "$lib/components/PrintModal.svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let funcionarioId = $derived($page.params.funcionarioId as string);
|
||||
|
||||
let funcionario = $state<any>(null);
|
||||
let simbolo = $state<any>(null);
|
||||
let documentosUrls = $state<Record<string, string | null>>({});
|
||||
let loading = $state(true);
|
||||
let showPrintModal = $state(false);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
loading = true;
|
||||
const data = await client.query(api.funcionarios.getFichaCompleta, {
|
||||
id: funcionarioId as any
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
goto("/recursos-humanos/funcionarios");
|
||||
return;
|
||||
}
|
||||
|
||||
funcionario = data;
|
||||
simbolo = data.simbolo;
|
||||
|
||||
// Carregar URLs dos documentos
|
||||
try {
|
||||
documentosUrls = await client.query(api.documentos.getDocumentosUrls, {
|
||||
funcionarioId: funcionarioId as any
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Erro ao carregar documentos:", err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erro ao carregar funcionário:", err);
|
||||
goto("/recursos-humanos/funcionarios");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelFromOptions(value: string | undefined, options: Array<{value: string, label: string}>): string {
|
||||
if (!value) return "-";
|
||||
return options.find(opt => opt.value === value)?.label || value;
|
||||
}
|
||||
|
||||
function downloadDocumento(url: string, nomeDoc: string) {
|
||||
if (!url) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = nomeDoc;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if funcionario}
|
||||
<main class="container mx-auto px-4 py-4 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><a href="/recursos-humanos/funcionarios" class="text-primary hover:underline">Funcionários</a></li>
|
||||
<li>Detalhes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">{funcionario.nome}</h1>
|
||||
<p class="text-base-content/70">Matrícula: {funcionario.matricula}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-primary gap-2"
|
||||
onclick={() => goto(`/recursos-humanos/funcionarios/${funcionarioId}/editar`)}
|
||||
>
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary gap-2"
|
||||
onclick={() => goto(`/recursos-humanos/funcionarios/${funcionarioId}/documentos`)}
|
||||
>
|
||||
<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="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Ver Documentos
|
||||
</button>
|
||||
<button class="btn btn-accent gap-2" onclick={() => showPrintModal = 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="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 Ficha
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Cards -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<!-- Coluna 1: Dados Pessoais -->
|
||||
<div class="space-y-6">
|
||||
<!-- Informações Pessoais -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Informações Pessoais</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><span class="font-semibold">CPF:</span> {maskCPF(funcionario.cpf)}</div>
|
||||
<div><span class="font-semibold">RG:</span> {funcionario.rg}</div>
|
||||
{#if funcionario.rgOrgaoExpedidor}
|
||||
<div><span class="font-semibold">Órgão Expedidor:</span> {funcionario.rgOrgaoExpedidor}</div>
|
||||
{/if}
|
||||
{#if funcionario.rgDataEmissao}
|
||||
<div><span class="font-semibold">Data Emissão RG:</span> {funcionario.rgDataEmissao}</div>
|
||||
{/if}
|
||||
<div><span class="font-semibold">Data Nascimento:</span> {funcionario.nascimento}</div>
|
||||
{#if funcionario.sexo}
|
||||
<div><span class="font-semibold">Sexo:</span> {getLabelFromOptions(funcionario.sexo, SEXO_OPTIONS)}</div>
|
||||
{/if}
|
||||
{#if funcionario.estadoCivil}
|
||||
<div><span class="font-semibold">Estado Civil:</span> {getLabelFromOptions(funcionario.estadoCivil, ESTADO_CIVIL_OPTIONS)}</div>
|
||||
{/if}
|
||||
{#if funcionario.nacionalidade}
|
||||
<div><span class="font-semibold">Nacionalidade:</span> {funcionario.nacionalidade}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filiação -->
|
||||
{#if funcionario.nomePai || funcionario.nomeMae}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Filiação</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{#if funcionario.nomePai}
|
||||
<div><span class="font-semibold">Pai:</span> {funcionario.nomePai}</div>
|
||||
{/if}
|
||||
{#if funcionario.nomeMae}
|
||||
<div><span class="font-semibold">Mãe:</span> {funcionario.nomeMae}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Naturalidade -->
|
||||
{#if funcionario.naturalidade || funcionario.naturalidadeUF}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Naturalidade</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{#if funcionario.naturalidade}
|
||||
<div><span class="font-semibold">Cidade:</span> {funcionario.naturalidade}</div>
|
||||
{/if}
|
||||
{#if funcionario.naturalidadeUF}
|
||||
<div><span class="font-semibold">UF:</span> {funcionario.naturalidadeUF}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Coluna 2: Documentos e Formação -->
|
||||
<div class="space-y-6">
|
||||
<!-- Documentos Pessoais -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Documentos Pessoais</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{#if funcionario.carteiraProfissionalNumero}
|
||||
<div><span class="font-semibold">Cart. Profissional:</span> {funcionario.carteiraProfissionalNumero}
|
||||
{#if funcionario.carteiraProfissionalSerie}
|
||||
- Série: {funcionario.carteiraProfissionalSerie}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if funcionario.reservistaNumero}
|
||||
<div><span class="font-semibold">Reservista:</span> {funcionario.reservistaNumero}
|
||||
{#if funcionario.reservistaSerie}
|
||||
- Série: {funcionario.reservistaSerie}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if funcionario.tituloEleitorNumero}
|
||||
<div><span class="font-semibold">Título Eleitor:</span> {funcionario.tituloEleitorNumero}</div>
|
||||
{#if funcionario.tituloEleitorZona || funcionario.tituloEleitorSecao}
|
||||
<div class="ml-4 text-xs">
|
||||
{#if funcionario.tituloEleitorZona}Zona: {funcionario.tituloEleitorZona}{/if}
|
||||
{#if funcionario.tituloEleitorSecao} - Seção: {funcionario.tituloEleitorSecao}{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if funcionario.pisNumero}
|
||||
<div><span class="font-semibold">PIS/PASEP:</span> {funcionario.pisNumero}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formação -->
|
||||
{#if funcionario.grauInstrucao || funcionario.formacao}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Formação</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{#if funcionario.grauInstrucao}
|
||||
<div><span class="font-semibold">Grau Instrução:</span> {getLabelFromOptions(funcionario.grauInstrucao, GRAU_INSTRUCAO_OPTIONS)}</div>
|
||||
{/if}
|
||||
{#if funcionario.formacao}
|
||||
<div><span class="font-semibold">Formação:</span> {funcionario.formacao}</div>
|
||||
{/if}
|
||||
{#if funcionario.formacaoRegistro}
|
||||
<div><span class="font-semibold">Registro Nº:</span> {funcionario.formacaoRegistro}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Saúde -->
|
||||
{#if funcionario.grupoSanguineo || funcionario.fatorRH}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Saúde</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
{#if funcionario.grupoSanguineo}
|
||||
<div><span class="font-semibold">Grupo Sanguíneo:</span> {funcionario.grupoSanguineo}</div>
|
||||
{/if}
|
||||
{#if funcionario.fatorRH}
|
||||
<div><span class="font-semibold">Fator RH:</span> {getLabelFromOptions(funcionario.fatorRH, FATOR_RH_OPTIONS)}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Contato -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Contato</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><span class="font-semibold">E-mail:</span> {funcionario.email}</div>
|
||||
<div><span class="font-semibold">Telefone:</span> {maskPhone(funcionario.telefone)}</div>
|
||||
</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">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Endereço</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>{funcionario.endereco}</div>
|
||||
<div>{funcionario.cidade} - {funcionario.uf}</div>
|
||||
<div><span class="font-semibold">CEP:</span> {maskCEP(funcionario.cep)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dados Bancários -->
|
||||
{#if funcionario.contaBradescoNumero}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg border-b pb-2 mb-3">Dados Bancários - Bradesco</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div><span class="font-semibold">Conta:</span> {funcionario.contaBradescoNumero}
|
||||
{#if funcionario.contaBradescoDV}-{funcionario.contaBradescoDV}{/if}
|
||||
</div>
|
||||
{#if funcionario.contaBradescoAgencia}
|
||||
<div><span class="font-semibold">Agência:</span> {funcionario.contaBradescoAgencia}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documentos Anexados -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<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-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>
|
||||
Documentos Anexados
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{#each documentos as doc}
|
||||
{@const temDocumento = documentosUrls[doc.campo]}
|
||||
<div
|
||||
class="card bg-base-200 shadow-sm border-2"
|
||||
class:border-success={temDocumento}
|
||||
class:border-base-300={!temDocumento}
|
||||
>
|
||||
<div class="card-body p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<div
|
||||
class={`w-8 h-8 rounded flex items-center justify-center flex-shrink-0 ${temDocumento ? 'bg-success/20' : 'bg-base-300'}`}
|
||||
>
|
||||
{#if temDocumento}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium line-clamp-2">{doc.nome}</p>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
{temDocumento ? 'Enviado' : 'Pendente'}
|
||||
</p>
|
||||
{#if temDocumento}
|
||||
<button
|
||||
class="btn btn-xs btn-ghost mt-2 gap-1"
|
||||
onclick={() => downloadDocumento(documentosUrls[doc.campo] || '', doc.nome)}
|
||||
>
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Baixar
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button
|
||||
class="btn btn-primary btn-sm gap-2"
|
||||
onclick={() => goto(`/recursos-humanos/funcionarios/${funcionarioId}/documentos`)}
|
||||
>
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Gerenciar Documentos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal de Impressão -->
|
||||
{#if showPrintModal}
|
||||
<PrintModal
|
||||
funcionario={funcionario}
|
||||
onClose={() => showPrintModal = false}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import FileUpload from "$lib/components/FileUpload.svelte";
|
||||
import ModelosDeclaracoes from "$lib/components/ModelosDeclaracoes.svelte";
|
||||
import { documentos, categoriasDocumentos, getDocumentosByCategoria } from "$lib/utils/documentos";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let funcionarioId = $derived($page.params.funcionarioId as string);
|
||||
|
||||
let funcionario = $state<any>(null);
|
||||
let documentosStorage = $state<Record<string, string | undefined>>({});
|
||||
let loading = $state(true);
|
||||
let filtro = $state<string>("todos"); // todos, enviados, pendentes
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
// Carregar dados do funcionário
|
||||
const data = await client.query(api.funcionarios.getById, {
|
||||
id: funcionarioId as any
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
goto("/recursos-humanos/funcionarios");
|
||||
return;
|
||||
}
|
||||
|
||||
funcionario = data;
|
||||
|
||||
// Mapear storage IDs dos documentos
|
||||
documentos.forEach(doc => {
|
||||
if ((data as any)[doc.campo]) {
|
||||
documentosStorage[doc.campo] = (data as any)[doc.campo];
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Erro ao carregar:", err);
|
||||
goto("/recursos-humanos/funcionarios");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDocumentoUpload(campo: string, file: File) {
|
||||
try {
|
||||
// Gerar URL de upload
|
||||
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
|
||||
|
||||
// Fazer upload do arquivo
|
||||
const result = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
|
||||
const { storageId } = await result.json();
|
||||
|
||||
// Atualizar documento no funcionário
|
||||
await client.mutation(api.documentos.updateDocumento, {
|
||||
funcionarioId: funcionarioId as any,
|
||||
campo,
|
||||
storageId: storageId as any,
|
||||
});
|
||||
|
||||
// Atualizar localmente
|
||||
documentosStorage[campo] = storageId;
|
||||
|
||||
// Recarregar
|
||||
await load();
|
||||
} catch (err: any) {
|
||||
throw new Error(err?.message || "Erro ao fazer upload");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDocumentoRemove(campo: string) {
|
||||
try {
|
||||
// Atualizar documento no funcionário (set to null)
|
||||
await client.mutation(api.documentos.updateDocumento, {
|
||||
funcionarioId: funcionarioId as any,
|
||||
campo,
|
||||
storageId: null,
|
||||
});
|
||||
|
||||
// Atualizar localmente
|
||||
documentosStorage[campo] = undefined;
|
||||
|
||||
// Recarregar
|
||||
await load();
|
||||
} catch (err: any) {
|
||||
alert("Erro ao remover documento: " + (err?.message || ""));
|
||||
}
|
||||
}
|
||||
|
||||
function documentosFiltrados() {
|
||||
return documentos.filter(doc => {
|
||||
const temDocumento = !!documentosStorage[doc.campo];
|
||||
if (filtro === "enviados") return temDocumento;
|
||||
if (filtro === "pendentes") return !temDocumento;
|
||||
return true; // todos
|
||||
});
|
||||
}
|
||||
|
||||
function contarDocumentos() {
|
||||
const total = documentos.length;
|
||||
const enviados = documentos.filter(doc => !!documentosStorage[doc.campo]).length;
|
||||
const pendentes = total - enviados;
|
||||
return { total, enviados, pendentes };
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if funcionario}
|
||||
<main class="container mx-auto px-4 py-4 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><a href="/recursos-humanos/funcionarios" class="text-primary hover:underline">Funcionários</a></li>
|
||||
<li><a href={`/recursos-humanos/funcionarios/${funcionarioId}`} class="text-primary hover:underline">{funcionario.nome}</a></li>
|
||||
<li>Documentos</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-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">Gerenciar Documentos</h1>
|
||||
<p class="text-base-content/70">{funcionario.nome} - Matrícula: {funcionario.matricula}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-ghost gap-2"
|
||||
onclick={() => goto(`/recursos-humanos/funcionarios/${funcionarioId}`)}
|
||||
>
|
||||
<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 aos Detalhes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estatísticas -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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 de Documentos</div>
|
||||
<div class="stat-value text-primary">{contarDocumentos().total}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Documentos Enviados</div>
|
||||
<div class="stat-value text-success">{contarDocumentos().enviados}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<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 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Documentos Pendentes</div>
|
||||
<div class="stat-value text-warning">{contarDocumentos().pendentes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modelos de Declarações -->
|
||||
<div class="mb-6">
|
||||
<ModelosDeclaracoes funcionario={funcionario} showPreencherButton={true} />
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-primary={filtro === "todos"}
|
||||
onclick={() => filtro = "todos"}
|
||||
>
|
||||
Todos ({contarDocumentos().total})
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-success={filtro === "enviados"}
|
||||
onclick={() => filtro = "enviados"}
|
||||
>
|
||||
Enviados ({contarDocumentos().enviados})
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-warning={filtro === "pendentes"}
|
||||
onclick={() => filtro = "pendentes"}
|
||||
>
|
||||
Pendentes ({contarDocumentos().pendentes})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documentos por Categoria -->
|
||||
{#each categoriasDocumentos as categoria}
|
||||
{@const docsCategoria = getDocumentosByCategoria(categoria).filter(doc => {
|
||||
const temDocumento = !!documentosStorage[doc.campo];
|
||||
if (filtro === "enviados") return temDocumento;
|
||||
if (filtro === "pendentes") return !temDocumento;
|
||||
return true;
|
||||
})}
|
||||
|
||||
{#if docsCategoria.length > 0}
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xl border-b pb-3 mb-4">
|
||||
{categoria}
|
||||
<div class="badge badge-primary ml-2">{docsCategoria.length}</div>
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#each docsCategoria as doc}
|
||||
<FileUpload
|
||||
label={doc.nome}
|
||||
helpUrl={doc.helpUrl}
|
||||
value={documentosStorage[doc.campo]}
|
||||
onUpload={(file) => handleDocumentoUpload(doc.campo, file)}
|
||||
onRemove={() => handleDocumentoRemove(doc.campo)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if documentosFiltrados().length === 0}
|
||||
<div class="alert">
|
||||
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Nenhum documento encontrado com o filtro selecionado.</span>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user