Feat chat #3

Merged
deyvisonwanderley merged 11 commits from feat-chat into master 2025-10-28 15:05:03 +00:00
117 changed files with 26899 additions and 1081 deletions
Showing only changes of commit 81e6eb4a42 - Show all commits

View File

@@ -0,0 +1,352 @@
<!-- de0a1ea6-0e97-42bf-a867-941b2346132b c70cab4f-9f78-4c1a-9087-09a2bf0196c8 -->
# Plano: Sistema Completo de Documentos e Cadastro de Funcionários
## 1. Atualizar Schema do Banco de Dados
**Arquivo:** `packages/backend/convex/schema.ts`
### Campos de Dados Pessoais Adicionais (todos opcionais):
- `nomePai: v.optional(v.string())`
- `nomeMae: v.optional(v.string())`
- `naturalidade: v.optional(v.string())` - cidade natal
- `naturalidadeUF: v.optional(v.string())` - UF com máscara (2 letras)
- `sexo: v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")))`
- `estadoCivil: v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel")))`
- `nacionalidade: v.optional(v.string())`
- `rgOrgaoExpedidor: v.optional(v.string())`
- `rgDataEmissao: v.optional(v.string())` - formato dd/mm/aaaa
- `carteiraProfissionalNumero: v.optional(v.string())`
- `carteiraProfissionalSerie: v.optional(v.string())`
- `carteiraProfissionalDataEmissao: v.optional(v.string())`
- `reservistaNumero: v.optional(v.string())`
- `reservistaSerie: v.optional(v.string())`
- `tituloEleitorNumero: v.optional(v.string())`
- `tituloEleitorZona: v.optional(v.string())`
- `tituloEleitorSecao: v.optional(v.string())`
- `grauInstrucao: v.optional(v.union(...))` - fundamental, medio, superior, pos_graduacao, mestrado, doutorado
- `formacao: v.optional(v.string())` - curso/formação
- `formacaoRegistro: v.optional(v.string())` - número de registro do diploma
- `pisNumero: v.optional(v.string())`
- `grupoSanguineo: v.optional(v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")))`
- `fatorRH: v.optional(v.union(v.literal("positivo"), v.literal("negativo")))`
- `nomeacaoPortaria: v.optional(v.string())` - número do ato/portaria
- `nomeacaoData: v.optional(v.string())`
- `nomeacaoDOE: v.optional(v.string())`
- `descricaoCargo: v.optional(v.string())`
- `pertenceOrgaoPublico: v.optional(v.boolean())`
- `orgaoOrigem: v.optional(v.string())`
- `aposentado: v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")))`
- `contaBradescoNumero: v.optional(v.string())`
- `contaBradescoDV: v.optional(v.string())`
- `contaBradescoAgencia: v.optional(v.string())`
### Campos de Documentos (Storage IDs opcionais) - 23 campos:
Todos como `v.optional(v.id("_storage"))`:
- `certidaoAntecedentesPF`, `certidaoAntecedentesJFPE`, `certidaoAntecedentesSDS`, `certidaoAntecedentesTJPE`, `certidaoImprobidade`, `rgFrente`, `rgVerso`, `cpfFrente`, `cpfVerso`, `situacaoCadastralCPF`, `tituloEleitorFrente`, `tituloEleitorVerso`, `comprovanteVotacao`, `carteiraProfissionalFrente`, `carteiraProfissionalVerso`, `comprovantePIS`, `certidaoRegistroCivil`, `certidaoNascimentoDependentes`, `cpfDependentes`, `reservistaDoc`, `comprovanteEscolaridade`, `comprovanteResidencia`, `comprovanteContaBradesco`
## 2. Atualizar Backend Convex
**Arquivo:** `packages/backend/convex/funcionarios.ts`
- Adicionar todos os novos campos nas mutations `create` e `update`
- Criar mutation `uploadDocumento(funcionarioId, tipoDocumento, storageId)` para vincular uploads
- Criar query `getDocumentosUrls(funcionarioId)` retornando objeto com URLs de todos os documentos
- Criar query `getFichaCompleta(funcionarioId)` retornando todos os dados formatados para impressão
## 3. Criar Componente de Upload de Arquivo
**Arquivo:** `apps/web/src/lib/components/FileUpload.svelte`
Props:
- `label: string` - nome do documento
- `helpUrl?: string` - URL de referência
- `value?: string` - storageId atual
- `onUpload: (file: File) => Promise<void>`
- `onRemove: () => Promise<void>`
Recursos:
- Input aceita PDF e imagens (jpg, png, jpeg)
- Preview com thumbnail para imagens, ícone para PDF
- Botão remover com confirmação
- Validação de tamanho máximo 10MB
- Loading state durante upload
- Tooltip com link de ajuda (ícone ?)
## 4. Atualizar Formulário de Cadastro
**Arquivo:** `apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte`
### Reorganizar em 8 cards:
**Card 1 - Informações Pessoais:**
- Nome, Matrícula, CPF (máscara), RG, Órgão Expedidor, Data Emissão RG
- Nome Pai, Nome Mãe
- Data Nascimento, Naturalidade, UF (máscara 2 letras)
- Sexo (select), Estado Civil (select), Nacionalidade
**Card 2 - Documentos Pessoais:**
- Carteira Profissional Nº, Série, Data Emissão
- Reservista Nº, Série
- Título Eleitor Nº, Zona, Seção
- PIS/PASEP Nº
**Card 3 - Formação e Saúde:**
- Grau Instrução (select), Formação, Registro Nº
- Grupo Sanguíneo (select), Fator RH (select)
**Card 4 - Endereço e Contato:**
- CEP, Cidade, UF, Endereço
- Telefone, Email
**Card 5 - Cargo e Vínculo:**
- Símbolo Tipo (CC/FG)
- Símbolo (select filtrado)
- Descrição Cargo/Função (novo campo opcional)
- Nomeação/Portaria Nº, Data, DOE
- Data Admissão
- Pertence a Órgão Público? (checkbox)
- Órgão de Origem (se extra-quadro)
- Aposentado (select: Não/FUNAPE-IPSEP/INSS)
**Card 6 - Dados Bancários:**
- Conta Bradesco Nº, DV, Agência
**Card 7 - Documentação Anexa (23 uploads):**
Organizar em subcategorias com ícones:
- Antecedentes Criminais (4 docs)
- Documentos Pessoais (6 docs)
- Documentos Eleitorais (3 docs)
- Documentos Profissionais (4 docs)
- Certidões e Comprovantes (6 docs)
Cada campo com tooltip (?) linkando para URL de referência
**Card 8 - Ações:**
- Botão Cancelar
- Botão Cadastrar
## 5. Atualizar Formulário de Edição
**Arquivo:** `apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte`
- Mesma estrutura do cadastro
- Carregar valores existentes
- Mostrar documentos já enviados com opção de substituir
- Preview de documentos existentes
## 6. Criar Página de Detalhes do Funcionário
**Arquivo:** `apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/+page.svelte`
Layout com 3 colunas de cards:
- Coluna 1: Dados Pessoais, Filiação, Naturalidade
- Coluna 2: Documentos, Formação, Saúde
- Coluna 3: Cargo, Vínculo, Bancários
Seção inferior: Grid de documentos anexados com status (enviado/pendente)
Cabeçalho: Botões "Editar", "Ver Documentos", "Imprimir Ficha"
## 7. Criar Página de Gerenciamento de Documentos
**Arquivo:** `apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/documentos/+page.svelte`
Grid 3x8 de cards, cada um com:
- Nome do documento
- Ícone de status (verde=enviado, amarelo=pendente)
- Preview ou ícone
- Botões: Upload/Substituir, Download, Visualizar, Remover
- Link de ajuda (?)
Filtros: Mostrar Todos / Apenas Enviados / Apenas Pendentes
## 8. Adicionar Botões de Impressão
**Arquivo:** `apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte`
No dropdown de ações de cada linha:
- Editar
- Ver Documentos
- **Imprimir Ficha** (novo)
- Excluir
## 9. Criar Modal de Impressão
**Arquivo:** `apps/web/src/lib/components/PrintModal.svelte`
Props: `funcionarioId: string`
Layout em 2 colunas:
- Coluna esquerda: Checkboxes organizados por seção
- Coluna direita: Preview em tempo real (opcional)
Seções de campos selecionáveis:
1. Dados Pessoais (15 campos)
2. Filiação (2 campos)
3. Naturalidade (2 campos)
4. Documentos (8 campos)
5. Formação (3 campos)
6. Saúde (2 campos)
7. Endereço (4 campos)
8. Contato (2 campos)
9. Cargo e Vínculo (9 campos)
10. Dados Bancários (3 campos)
11. Documentos Anexos (23 campos)
Botões:
- Selecionar Todos / Desmarcar Todos (por seção)
- Cancelar
- Gerar PDF
Geração do PDF:
- Usar jsPDF + autotable
- Cabeçalho com logo da secretaria
- Título "FICHA CADASTRAL DE FUNCIONÁRIO"
- Dados em formato de tabela (label: valor)
- Seções separadas visualmente
- Rodapé com data de geração
## 10. Criar Helper de Máscaras
**Arquivo:** `apps/web/src/lib/utils/masks.ts`
Funções reutilizáveis:
- `maskCPF(value: string): string`
- `maskUF(value: string): string` - força uppercase, 2 chars
- `maskCEP(value: string): string`
- `maskPhone(value: string): string`
- `maskDate(value: string): string`
- `validateCPF(value: string): boolean`
- `validateDate(value: string): boolean`
## 11. Criar Seção de Modelos de Declarações
### Estrutura de Arquivos
**Pasta:** `apps/web/static/modelos/declaracoes/`
Armazenar os 5 modelos de declarações em PDF que os funcionários devem preencher e assinar.
### Componente de Modelos
**Arquivo:** `apps/web/src/lib/components/ModelosDeclaracoes.svelte`
Componente exibindo card com:
- Título: "Modelos de Declarações"
- Descrição: "Baixe os modelos, preencha, assine e faça upload no sistema"
- Lista dos 5 modelos com:
- Nome do documento
- Ícone de PDF
- Botão "Baixar Modelo"
- Botão "Gerar Preenchido" (se dados disponíveis)
- Layout em grid responsivo
### Gerador de Declarações
**Arquivo:** `apps/web/src/lib/utils/declaracoesGenerator.ts`
Funções para gerar cada uma das 5 declarações preenchidas com dados do funcionário:
- `gerarDeclaracao1(funcionario): Blob`
- `gerarDeclaracao2(funcionario): Blob`
- `gerarDeclaracao3(funcionario): Blob`
- `gerarDeclaracao4(funcionario): Blob`
- `gerarDeclaracao5(funcionario): Blob`
Cada função usa jsPDF para:
- Replicar o layout do modelo
- Preencher com dados do funcionário
- Deixar campo de assinatura em branco
- Retornar PDF pronto para download
### Modal Seletor de Modelos
**Arquivo:** `apps/web/src/lib/components/SeletorModelosModal.svelte`
Modal para escolher quais modelos baixar:
- Checkboxes para cada um dos 5 modelos
- Opção: "Baixar modelos vazios" ou "Gerar preenchidos"
- Botão "Selecionar Todos"
- Botão "Baixar Selecionados"
- Se "gerar preenchidos", preenche com dados do funcionário
### Integração nas Páginas
Adicionar componente `<ModelosDeclaracoes />` em:
1. Formulário de cadastro (antes do card de documentação anexa)
2. Página de gerenciamento de documentos (seção superior)
3. Página de detalhes do funcionário (botão "Baixar Modelos" no cabeçalho)
## 12. Instalar Dependências
**Arquivo:** `apps/web/package.json`
```bash
npm install jspdf jspdf-autotable
npm install -D @types/jspdf
```
## Referências dos Documentos
Manter estrutura de dados com URLs:
1. Cert. Antecedentes PF: https://servicos.pf.gov.br/epol-sinic-publico/
2. Cert. Antecedentes JFPE: https://certidoes.trf5.jus.br/certidoes2022/paginas/certidaocriminal.faces
3. Cert. Antecedentes SDS-PE: http://www.servicos.sds.pe.gov.br/antecedentes/...
4. Cert. Antecedentes TJPE: https://certidoesunificadas.app.tjpe.jus.br/certidao-criminal-pf
5. Cert. Improbidade: https://www.cnj.jus.br/improbidade_adm/consultar_requerido
6-10. RG, CPF, Situação CPF: URLs fornecidas
11-23. Demais documentos com URLs correspondentes
## Design e UX
- DaisyUI para consistência
- Cards com sombras suaves
- Ícones lucide-svelte ou heroicons
- Cores: verde para sucesso, amarelo para pendente, vermelho para erro
- Animações suaves de transição
- Layout responsivo (mobile-first)
- Tooltips discretos
- Feedback imediato em ações
- Progress indicators durante uploads
### To-dos
- [ ] Atualizar schema do banco com campo descricaoCargo e 23 campos de documentos
- [ ] Criar mutations e queries no backend para upload e gerenciamento de documentos
- [ ] Criar componente reutilizável FileUpload.svelte com preview e validação
- [ ] Adicionar campo descricaoCargo e seção de documentos no formulário de cadastro
- [ ] Adicionar campo descricaoCargo e seção de documentos no formulário de edição
- [ ] Criar página de detalhes do funcionário com visualização de documentos
- [ ] Criar página de gerenciamento centralizado de documentos
- [ ] Adicionar botões de impressão na listagem e página de detalhes
- [ ] Criar modal de impressão com checkboxes e geração de PDF
- [ ] Instalar jspdf e jspdf-autotable no package.json do web

View File

@@ -34,6 +34,8 @@
"better-auth": "1.3.27", "better-auth": "1.3.27",
"convex": "^1.28.0", "convex": "^1.28.0",
"convex-svelte": "^0.0.11", "convex-svelte": "^0.0.11",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"zod": "^4.0.17" "zod": "^4.0.17"
} }
} }

View File

@@ -0,0 +1,273 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
interface Props {
label: string;
helpUrl?: string;
value?: string; // storageId
disabled?: boolean;
onUpload: (file: File) => Promise<void>;
onRemove: () => Promise<void>;
}
let {
label,
helpUrl,
value = $bindable(),
disabled = false,
onUpload,
onRemove,
}: Props = $props();
const client = useConvexClient();
let fileInput: HTMLInputElement;
let uploading = $state(false);
let error = $state<string | null>(null);
let fileName = $state<string>("");
let fileType = $state<string>("");
let previewUrl = $state<string | null>(null);
let fileUrl = $state<string | null>(null);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [
"application/pdf",
"image/jpeg",
"image/jpg",
"image/png",
];
// Buscar URL do arquivo quando houver um storageId
$effect(() => {
if (value && !fileName) {
// Tem storageId mas não é um upload recente
loadExistingFile(value);
}
});
async function loadExistingFile(storageId: string) {
try {
const url = await client.storage.getUrl(storageId as any);
if (url) {
fileUrl = url;
fileName = "Documento anexado";
// Detectar tipo pelo URL ou assumir PDF
if (url.includes(".pdf") || url.includes("application/pdf")) {
fileType = "application/pdf";
} else {
fileType = "image/jpeg";
previewUrl = url; // Para imagens, a URL serve como preview
}
}
} catch (err) {
console.error("Erro ao carregar arquivo existente:", err);
}
}
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
error = null;
// Validate file size
if (file.size > MAX_FILE_SIZE) {
error = "Arquivo muito grande. Tamanho máximo: 10MB";
target.value = "";
return;
}
// Validate file type
if (!ALLOWED_TYPES.includes(file.type)) {
error = "Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)";
target.value = "";
return;
}
try {
uploading = true;
fileName = file.name;
fileType = file.type;
// Create preview for images
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
previewUrl = e.target?.result as string;
};
reader.readAsDataURL(file);
}
await onUpload(file);
} catch (err: any) {
error = err?.message || "Erro ao fazer upload do arquivo";
previewUrl = null;
} finally {
uploading = false;
target.value = "";
}
}
async function handleRemove() {
if (!confirm("Tem certeza que deseja remover este arquivo?")) {
return;
}
try {
uploading = true;
await onRemove();
fileName = "";
fileType = "";
previewUrl = null;
fileUrl = null;
} catch (err: any) {
error = err?.message || "Erro ao remover arquivo";
} finally {
uploading = false;
}
}
function handleView() {
if (fileUrl) {
window.open(fileUrl, '_blank');
}
}
function openFileDialog() {
fileInput?.click();
}
</script>
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium flex items-center gap-2">
{label}
{#if helpUrl}
<div class="tooltip tooltip-right" data-tip="Clique para acessar o link">
<a
href={helpUrl}
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:text-primary-focus transition-colors"
aria-label="Acessar link"
>
<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>
</a>
</div>
{/if}
</span>
</label>
<input
type="file"
bind:this={fileInput}
onchange={handleFileSelect}
accept=".pdf,.jpg,.jpeg,.png"
class="hidden"
{disabled}
/>
{#if value || fileName}
<div class="flex items-center gap-2 p-3 border border-base-300 rounded-lg bg-base-100">
<!-- Preview -->
<div class="flex-shrink-0">
{#if previewUrl}
<img src={previewUrl} alt="Preview" class="w-12 h-12 object-cover rounded" />
{:else if fileType === "application/pdf" || fileName.endsWith(".pdf")}
<div class="w-12 h-12 bg-error/10 rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-error" 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>
</div>
{:else}
<div class="w-12 h-12 bg-success/10 rounded flex items-center justify-center">
<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="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>
{/if}
</div>
<!-- File info -->
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{fileName || "Arquivo anexado"}</p>
<p class="text-xs text-base-content/60">
{#if uploading}
Carregando...
{:else}
Enviado com sucesso
{/if}
</p>
</div>
<!-- Actions -->
<div class="flex gap-2">
{#if fileUrl}
<button
type="button"
onclick={handleView}
class="btn btn-sm btn-ghost text-info"
disabled={uploading || disabled}
title="Visualizar arquivo"
>
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
{/if}
<button
type="button"
onclick={openFileDialog}
class="btn btn-sm btn-ghost"
disabled={uploading || disabled}
title="Substituir arquivo"
>
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
</button>
<button
type="button"
onclick={handleRemove}
class="btn btn-sm btn-ghost text-error"
disabled={uploading || disabled}
title="Remover arquivo"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
{:else}
<button
type="button"
onclick={openFileDialog}
class="btn btn-outline btn-block justify-start gap-2"
disabled={uploading || disabled}
>
{#if uploading}
<span class="loading loading-spinner loading-sm"></span>
Carregando...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Selecionar arquivo (PDF ou imagem, máx. 10MB)
{/if}
</button>
{/if}
{#if error}
<label class="label">
<span class="label-text-alt text-error">{error}</span>
</label>
{/if}
</div>

View File

@@ -0,0 +1,162 @@
<script lang="ts">
import { modelosDeclaracoes } from "$lib/utils/modelosDeclaracoes";
import {
gerarDeclaracaoAcumulacaoCargo,
gerarDeclaracaoDependentesIR,
gerarDeclaracaoIdoneidade,
gerarTermoNepotismo,
gerarTermoOpcaoRemuneracao,
downloadBlob
} from "$lib/utils/declaracoesGenerator";
interface Props {
funcionario?: any;
showPreencherButton?: boolean;
}
let { funcionario, showPreencherButton = false }: Props = $props();
let generating = $state(false);
function baixarModelo(arquivoUrl: string, nomeModelo: string) {
const link = document.createElement('a');
link.href = arquivoUrl;
link.download = nomeModelo + '.pdf';
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function gerarPreenchido(modeloId: string) {
if (!funcionario) {
alert('Dados do funcionário não disponíveis');
return;
}
try {
generating = true;
let blob: Blob;
let nomeArquivo: string;
switch (modeloId) {
case 'acumulacao_cargo':
blob = await gerarDeclaracaoAcumulacaoCargo(funcionario);
nomeArquivo = `Declaracao_Acumulacao_Cargo_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'dependentes_ir':
blob = await gerarDeclaracaoDependentesIR(funcionario);
nomeArquivo = `Declaracao_Dependentes_IR_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'idoneidade':
blob = await gerarDeclaracaoIdoneidade(funcionario);
nomeArquivo = `Declaracao_Idoneidade_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'nepotismo':
blob = await gerarTermoNepotismo(funcionario);
nomeArquivo = `Termo_Nepotismo_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'opcao_remuneracao':
blob = await gerarTermoOpcaoRemuneracao(funcionario);
nomeArquivo = `Termo_Opcao_Remuneracao_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
default:
alert('Modelo não encontrado');
return;
}
downloadBlob(blob, nomeArquivo);
} catch (error) {
console.error('Erro ao gerar declaração:', error);
alert('Erro ao gerar declaração preenchida');
} finally {
generating = false;
}
}
</script>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<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="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>
Modelos de Declarações
</h2>
<div class="alert alert-info shadow-sm mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" 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>
<div class="text-sm">
<p class="font-semibold">Baixe os modelos, preencha, assine e faça upload no sistema</p>
<p class="text-xs opacity-80 mt-1">Estes documentos são necessários para completar o cadastro do funcionário</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each modelosDeclaracoes as modelo}
<div class="card bg-base-200 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body p-4">
<div class="flex items-start gap-3">
<!-- Ícone PDF -->
<div class="flex-shrink-0 w-12 h-12 bg-error/10 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-error" 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>
</div>
<!-- Conteúdo -->
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-sm mb-1 line-clamp-2">{modelo.nome}</h3>
<p class="text-xs text-base-content/70 mb-3 line-clamp-2">{modelo.descricao}</p>
<!-- Ações -->
<div class="flex flex-col gap-2">
<button
type="button"
class="btn btn-primary btn-xs gap-1"
onclick={() => baixarModelo(modelo.arquivo, modelo.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 Modelo
</button>
{#if showPreencherButton && modelo.podePreencherAutomaticamente && funcionario}
<button
type="button"
class="btn btn-outline btn-xs gap-1"
onclick={() => gerarPreenchido(modelo.id)}
disabled={generating}
>
{#if generating}
<span class="loading loading-spinner loading-xs"></span>
Gerando...
{:else}
<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="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>
Gerar Preenchido
{/if}
</button>
{/if}
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
<div class="mt-4 text-xs text-base-content/60 text-center">
<p>💡 Dica: Após preencher e assinar os documentos, faça upload na seção "Documentação Anexa"</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,463 @@
<script lang="ts">
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import { maskCPF, maskCEP, maskPhone } from "$lib/utils/masks";
import {
SEXO_OPTIONS, ESTADO_CIVIL_OPTIONS, GRAU_INSTRUCAO_OPTIONS,
GRUPO_SANGUINEO_OPTIONS, FATOR_RH_OPTIONS, APOSENTADO_OPTIONS
} from "$lib/utils/constants";
import logoGovPE from "$lib/assets/logo_governo_PE.png";
interface Props {
funcionario: any;
onClose: () => void;
}
let { funcionario, onClose }: Props = $props();
let modalRef: HTMLDialogElement;
let generating = $state(false);
// Seções selecionáveis
let sections = $state({
dadosPessoais: true,
filiacao: true,
naturalidade: true,
documentos: true,
formacao: true,
saude: true,
endereco: true,
contato: true,
cargo: true,
bancario: true,
});
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 selectAll() {
Object.keys(sections).forEach(key => {
sections[key as keyof typeof sections] = true;
});
}
function deselectAll() {
Object.keys(sections).forEach(key => {
sections[key as keyof typeof sections] = false;
});
}
async function gerarPDF() {
try {
generating = true;
const doc = new jsPDF();
// Logo no canto superior esquerdo (proporcional)
let yPosition = 20;
try {
const logoImg = new Image();
logoImg.src = logoGovPE;
await new Promise<void>((resolve, reject) => {
logoImg.onload = () => resolve();
logoImg.onerror = () => reject();
setTimeout(() => reject(), 3000); // timeout após 3s
});
// Logo proporcional: largura 25mm, altura ajustada automaticamente
const logoWidth = 25;
const aspectRatio = logoImg.height / logoImg.width;
const logoHeight = logoWidth * aspectRatio;
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
// Ajustar posição inicial do texto para ficar ao lado da logo
yPosition = Math.max(20, 10 + logoHeight / 2);
} catch (err) {
console.warn('Não foi possível carregar a logo:', err);
}
// Cabeçalho (alinhado com a logo)
doc.setFontSize(16);
doc.setFont('helvetica', 'bold');
doc.text('Secretaria de Esportes', 50, yPosition);
doc.setFontSize(12);
doc.setFont('helvetica', 'normal');
doc.text('Governo de Pernambuco', 50, yPosition + 7);
yPosition = Math.max(45, yPosition + 25);
// Título da ficha
doc.setFontSize(18);
doc.setFont('helvetica', 'bold');
doc.text('FICHA CADASTRAL DE FUNCIONÁRIO', 105, yPosition, { align: 'center' });
yPosition += 8;
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text(`Gerado em: ${new Date().toLocaleString('pt-BR')}`, 105, yPosition, { align: 'center' });
yPosition += 12;
// Dados Pessoais
if (sections.dadosPessoais) {
const dadosPessoais: any[] = [
['Nome', funcionario.nome],
['Matrícula', funcionario.matricula],
['CPF', maskCPF(funcionario.cpf)],
['RG', funcionario.rg],
['Data Nascimento', funcionario.nascimento],
];
if (funcionario.rgOrgaoExpedidor) dadosPessoais.push(['Órgão Expedidor RG', funcionario.rgOrgaoExpedidor]);
if (funcionario.rgDataEmissao) dadosPessoais.push(['Data Emissão RG', funcionario.rgDataEmissao]);
if (funcionario.sexo) dadosPessoais.push(['Sexo', getLabelFromOptions(funcionario.sexo, SEXO_OPTIONS)]);
if (funcionario.estadoCivil) dadosPessoais.push(['Estado Civil', getLabelFromOptions(funcionario.estadoCivil, ESTADO_CIVIL_OPTIONS)]);
if (funcionario.nacionalidade) dadosPessoais.push(['Nacionalidade', funcionario.nacionalidade]);
autoTable(doc, {
startY: yPosition,
head: [['DADOS PESSOAIS', '']],
body: dadosPessoais,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Filiação
if (sections.filiacao && (funcionario.nomePai || funcionario.nomeMae)) {
const filiacao: any[] = [];
if (funcionario.nomePai) filiacao.push(['Nome do Pai', funcionario.nomePai]);
if (funcionario.nomeMae) filiacao.push(['Nome da Mãe', funcionario.nomeMae]);
autoTable(doc, {
startY: yPosition,
head: [['FILIAÇÃO', '']],
body: filiacao,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Naturalidade
if (sections.naturalidade && (funcionario.naturalidade || funcionario.naturalidadeUF)) {
const naturalidade: any[] = [];
if (funcionario.naturalidade) naturalidade.push(['Cidade', funcionario.naturalidade]);
if (funcionario.naturalidadeUF) naturalidade.push(['UF', funcionario.naturalidadeUF]);
autoTable(doc, {
startY: yPosition,
head: [['NATURALIDADE', '']],
body: naturalidade,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Documentos
if (sections.documentos) {
const documentosData: any[] = [];
if (funcionario.carteiraProfissionalNumero) {
documentosData.push(['Cart. Profissional', `Nº ${funcionario.carteiraProfissionalNumero}${funcionario.carteiraProfissionalSerie ? ' - Série: ' + funcionario.carteiraProfissionalSerie : ''}`]);
}
if (funcionario.reservistaNumero) {
documentosData.push(['Reservista', `Nº ${funcionario.reservistaNumero}${funcionario.reservistaSerie ? ' - Série: ' + funcionario.reservistaSerie : ''}`]);
}
if (funcionario.tituloEleitorNumero) {
let titulo = `Nº ${funcionario.tituloEleitorNumero}`;
if (funcionario.tituloEleitorZona) titulo += ` - Zona: ${funcionario.tituloEleitorZona}`;
if (funcionario.tituloEleitorSecao) titulo += ` - Seção: ${funcionario.tituloEleitorSecao}`;
documentosData.push(['Título Eleitor', titulo]);
}
if (funcionario.pisNumero) documentosData.push(['PIS/PASEP', funcionario.pisNumero]);
if (documentosData.length > 0) {
autoTable(doc, {
startY: yPosition,
head: [['DOCUMENTOS', '']],
body: documentosData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
}
// Formação
if (sections.formacao && (funcionario.grauInstrucao || funcionario.formacao)) {
const formacaoData: any[] = [];
if (funcionario.grauInstrucao) formacaoData.push(['Grau Instrução', getLabelFromOptions(funcionario.grauInstrucao, GRAU_INSTRUCAO_OPTIONS)]);
if (funcionario.formacao) formacaoData.push(['Formação', funcionario.formacao]);
if (funcionario.formacaoRegistro) formacaoData.push(['Registro Nº', funcionario.formacaoRegistro]);
autoTable(doc, {
startY: yPosition,
head: [['FORMAÇÃO', '']],
body: formacaoData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Saúde
if (sections.saude && (funcionario.grupoSanguineo || funcionario.fatorRH)) {
const saudeData: any[] = [];
if (funcionario.grupoSanguineo) saudeData.push(['Grupo Sanguíneo', funcionario.grupoSanguineo]);
if (funcionario.fatorRH) saudeData.push(['Fator RH', getLabelFromOptions(funcionario.fatorRH, FATOR_RH_OPTIONS)]);
autoTable(doc, {
startY: yPosition,
head: [['SAÚDE', '']],
body: saudeData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Endereço
if (sections.endereco) {
const enderecoData: any[] = [
['Endereço', funcionario.endereco],
['Cidade', funcionario.cidade],
['UF', funcionario.uf],
['CEP', maskCEP(funcionario.cep)],
];
autoTable(doc, {
startY: yPosition,
head: [['ENDEREÇO', '']],
body: enderecoData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Contato
if (sections.contato) {
const contatoData: any[] = [
['E-mail', funcionario.email],
['Telefone', maskPhone(funcionario.telefone)],
];
autoTable(doc, {
startY: yPosition,
head: [['CONTATO', '']],
body: contatoData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Nova página para cargo
if (yPosition > 200) {
doc.addPage();
yPosition = 20;
}
// Cargo e Vínculo
if (sections.cargo) {
const cargoData: any[] = [
['Tipo', funcionario.simboloTipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'],
];
if (funcionario.simbolo) {
cargoData.push(['Símbolo', funcionario.simbolo.nome]);
}
if (funcionario.descricaoCargo) cargoData.push(['Descrição', funcionario.descricaoCargo]);
if (funcionario.admissaoData) cargoData.push(['Data Admissão', funcionario.admissaoData]);
if (funcionario.nomeacaoPortaria) cargoData.push(['Portaria', funcionario.nomeacaoPortaria]);
if (funcionario.nomeacaoData) cargoData.push(['Data Nomeação', funcionario.nomeacaoData]);
if (funcionario.nomeacaoDOE) cargoData.push(['DOE', funcionario.nomeacaoDOE]);
if (funcionario.pertenceOrgaoPublico) {
cargoData.push(['Pertence Órgão Público', 'Sim']);
if (funcionario.orgaoOrigem) cargoData.push(['Órgão Origem', funcionario.orgaoOrigem]);
}
if (funcionario.aposentado && funcionario.aposentado !== 'nao') {
cargoData.push(['Aposentado', getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)]);
}
autoTable(doc, {
startY: yPosition,
head: [['CARGO E VÍNCULO', '']],
body: cargoData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Dados Bancários
if (sections.bancario && funcionario.contaBradescoNumero) {
const bancarioData: any[] = [
['Conta', `${funcionario.contaBradescoNumero}${funcionario.contaBradescoDV ? '-' + funcionario.contaBradescoDV : ''}`],
];
if (funcionario.contaBradescoAgencia) bancarioData.push(['Agência', funcionario.contaBradescoAgencia]);
autoTable(doc, {
startY: yPosition,
head: [['DADOS BANCÁRIOS - BRADESCO', '']],
body: bancarioData,
theme: 'grid',
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
styles: { fontSize: 9 },
});
yPosition = (doc as any).lastAutoTable.finalY + 10;
}
// Adicionar rodapé em todas as páginas
const pageCount = (doc as any).internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.setTextColor(128, 128, 128);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
doc.text(`Página ${i} de ${pageCount}`, 195, 285, { align: 'right' });
}
// Salvar PDF
doc.save(`Ficha_${funcionario.nome.replace(/ /g, '_')}_${new Date().getTime()}.pdf`);
onClose();
} catch (error) {
console.error('Erro ao gerar PDF:', error);
alert('Erro ao gerar PDF. Verifique o console para mais detalhes.');
} finally {
generating = false;
}
}
$effect(() => {
if (modalRef) {
modalRef.showModal();
}
});
</script>
<dialog bind:this={modalRef} class="modal">
<div class="modal-box max-w-4xl">
<h3 class="font-bold text-2xl mb-4">Imprimir Ficha Cadastral</h3>
<p class="text-sm text-base-content/70 mb-6">Selecione as seções que deseja incluir no PDF</p>
<!-- Botões de seleção -->
<div class="flex gap-2 mb-6">
<button type="button" class="btn btn-sm btn-outline" onclick={selectAll}>
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Selecionar Todos
</button>
<button type="button" class="btn btn-sm btn-outline" onclick={deselectAll}>
<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>
Desmarcar Todos
</button>
</div>
<!-- Grid de checkboxes -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6 max-h-96 overflow-y-auto p-2 border rounded-lg bg-base-200">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.dadosPessoais} />
<span class="label-text">Dados Pessoais</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.filiacao} />
<span class="label-text">Filiação</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.naturalidade} />
<span class="label-text">Naturalidade</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.documentos} />
<span class="label-text">Documentos</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.formacao} />
<span class="label-text">Formação</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.saude} />
<span class="label-text">Saúde</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.endereco} />
<span class="label-text">Endereço</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.contato} />
<span class="label-text">Contato</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.cargo} />
<span class="label-text">Cargo e Vínculo</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.bancario} />
<span class="label-text">Dados Bancários</span>
</label>
</div>
<!-- Ações -->
<div class="modal-action">
<button type="button" class="btn btn-ghost" onclick={onClose} disabled={generating}>
Cancelar
</button>
<button type="button" class="btn btn-primary gap-2" onclick={gerarPDF} disabled={generating}>
{#if generating}
<span class="loading loading-spinner loading-sm"></span>
Gerando PDF...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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>
Gerar PDF
{/if}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="button" onclick={onClose}>fechar</button>
</form>
</dialog>

View File

@@ -0,0 +1,49 @@
// Constantes para selects e opções do formulário
export const SEXO_OPTIONS = [
{ value: "masculino", label: "Masculino" },
{ value: "feminino", label: "Feminino" },
{ value: "outro", label: "Outro" },
];
export const ESTADO_CIVIL_OPTIONS = [
{ value: "solteiro", label: "Solteiro(a)" },
{ value: "casado", label: "Casado(a)" },
{ value: "divorciado", label: "Divorciado(a)" },
{ value: "viuvo", label: "Viúvo(a)" },
{ value: "uniao_estavel", label: "União Estável" },
];
export const GRAU_INSTRUCAO_OPTIONS = [
{ value: "fundamental", label: "Ensino Fundamental" },
{ value: "medio", label: "Ensino Médio" },
{ value: "superior", label: "Ensino Superior" },
{ value: "pos_graduacao", label: "Pós-Graduação" },
{ value: "mestrado", label: "Mestrado" },
{ value: "doutorado", label: "Doutorado" },
];
export const GRUPO_SANGUINEO_OPTIONS = [
{ value: "A", label: "A" },
{ value: "B", label: "B" },
{ value: "AB", label: "AB" },
{ value: "O", label: "O" },
];
export const FATOR_RH_OPTIONS = [
{ value: "positivo", label: "Positivo (+)" },
{ value: "negativo", label: "Negativo (-)" },
];
export const APOSENTADO_OPTIONS = [
{ value: "nao", label: "Não" },
{ value: "funape_ipsep", label: "FUNAPE/IPSEP" },
{ value: "inss", label: "INSS" },
];
export const UFS_BRASIL = [
"AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA",
"MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN",
"RS", "RO", "RR", "SC", "SP", "SE", "TO"
];

View File

@@ -0,0 +1,581 @@
import jsPDF from 'jspdf';
import type { Doc } from '@sgse-app/backend/convex/_generated/dataModel';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
type Funcionario = Doc<'funcionarios'>;
// Helper para adicionar logo no canto superior esquerdo
async function addLogo(doc: jsPDF): Promise<number> {
try {
// Criar uma promise para carregar a imagem
const logoImg = await new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous'; // Para evitar problemas de CORS
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
// Timeout de 3 segundos
setTimeout(() => reject(new Error('Timeout loading logo')), 3000);
// Importante: definir src depois de definir os handlers
img.src = logoGovPE;
});
// Logo proporcional: largura 25mm, altura ajustada automaticamente
const logoWidth = 25;
const aspectRatio = logoImg.height / logoImg.width;
const logoHeight = logoWidth * aspectRatio;
// Adicionar a imagem ao PDF
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
// Retorna a posição Y onde o conteúdo pode começar (logo + margem)
return 10 + logoHeight + 5;
} catch (err) {
console.error('Erro ao carregar logo:', err);
return 20; // Posição padrão se a logo falhar
}
}
// Helper para adicionar texto formatado
function addText(doc: jsPDF, text: string, x: number, y: number, options?: { bold?: boolean; size?: number; align?: 'left' | 'center' | 'right' }) {
if (options?.bold) {
doc.setFont('helvetica', 'bold');
} else {
doc.setFont('helvetica', 'normal');
}
if (options?.size) {
doc.setFontSize(options.size);
}
const align = options?.align || 'left';
doc.text(text, x, y, { align });
}
// Helper para adicionar campo com valor
function addField(doc: jsPDF, label: string, value: string, x: number, y: number, width?: number) {
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text(label, x, y);
doc.setFont('helvetica', 'normal');
const labelWidth = doc.getTextWidth(label) + 2;
if (width) {
// Desenhar linha para preenchimento
doc.line(x + labelWidth, y + 1, x + width, y + 1);
if (value) {
doc.text(value, x + labelWidth + 2, y);
}
} else {
doc.text(value || '_____________________', x + labelWidth + 2, y);
}
return y + 7;
}
/**
* 1. Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos
*/
export async function gerarDeclaracaoAcumulacaoCargo(funcionario: Funcionario): Promise<Blob> {
const doc = new jsPDF();
// Adicionar logo e obter posição inicial do conteúdo
let y = await addLogo(doc);
// Cabeçalho (ao lado da logo)
addText(doc, 'GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(y - 10, 20), { bold: true, size: 14, align: 'center' });
addText(doc, 'SECRETARIA DE ESPORTES', 105, Math.max(y - 2, 28), { bold: true, size: 12, align: 'center' });
y = Math.max(y, 40);
y += 5;
addText(doc, 'DECLARAÇÃO DE ACUMULAÇÃO DE CARGO, EMPREGO,', 105, y, { bold: true, size: 12, align: 'center' });
y += 6;
addText(doc, 'FUNÇÃO PÚBLICA OU PROVENTOS', 105, y, { bold: true, size: 12, align: 'center' });
y += 15;
// Corpo
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const text1 = `Eu, ${funcionario.nome}, portador(a) do CPF nº ${funcionario.cpf}, `;
const text2 = `inscrito(a) no RG nº ${funcionario.rg}, residente e domiciliado(a) à ${funcionario.endereco}, `;
const text3 = `${funcionario.cidade}/${funcionario.uf}, DECLARO, para os devidos fins, que:`;
doc.text(text1, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text2, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text3, 20, y, { maxWidth: 170 });
y += 15;
// Opções
doc.setFont('helvetica', 'bold');
doc.text('( ) NÃO EXERÇO', 25, y);
y += 7;
doc.setFont('helvetica', 'normal');
doc.text('Outro cargo, emprego ou função pública, bem como não percebo proventos de', 30, y, { maxWidth: 160 });
y += 5;
doc.text('aposentadoria de regime próprio de previdência social ou do regime geral de', 30, y, { maxWidth: 160 });
y += 5;
doc.text('previdência social.', 30, y);
y += 12;
doc.setFont('helvetica', 'bold');
doc.text('( ) EXERÇO', 25, y);
y += 7;
doc.setFont('helvetica', 'normal');
doc.text('Outro cargo, emprego ou função pública, conforme discriminado abaixo:', 30, y, { maxWidth: 160 });
y += 10;
// Campos para preenchimento de outro cargo
y = addField(doc, 'Órgão/Entidade:', funcionario.orgaoOrigem || '', 30, y, 160);
y = addField(doc, 'Cargo/Função:', '', 30, y, 160);
y = addField(doc, 'Carga Horária:', '', 30, y, 80);
y = addField(doc, 'Remuneração:', '', 30, y, 80);
y += 5;
doc.setFont('helvetica', 'bold');
doc.text('( ) PERCEBO', 25, y);
y += 7;
doc.setFont('helvetica', 'normal');
doc.text('Proventos de aposentadoria:', 30, y);
y += 10;
y = addField(doc, 'Regime:', funcionario.aposentado === 'funape_ipsep' ? 'FUNAPE/IPSEP' : funcionario.aposentado === 'inss' ? 'INSS' : '', 30, y, 160);
y = addField(doc, 'Valor:', '', 30, y, 80);
y += 15;
// Declaração de veracidade
doc.text('Declaro, ainda, que estou ciente de que a acumulação ilegal de cargos,', 20, y, { maxWidth: 170 });
y += 5;
doc.text('empregos ou funções públicas constitui infração administrativa, sujeitando-me', 20, y, { maxWidth: 170 });
y += 5;
doc.text('às sanções legais cabíveis.', 20, y);
y += 20;
// Data e local
const hoje = new Date().toLocaleDateString('pt-BR');
doc.text(`Recife, ${hoje}`, 20, y);
y += 25;
// Assinatura
doc.line(70, y, 140, y);
y += 5;
addText(doc, funcionario.nome, 105, y, { align: 'center' });
y += 5;
addText(doc, `CPF: ${funcionario.cpf}`, 105, y, { size: 9, align: 'center' });
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
return doc.output('blob');
}
/**
* 2. Declaração de Dependentes para Fins de Imposto de Renda
*/
export async function gerarDeclaracaoDependentesIR(funcionario: Funcionario): Promise<Blob> {
const doc = new jsPDF();
// Adicionar logo e obter posição inicial do conteúdo
let y = await addLogo(doc);
// Cabeçalho (ao lado da logo)
addText(doc, 'GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(y - 10, 20), { bold: true, size: 14, align: 'center' });
addText(doc, 'SECRETARIA DE ESPORTES', 105, Math.max(y - 2, 28), { bold: true, size: 12, align: 'center' });
y = Math.max(y, 40);
y += 5;
addText(doc, 'DECLARAÇÃO DE DEPENDENTES', 105, y, { bold: true, size: 12, align: 'center' });
y += 6;
addText(doc, 'PARA FINS DE IMPOSTO DE RENDA', 105, y, { bold: true, size: 12, align: 'center' });
y += 15;
// Corpo
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const text1 = `Eu, ${funcionario.nome}, portador(a) do CPF nº ${funcionario.cpf}, `;
const text2 = `inscrito(a) no RG nº ${funcionario.rg}, matrícula nº ${funcionario.matricula}, `;
const text3 = `DECLARO, para fins de dedução no Imposto de Renda na Fonte, que possuo os seguintes dependentes:`;
doc.text(text1, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text2, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text3, 20, y, { maxWidth: 170 });
y += 15;
// Tabela de dependentes
doc.setFont('helvetica', 'bold');
doc.setFontSize(10);
doc.text('NOME', 20, y);
doc.text('CPF', 80, y);
doc.text('PARENTESCO', 130, y);
doc.text('NASC.', 175, y);
y += 2;
doc.line(20, y, 195, y);
y += 8;
// Linhas para preenchimento (5 linhas)
doc.setFont('helvetica', 'normal');
for (let i = 0; i < 5; i++) {
doc.line(20, y, 75, y);
doc.line(80, y, 125, y);
doc.line(130, y, 170, y);
doc.line(175, y, 195, y);
y += 12;
}
y += 10;
// Declaração de veracidade
doc.setFontSize(11);
doc.text('Declaro estar ciente de que a inclusão de dependente sem direito constitui', 20, y, { maxWidth: 170 });
y += 5;
doc.text('falsidade ideológica, sujeitando-me às penalidades previstas em lei, inclusive', 20, y, { maxWidth: 170 });
y += 5;
doc.text('ao recolhimento do imposto devido acrescido de multa e juros.', 20, y, { maxWidth: 170 });
y += 20;
// Data e local
const hoje = new Date().toLocaleDateString('pt-BR');
doc.text(`Recife, ${hoje}`, 20, y);
y += 25;
// Assinatura
doc.line(70, y, 140, y);
y += 5;
addText(doc, funcionario.nome, 105, y, { align: 'center' });
y += 5;
addText(doc, `CPF: ${funcionario.cpf} | Matrícula: ${funcionario.matricula}`, 105, y, { size: 9, align: 'center' });
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
return doc.output('blob');
}
/**
* 3. Declaração de Idoneidade
*/
export async function gerarDeclaracaoIdoneidade(funcionario: Funcionario): Promise<Blob> {
const doc = new jsPDF();
// Adicionar logo e obter posição inicial do conteúdo
let y = await addLogo(doc);
// Cabeçalho (ao lado da logo)
addText(doc, 'GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(y - 10, 20), { bold: true, size: 14, align: 'center' });
addText(doc, 'SECRETARIA DE ESPORTES', 105, Math.max(y - 2, 28), { bold: true, size: 12, align: 'center' });
y = Math.max(y, 40);
y += 5;
addText(doc, 'DECLARAÇÃO DE IDONEIDADE MORAL', 105, y, { bold: true, size: 12, align: 'center' });
y += 15;
// Corpo
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const text1 = `Eu, ${funcionario.nome}, portador(a) do CPF nº ${funcionario.cpf}, `;
const text2 = `inscrito(a) no RG nº ${funcionario.rg}, residente e domiciliado(a) à ${funcionario.endereco}, `;
const text3 = `${funcionario.cidade}/${funcionario.uf}, DECLARO, sob as penas da lei, que:`;
doc.text(text1, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text2, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text3, 20, y, { maxWidth: 170 });
y += 15;
// Itens da declaração
const itens = [
'Gozo de boa saúde física e mental para o exercício das atribuições do cargo/função;',
'Não fui condenado(a) por crime contra a Administração Pública;',
'Não fui condenado(a) por ato de improbidade administrativa;',
'Não sofri, no exercício de função pública, penalidade incompatível com a investidura em cargo público;',
'Não estou em situação de incompatibilidade ou impedimento para o exercício de cargo ou função pública;',
'Tenho idoneidade moral e reputação ilibada;',
'Não respondo a processo administrativo disciplinar em qualquer esfera da Administração Pública;',
'Não fui demitido(a) ou exonerado(a) de cargo ou função pública por justa causa.'
];
itens.forEach((item, index) => {
doc.text(`${index + 1}. ${item}`, 20, y, { maxWidth: 170 });
y += 12;
});
y += 10;
// Declaração de veracidade
doc.text('Declaro, ainda, que todas as informações aqui prestadas são verdadeiras,', 20, y, { maxWidth: 170 });
y += 5;
doc.text('estando ciente de que a falsidade desta declaração configura crime previsto no', 20, y, { maxWidth: 170 });
y += 5;
doc.text('Código Penal Brasileiro, passível de apuração na forma da lei.', 20, y);
y += 20;
// Data e local
const hoje = new Date().toLocaleDateString('pt-BR');
doc.text(`Recife, ${hoje}`, 20, y);
y += 25;
// Assinatura
doc.line(70, y, 140, y);
y += 5;
addText(doc, funcionario.nome, 105, y, { align: 'center' });
y += 5;
addText(doc, `CPF: ${funcionario.cpf}`, 105, y, { size: 9, align: 'center' });
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
return doc.output('blob');
}
/**
* 4. Termo de Declaração de Nepotismo
*/
export async function gerarTermoNepotismo(funcionario: Funcionario): Promise<Blob> {
const doc = new jsPDF();
// Adicionar logo e obter posição inicial do conteúdo
let y = await addLogo(doc);
// Cabeçalho (ao lado da logo)
addText(doc, 'GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(y - 10, 20), { bold: true, size: 14, align: 'center' });
addText(doc, 'SECRETARIA DE ESPORTES', 105, Math.max(y - 2, 28), { bold: true, size: 12, align: 'center' });
y = Math.max(y, 40);
y += 5;
addText(doc, 'TERMO DE DECLARAÇÃO DE NEPOTISMO', 105, y, { bold: true, size: 12, align: 'center' });
y += 15;
// Corpo
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const text1 = `Eu, ${funcionario.nome}, portador(a) do CPF nº ${funcionario.cpf}, `;
const text2 = `inscrito(a) no RG nº ${funcionario.rg}, matrícula nº ${funcionario.matricula}, `;
const text3 = `nomeado(a) para o cargo/função de ${funcionario.descricaoCargo || '_________________'}, `;
const text4 = `DECLARO, para os fins do disposto na Súmula Vinculante nº 13 do STF e demais `;
const text5 = `normas de combate ao nepotismo, que:`;
doc.text(text1, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text2, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text3, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text4, 20, y, { maxWidth: 170 });
y += 5;
doc.text(text5, 20, y, { maxWidth: 170 });
y += 15;
// Opções
doc.setFont('helvetica', 'bold');
doc.text('( ) NÃO POSSUO', 25, y);
y += 7;
doc.setFont('helvetica', 'normal');
doc.text('Cônjuge, companheiro(a) ou parente em linha reta, colateral ou por afinidade, até', 30, y, { maxWidth: 160 });
y += 5;
doc.text('o terceiro grau, exercendo cargo em comissão ou função de confiança nesta', 30, y, { maxWidth: 160 });
y += 5;
doc.text('Secretaria ou em órgão a ela vinculado.', 30, y);
y += 12;
doc.setFont('helvetica', 'bold');
doc.text('( ) POSSUO', 25, y);
y += 7;
doc.setFont('helvetica', 'normal');
doc.text('O(s) seguinte(s) parente(s) com vínculo nesta Secretaria:', 30, y);
y += 10;
// Campos para parentes
for (let i = 0; i < 3; i++) {
y = addField(doc, 'Nome:', '', 30, y, 160);
y = addField(doc, 'CPF:', '', 30, y, 80);
y = addField(doc, 'Grau de Parentesco:', '', 110, y - 7, 80);
y = addField(doc, 'Cargo/Função:', '', 30, y, 160);
y = addField(doc, 'Órgão:', '', 30, y, 160);
y += 8;
}
y += 5;
// Declaração de veracidade
doc.text('Declaro estar ciente de que a nomeação, designação ou contratação em', 20, y, { maxWidth: 170 });
y += 5;
doc.text('desconformidade com as vedações ao nepotismo importará em nulidade do ato,', 20, y, { maxWidth: 170 });
y += 5;
doc.text('sem prejuízo das sanções administrativas, civis e penais cabíveis.', 20, y);
y += 20;
// Data e local
const hoje = new Date().toLocaleDateString('pt-BR');
doc.text(`Recife, ${hoje}`, 20, y);
y += 25;
// Assinatura
doc.line(70, y, 140, y);
y += 5;
addText(doc, funcionario.nome, 105, y, { align: 'center' });
y += 5;
addText(doc, `CPF: ${funcionario.cpf} | Matrícula: ${funcionario.matricula}`, 105, y, { size: 9, align: 'center' });
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
return doc.output('blob');
}
/**
* 5. Termo de Opção - Remuneração
*/
export async function gerarTermoOpcaoRemuneracao(funcionario: Funcionario): Promise<Blob> {
const doc = new jsPDF();
// Adicionar logo e obter posição inicial do conteúdo
let y = await addLogo(doc);
// Cabeçalho (ao lado da logo)
addText(doc, 'GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(y - 10, 20), { bold: true, size: 14, align: 'center' });
addText(doc, 'SECRETARIA DE ESPORTES', 105, Math.max(y - 2, 28), { bold: true, size: 12, align: 'center' });
y = Math.max(y, 40);
y += 5;
addText(doc, 'TERMO DE OPÇÃO DE REMUNERAÇÃO', 105, y, { bold: true, size: 12, align: 'center' });
y += 15;
// Corpo
doc.setFontSize(11);
doc.setFont('helvetica', 'normal');
const text1 = `Eu, ${funcionario.nome}, portador(a) do CPF nº ${funcionario.cpf}, `;
const text2 = `inscrito(a) no RG nº ${funcionario.rg}, matrícula nº ${funcionario.matricula}, `;
const text3 = `nomeado(a) para o cargo/função de ${funcionario.descricaoCargo || '_________________'}, `;
const text4 = `nos termos do Ato/Portaria nº ${funcionario.nomeacaoPortaria || '_____'} de ${funcionario.nomeacaoData || '___/___/___'}, `;
const text5 = `DECLARO, para os devidos fins, que:`;
doc.text(text1, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text2, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text3, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text4, 20, y, { maxWidth: 170 });
y += 7;
doc.text(text5, 20, y);
y += 15;
// Seção 1 - Vínculo Anterior
doc.setFont('helvetica', 'bold');
doc.text('1. QUANTO AO VÍNCULO ANTERIOR:', 20, y);
y += 10;
doc.setFont('helvetica', 'normal');
doc.text('( ) NÃO POSSUO outro vínculo com a Administração Pública', 25, y);
y += 10;
doc.text('( ) POSSUO vínculo efetivo com:', 25, y);
y += 8;
y = addField(doc, 'Órgão/Entidade:', funcionario.orgaoOrigem || '', 30, y, 160);
y = addField(doc, 'Cargo:', '', 30, y, 160);
y = addField(doc, 'Matrícula:', '', 30, y, 80);
y += 10;
// Seção 2 - Opção de Remuneração
doc.setFont('helvetica', 'bold');
doc.text('2. QUANTO À REMUNERAÇÃO, OPTO POR RECEBER:', 20, y);
y += 10;
doc.setFont('helvetica', 'normal');
doc.text('( ) A remuneração do cargo em comissão/função gratificada ora assumido', 25, y);
y += 10;
doc.text('( ) A remuneração do cargo efetivo + a gratificação/símbolo', 25, y);
y += 10;
doc.text('( ) A remuneração do cargo efetivo (sem percepção de gratificação)', 25, y);
y += 15;
// Seção 3 - Dados Bancários
doc.setFont('helvetica', 'bold');
doc.text('3. DADOS BANCÁRIOS PARA PAGAMENTO:', 20, y);
y += 10;
doc.setFont('helvetica', 'normal');
y = addField(doc, 'Banco:', 'Bradesco', 20, y, 80);
y = addField(doc, 'Agência:', funcionario.contaBradescoAgencia || '', 110, y - 7, 80);
y = addField(doc, 'Conta Corrente:', funcionario.contaBradescoNumero || '', 20, y, 80);
y = addField(doc, 'Dígito:', funcionario.contaBradescoDV || '', 110, y - 7, 40);
y += 15;
// Declaração de ciência
doc.text('Declaro estar ciente de que:', 20, y);
y += 8;
const ciencias = [
'A remuneração será paga conforme a opção acima, respeitada a legislação vigente;',
'Qualquer alteração na opção deverá ser comunicada formalmente à Secretaria;',
'A não apresentação deste termo poderá implicar em atraso no pagamento;',
'As informações aqui prestadas são verdadeiras e atualizadas.'
];
ciencias.forEach((item, index) => {
doc.text(`${index + 1}. ${item}`, 25, y, { maxWidth: 165 });
y += 10;
});
y += 5;
// Data e local
const hoje = new Date().toLocaleDateString('pt-BR');
doc.text(`Recife, ${hoje}`, 20, y);
y += 25;
// Assinatura
doc.line(70, y, 140, y);
y += 5;
addText(doc, funcionario.nome, 105, y, { align: 'center' });
y += 5;
addText(doc, `CPF: ${funcionario.cpf} | Matrícula: ${funcionario.matricula}`, 105, y, { size: 9, align: 'center' });
// Rodapé
doc.setFontSize(8);
doc.setTextColor(100);
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
return doc.output('blob');
}
// Função helper para download
export function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}

View File

@@ -0,0 +1,187 @@
// Definições dos documentos com URLs de referência
export interface DocumentoDefinicao {
campo: string;
nome: string;
helpUrl?: string;
categoria: string;
}
export const documentos: DocumentoDefinicao[] = [
// Antecedentes Criminais
{
campo: "certidaoAntecedentesPF",
nome: "Certidão de Antecedentes Criminais - Polícia Federal",
helpUrl: "https://servicos.pf.gov.br/epol-sinic-publico/",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesJFPE",
nome: "Certidão de Antecedentes Criminais - Justiça Federal de Pernambuco",
helpUrl: "https://certidoes.trf5.jus.br/certidoes2022/paginas/certidaocriminal.faces",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesSDS",
nome: "Certidão de Antecedentes Criminais - SDS-PE",
helpUrl: "http://www.servicos.sds.pe.gov.br/antecedentes/public/pages/certidaoAntecedentesCriminais/certidaoAntecedentesCriminaisEmitir.jsf",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoAntecedentesTJPE",
nome: "Certidão de Antecedentes Criminais - TJPE",
helpUrl: "https://certidoesunificadas.app.tjpe.jus.br/certidao-criminal-pf",
categoria: "Antecedentes Criminais",
},
{
campo: "certidaoImprobidade",
nome: "Certidão Improbidade Administrativa",
helpUrl: "https://www.cnj.jus.br/improbidade_adm/consultar_requerido.php",
categoria: "Antecedentes Criminais",
},
// Documentos Pessoais
{
campo: "rgFrente",
nome: "Carteira de Identidade SDS/PE ou (SSP-PE) - Frente",
categoria: "Documentos Pessoais",
},
{
campo: "rgVerso",
nome: "Carteira de Identidade SDS/PE ou (SSP-PE) - Verso",
categoria: "Documentos Pessoais",
},
{
campo: "cpfFrente",
nome: "CPF/CIC - Frente",
categoria: "Documentos Pessoais",
},
{
campo: "cpfVerso",
nome: "CPF/CIC - Verso",
categoria: "Documentos Pessoais",
},
{
campo: "situacaoCadastralCPF",
nome: "Situação Cadastral CPF",
helpUrl: "https://servicos.receita.fazenda.gov.br/servicos/cpf/consultasituacao/consultapublica.asp",
categoria: "Documentos Pessoais",
},
{
campo: "certidaoRegistroCivil",
nome: "Certidão de Registro Civil (Nascimento, Casamento ou União Estável)",
categoria: "Documentos Pessoais",
},
// Documentos Eleitorais
{
campo: "tituloEleitorFrente",
nome: "Título de Eleitor - Frente",
categoria: "Documentos Eleitorais",
},
{
campo: "tituloEleitorVerso",
nome: "Título de Eleitor - Verso",
categoria: "Documentos Eleitorais",
},
{
campo: "comprovanteVotacao",
nome: "Comprovante de Votação Última Eleição ou Certidão de Quitação Eleitoral",
helpUrl: "https://www.tse.jus.br",
categoria: "Documentos Eleitorais",
},
// Documentos Profissionais
{
campo: "carteiraProfissionalFrente",
nome: "Carteira Profissional - Frente (página da foto)",
categoria: "Documentos Profissionais",
},
{
campo: "carteiraProfissionalVerso",
nome: "Carteira Profissional - Verso (página da foto)",
categoria: "Documentos Profissionais",
},
{
campo: "comprovantePIS",
nome: "Comprovante de PIS/PASEP",
categoria: "Documentos Profissionais",
},
{
campo: "reservistaDoc",
nome: "Reservista (obrigatória para homem até 45 anos)",
categoria: "Documentos Profissionais",
},
// Certidões e Comprovantes
{
campo: "certidaoNascimentoDependentes",
nome: "Certidão de Nascimento do(s) Dependente(s) para Imposto de Renda",
categoria: "Certidões e Comprovantes",
},
{
campo: "cpfDependentes",
nome: "CPF do(s) Dependente(s) para Imposto de Renda",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteEscolaridade",
nome: "Documento de Comprovação do Nível de Escolaridade",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteResidencia",
nome: "Comprovante de Residência",
categoria: "Certidões e Comprovantes",
},
{
campo: "comprovanteContaBradesco",
nome: "Comprovante de Conta-Corrente no Banco BRADESCO",
categoria: "Certidões e Comprovantes",
},
// Declarações
{
campo: "declaracaoAcumulacaoCargo",
nome: "Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos",
categoria: "Declarações",
},
{
campo: "declaracaoDependentesIR",
nome: "Declaração de Dependentes para Fins de Imposto de Renda",
categoria: "Declarações",
},
{
campo: "declaracaoIdoneidade",
nome: "Declaração de Idoneidade",
categoria: "Declarações",
},
{
campo: "termoNepotismo",
nome: "Termo de Declaração de Nepotismo",
categoria: "Declarações",
},
{
campo: "termoOpcaoRemuneracao",
nome: "Termo de Opção - Remuneração",
categoria: "Declarações",
},
];
export const categoriasDocumentos = [
"Antecedentes Criminais",
"Documentos Pessoais",
"Documentos Eleitorais",
"Documentos Profissionais",
"Certidões e Comprovantes",
"Declarações",
];
export function getDocumentosByCategoria(categoria: string): DocumentoDefinicao[] {
return documentos.filter(doc => doc.categoria === categoria);
}
export function getDocumentoDefinicao(campo: string): DocumentoDefinicao | undefined {
return documentos.find(doc => doc.campo === campo);
}

View File

@@ -0,0 +1,176 @@
// Helper functions for input masks and validations
/** Remove all non-digit characters from string */
export const onlyDigits = (value: string): string => {
return (value || "").replace(/\D/g, "");
};
/** Format CPF: 000.000.000-00 */
export const maskCPF = (value: string): string => {
const digits = onlyDigits(value).slice(0, 11);
return digits
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
};
/** Validate CPF format and checksum */
export const validateCPF = (value: string): boolean => {
const digits = onlyDigits(value);
if (digits.length !== 11 || /^([0-9])\1+$/.test(digits)) {
return false;
}
const calculateDigit = (base: string, factor: number): number => {
let sum = 0;
for (let i = 0; i < base.length; i++) {
sum += parseInt(base[i]) * (factor - i);
}
const rest = (sum * 10) % 11;
return rest === 10 ? 0 : rest;
};
const digit1 = calculateDigit(digits.slice(0, 9), 10);
const digit2 = calculateDigit(digits.slice(0, 10), 11);
return digits[9] === String(digit1) && digits[10] === String(digit2);
};
/** Format CEP: 00000-000 */
export const maskCEP = (value: string): string => {
const digits = onlyDigits(value).slice(0, 8);
return digits.replace(/(\d{5})(\d{1,3})$/, "$1-$2");
};
/** Format phone: (00) 0000-0000 or (00) 00000-0000 */
export const maskPhone = (value: string): string => {
const digits = onlyDigits(value).slice(0, 11);
if (digits.length <= 10) {
return digits
.replace(/(\d{2})(\d)/, "($1) $2")
.replace(/(\d{4})(\d{1,4})$/, "$1-$2");
}
return digits
.replace(/(\d{2})(\d)/, "($1) $2")
.replace(/(\d{5})(\d{1,4})$/, "$1-$2");
};
/** Format date: dd/mm/aaaa */
export const maskDate = (value: string): string => {
const digits = onlyDigits(value).slice(0, 8);
return digits
.replace(/(\d{2})(\d)/, "$1/$2")
.replace(/(\d{2})(\d{1,4})$/, "$1/$2");
};
/** Validate date in format dd/mm/aaaa */
export const validateDate = (value: string): boolean => {
const match = value.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!match) return false;
const day = Number(match[1]);
const month = Number(match[2]) - 1;
const year = Number(match[3]);
const date = new Date(year, month, day);
return (
date.getFullYear() === year &&
date.getMonth() === month &&
date.getDate() === day
);
};
/** Format UF: uppercase, max 2 chars */
export const maskUF = (value: string): string => {
return (value || "").toUpperCase().replace(/[^A-Z]/g, "").slice(0, 2);
};
/** Format RG by UF */
const rgFormatByUF: Record<string, [number, number, number, number]> = {
RJ: [2, 3, 2, 1],
SP: [2, 3, 3, 1],
MG: [2, 3, 3, 1],
ES: [2, 3, 3, 1],
PR: [2, 3, 3, 1],
SC: [2, 3, 3, 1],
RS: [2, 3, 3, 1],
BA: [2, 3, 3, 1],
PE: [2, 3, 3, 1],
CE: [2, 3, 3, 1],
PA: [2, 3, 3, 1],
AM: [2, 3, 3, 1],
AC: [2, 3, 3, 1],
AP: [2, 3, 3, 1],
AL: [2, 3, 3, 1],
RN: [2, 3, 3, 1],
PB: [2, 3, 3, 1],
MA: [2, 3, 3, 1],
PI: [2, 3, 3, 1],
DF: [2, 3, 3, 1],
GO: [2, 3, 3, 1],
MT: [2, 3, 3, 1],
MS: [2, 3, 3, 1],
RO: [2, 3, 3, 1],
RR: [2, 3, 3, 1],
TO: [2, 3, 3, 1],
};
export const maskRGByUF = (uf: string, value: string): string => {
const raw = (value || "").toUpperCase().replace(/[^0-9X]/g, "");
const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1];
const baseMax = a + b + c;
const baseDigits = raw.replace(/X/g, "").slice(0, baseMax);
const verifier = raw.slice(baseDigits.length, baseDigits.length + dv).slice(0, 1);
const g1 = baseDigits.slice(0, a);
const g2 = baseDigits.slice(a, a + b);
const g3 = baseDigits.slice(a + b, a + b + c);
let formatted = g1;
if (g2) formatted += `.${g2}`;
if (g3) formatted += `.${g3}`;
if (verifier) formatted += `-${verifier}`;
return formatted;
};
export const padRGLeftByUF = (uf: string, value: string): string => {
const raw = (value || "").toUpperCase().replace(/[^0-9X]/g, "");
const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1];
const baseMax = a + b + c;
let base = raw.replace(/X/g, "");
const verifier = raw.slice(base.length, base.length + dv).slice(0, 1);
if (base.length < baseMax) {
base = base.padStart(baseMax, "0");
}
return maskRGByUF(uf, base + (verifier || ""));
};
/** Format account number */
export const maskContaBancaria = (value: string): string => {
const digits = onlyDigits(value);
return digits;
};
/** Format zone and section for voter title */
export const maskZonaSecao = (value: string): string => {
const digits = onlyDigits(value).slice(0, 4);
return digits;
};
/** Format general numeric field */
export const maskNumeric = (value: string): string => {
return onlyDigits(value);
};
/** Remove extra spaces and trim */
export const normalizeText = (value: string): string => {
return (value || "").replace(/\s+/g, " ").trim();
};

View File

@@ -0,0 +1,52 @@
// Definições dos modelos de declaração
export interface ModeloDeclaracao {
id: string;
nome: string;
descricao: string;
arquivo: string;
podePreencherAutomaticamente: boolean;
}
export const modelosDeclaracoes: ModeloDeclaracao[] = [
{
id: "acumulacao_cargo",
nome: "Declaração de Acumulação de Cargo",
descricao: "Declaração sobre acumulação de cargo, emprego, função pública ou proventos",
arquivo: "/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf",
podePreencherAutomaticamente: true,
},
{
id: "dependentes_ir",
nome: "Declaração de Dependentes",
descricao: "Declaração de dependentes para fins de Imposto de Renda",
arquivo: "/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf",
podePreencherAutomaticamente: true,
},
{
id: "idoneidade",
nome: "Declaração de Idoneidade",
descricao: "Declaração de idoneidade moral e conduta ilibada",
arquivo: "/modelos/declaracoes/Declaração de Idoneidade.pdf",
podePreencherAutomaticamente: true,
},
{
id: "nepotismo",
nome: "Termo de Declaração de Nepotismo",
descricao: "Declaração sobre inexistência de situação de nepotismo",
arquivo: "/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf",
podePreencherAutomaticamente: true,
},
{
id: "opcao_remuneracao",
nome: "Termo de Opção - Remuneração",
descricao: "Termo de opção de remuneração",
arquivo: "/modelos/declaracoes/Termo de Opção - Remuneração.pdf",
podePreencherAutomaticamente: true,
},
];
export function getModeloById(id: string): ModeloDeclaracao | undefined {
return modelosDeclaracoes.find(modelo => modelo.id === id);
}

View File

@@ -3,6 +3,7 @@
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from "@sgse-app/backend/convex/_generated/api";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { SimboloTipo } from "@sgse-app/backend/convex/schema"; import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
import PrintModal from "$lib/components/PrintModal.svelte";
const client = useConvexClient(); const client = useConvexClient();
@@ -12,6 +13,7 @@
let deletingId: string | null = null; let deletingId: string | null = null;
let toDelete: { id: string; nome: string } | null = null; let toDelete: { id: string; nome: string } | null = null;
let openMenuId: string | null = null; let openMenuId: string | null = null;
let funcionarioParaImprimir: any = null;
let filtroNome = ""; let filtroNome = "";
let filtroCPF = ""; let filtroCPF = "";
@@ -48,6 +50,18 @@
toDelete = null; toDelete = null;
(document.getElementById("delete_modal_func") as HTMLDialogElement)?.close(); (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() { async function confirmDelete() {
if (!toDelete) return; if (!toDelete) return;
try { 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> <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> </button>
<ul class="dropdown-content menu bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg border border-base-300"> <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><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> </ul>
</div> </div>
</td> </td>
@@ -261,5 +278,12 @@
<button>close</button> <button>close</button>
</form> </form>
</dialog> </dialog>
</main>
<!-- Modal de Impressão -->
{#if funcionarioParaImprimir}
<PrintModal
funcionario={funcionarioParaImprimir}
onClose={() => funcionarioParaImprimir = null}
/>
{/if}
</main>

View File

@@ -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}

View File

@@ -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}

400
fix-editar.js Normal file
View File

@@ -0,0 +1,400 @@
const fs = require('fs');
const path = require('path');
const baseDir = 'apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios';
const cadastroPath = path.join(baseDir, 'cadastro/+page.svelte');
const editarPath = path.join(baseDir, '[funcionarioId]/editar/+page.svelte');
console.log('Reading files...');
const cadastro = fs.readFileSync(cadastroPath, 'utf8');
// Create the edit file from scratch
const editContent = `<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
import FileUpload from "$lib/components/FileUpload.svelte";
import {
maskCPF, maskCEP, maskPhone, maskDate, onlyDigits,
validateCPF, validateDate
} from "$lib/utils/masks";
import {
SEXO_OPTIONS, ESTADO_CIVIL_OPTIONS, GRAU_INSTRUCAO_OPTIONS,
GRUPO_SANGUINEO_OPTIONS, FATOR_RH_OPTIONS, APOSENTADO_OPTIONS, UFS_BRASIL
} from "$lib/utils/constants";
import { documentos, categoriasDocumentos, getDocumentosByCategoria } from "$lib/utils/documentos";
import ModelosDeclaracoes from "$lib/components/ModelosDeclaracoes.svelte";
const client = useConvexClient();
let funcionarioId = $derived($page.params.funcionarioId as string);
let simbolos: Array<{
_id: string;
nome: string;
tipo: SimboloTipo;
descricao: string;
}> = [];
let tipo: SimboloTipo = "cargo_comissionado";
let loading = $state(false);
let loadingData = $state(true);
let notice = $state<{ kind: "success" | "error"; text: string } | null>(null);
// Campos obrigatórios
let nome = $state("");
let matricula = $state("");
let cpf = $state("");
let rg = $state("");
let nascimento = $state("");
let email = $state("");
let telefone = $state("");
let endereco = $state("");
let cep = $state("");
let cidade = $state("");
let uf = $state("");
let simboloId = $state("");
let admissaoData = $state("");
// Dados Pessoais Adicionais
let nomePai = $state("");
let nomeMae = $state("");
let naturalidade = $state("");
let naturalidadeUF = $state("");
let sexo = $state("");
let estadoCivil = $state("");
let nacionalidade = $state("Brasileira");
// Documentos Pessoais
let rgOrgaoExpedidor = $state("");
let rgDataEmissao = $state("");
let carteiraProfissionalNumero = $state("");
let carteiraProfissionalSerie = $state("");
let carteiraProfissionalDataEmissao = $state("");
let reservistaNumero = $state("");
let reservistaSerie = $state("");
let tituloEleitorNumero = $state("");
let tituloEleitorZona = $state("");
let tituloEleitorSecao = $state("");
let pisNumero = $state("");
// Formação e Saúde
let grauInstrucao = $state("");
let formacao = $state("");
let formacaoRegistro = $state("");
let grupoSanguineo = $state("");
let fatorRH = $state("");
// Cargo e Vínculo
let descricaoCargo = $state("");
let nomeacaoPortaria = $state("");
let nomeacaoData = $state("");
let nomeacaoDOE = $state("");
let pertenceOrgaoPublico = $state(false);
let orgaoOrigem = $state("");
let aposentado = $state("nao");
// Dados Bancários
let contaBradescoNumero = $state("");
let contaBradescoDV = $state("");
let contaBradescoAgencia = $state("");
// Documentos (Storage IDs)
let documentosStorage: Record<string, string | undefined> = $state({});
async function loadSimbolos() {
const list = await client.query(api.simbolos.getAll, {} as any);
simbolos = list.map((s: any) => ({
_id: s._id,
nome: s.nome,
tipo: s.tipo,
descricao: s.descricao
}));
}
async function loadFuncionario() {
try {
const func = await client.query(api.funcionarios.getById, { id: funcionarioId as any });
// Preencher campos
nome = func.nome;
matricula = func.matricula;
cpf = maskCPF(func.cpf);
rg = func.rg;
nascimento = func.nascimento;
email = func.email;
telefone = maskPhone(func.telefone);
endereco = func.endereco;
cep = maskCEP(func.cep);
cidade = func.cidade;
uf = func.uf;
simboloId = func.simboloId;
tipo = func.simboloTipo;
admissaoData = func.admissaoData || "";
// Dados adicionais
nomePai = func.nomePai || "";
nomeMae = func.nomeMae || "";
naturalidade = func.naturalidade || "";
naturalidadeUF = func.naturalidadeUF || "";
sexo = func.sexo || "";
estadoCivil = func.estadoCivil || "";
nacionalidade = func.nacionalidade || "Brasileira";
rgOrgaoExpedidor = func.rgOrgaoExpedidor || "";
rgDataEmissao = func.rgDataEmissao || "";
carteiraProfissionalNumero = func.carteiraProfissionalNumero || "";
carteiraProfissionalSerie = func.carteiraProfissionalSerie || "";
carteiraProfissionalDataEmissao = func.carteiraProfissionalDataEmissao || "";
reservistaNumero = func.reservistaNumero || "";
reservistaSerie = func.reservistaSerie || "";
tituloEleitorNumero = func.tituloEleitorNumero || "";
tituloEleitorZona = func.tituloEleitorZona || "";
tituloEleitorSecao = func.tituloEleitorSecao || "";
pisNumero = func.pisNumero || "";
grauInstrucao = func.grauInstrucao || "";
formacao = func.formacao || "";
formacaoRegistro = func.formacaoRegistro || "";
grupoSanguineo = func.grupoSanguineo || "";
fatorRH = func.fatorRH || "";
descricaoCargo = func.descricaoCargo || "";
nomeacaoPortaria = func.nomeacaoPortaria || "";
nomeacaoData = func.nomeacaoData || "";
nomeacaoDOE = func.nomeacaoDOE || "";
pertenceOrgaoPublico = func.pertenceOrgaoPublico || false;
orgaoOrigem = func.orgaoOrigem || "";
aposentado = func.aposentado || "nao";
contaBradescoNumero = func.contaBradescoNumero || "";
contaBradescoDV = func.contaBradescoDV || "";
contaBradescoAgencia = func.contaBradescoAgencia || "";
// Documentos
documentosStorage = {};
documentos.forEach(doc => {
const storageId = (func as any)[doc.campo];
if (storageId) {
documentosStorage[doc.campo] = storageId;
}
});
} catch (error) {
console.error("Erro ao carregar funcionário:", error);
notice = { kind: "error", text: "Erro ao carregar dados do funcionário" };
} finally {
loadingData = false;
}
}
async function fillFromCEP(cepValue: string) {
const cepDigits = onlyDigits(cepValue);
if (cepDigits.length !== 8) return;
try {
const res = await fetch(\`https://viacep.com.br/ws/\${cepDigits}/json/\`);
const data = await res.json();
if (!data || data.erro) return;
const enderecoFull = [data.logradouro, data.bairro].filter(Boolean).join(", ");
endereco = enderecoFull;
cidade = data.localidade || "";
uf = data.uf || "";
} catch {}
}
async function handleDocumentoUpload(campo: string, file: File) {
try {
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();
documentosStorage[campo] = storageId;
} catch (err: any) {
throw new Error(err?.message || "Erro ao fazer upload");
}
}
async function handleDocumentoRemove(campo: string) {
documentosStorage[campo] = undefined;
}
async function handleSubmit() {
if (!nome || !matricula || !cpf || !rg || !nascimento || !email || !telefone) {
notice = { kind: "error", text: "Preencha todos os campos obrigatórios" };
return;
}
if (!validateCPF(cpf)) {
notice = { kind: "error", text: "CPF inválido" };
return;
}
if (!validateDate(nascimento)) {
notice = { kind: "error", text: "Data de nascimento inválida" };
return;
}
if (!simboloId) {
notice = { kind: "error", text: "Selecione um símbolo" };
return;
}
try {
loading = true;
const payload = {
nome,
matricula,
cpf: onlyDigits(cpf),
rg: onlyDigits(rg),
nascimento,
email,
telefone: onlyDigits(telefone),
endereco,
cep: onlyDigits(cep),
cidade,
uf: uf.toUpperCase(),
simboloId: simboloId as any,
simboloTipo: tipo,
admissaoData: admissaoData || undefined,
nomePai: nomePai || undefined,
nomeMae: nomeMae || undefined,
naturalidade: naturalidade || undefined,
naturalidadeUF: naturalidadeUF ? naturalidadeUF.toUpperCase() : undefined,
sexo: sexo || undefined,
estadoCivil: estadoCivil || undefined,
nacionalidade: nacionalidade || undefined,
rgOrgaoExpedidor: rgOrgaoExpedidor || undefined,
rgDataEmissao: rgDataEmissao || undefined,
carteiraProfissionalNumero: carteiraProfissionalNumero || undefined,
carteiraProfissionalSerie: carteiraProfissionalSerie || undefined,
carteiraProfissionalDataEmissao: carteiraProfissionalDataEmissao || undefined,
reservistaNumero: reservistaNumero || undefined,
reservistaSerie: reservistaSerie || undefined,
tituloEleitorNumero: tituloEleitorNumero || undefined,
tituloEleitorZona: tituloEleitorZona || undefined,
tituloEleitorSecao: tituloEleitorSecao || undefined,
pisNumero: pisNumero || undefined,
grauInstrucao: grauInstrucao || undefined,
formacao: formacao || undefined,
formacaoRegistro: formacaoRegistro || undefined,
grupoSanguineo: grupoSanguineo || undefined,
fatorRH: fatorRH || undefined,
descricaoCargo: descricaoCargo || undefined,
nomeacaoPortaria: nomeacaoPortaria || undefined,
nomeacaoData: nomeacaoData || undefined,
nomeacaoDOE: nomeacaoDOE || undefined,
pertenceOrgaoPublico: pertenceOrgaoPublico || undefined,
orgaoOrigem: orgaoOrigem || undefined,
aposentado: aposentado || undefined,
contaBradescoNumero: contaBradescoNumero || undefined,
contaBradescoDV: contaBradescoDV || undefined,
contaBradescoAgencia: contaBradescoAgencia || undefined,
...Object.fromEntries(
Object.entries(documentosStorage).map(([key, value]) => [key, value as any])
),
};
await client.mutation(api.funcionarios.update, { id: funcionarioId as any, ...payload as any });
notice = { kind: "success", text: "Funcionário atualizado com sucesso!" };
setTimeout(() => goto("/recursos-humanos/funcionarios"), 600);
} catch (e: any) {
notice = { kind: "error", text: "Erro ao atualizar funcionário." };
} finally {
loading = false;
}
}
async function init() {
await loadSimbolos();
await loadFuncionario();
}
$effect(() => {
if (funcionarioId) {
init();
}
});
</script>
{#if loadingData}
<div class="flex items-center justify-center min-h-screen">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<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>Editar</li>
</ul>
</div>
<!-- Cabeçalho -->
<div class="mb-6">
<div class="flex items-center gap-4 mb-2">
<div class="p-3 bg-yellow-500/20 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-primary">Editar Funcionário</h1>
<p class="text-base-content/70">Atualize as informações do funcionário</p>
</div>
</div>
</div>
<!-- Alertas -->
{#if notice}
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
{#if notice.kind === "success"}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
{:else}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
{/if}
</svg>
<span>{notice.text}</span>
</div>
{/if}
<!-- Formulário de Edição -->
<form class="space-y-6" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
`;
// Extract form from cadastro (from line 294 to 1181)
const cadastroLines = cadastro.split('\n');
const formLines = cadastroLines.slice(293, 1181); // Get lines 294-1181 (0-indexed)
const formContent = formLines.join('\n');
// Replace "Cadastrar" with "Atualizar" in button
const fixedForm = formContent
.replace('Cadastrar Funcionário', 'Atualizar Funcionário')
.replace('Cadastrando...', 'Atualizando...');
const finalContent = editContent + fixedForm + '\n </form>\n </main>\n{/if}';
fs.writeFileSync(editarPath, finalContent, 'utf8');
console.log(`✓ File created successfully!`);
console.log(` Total lines: ${finalContent.split('\n').length}`);
console.log(` File saved to: ${editarPath}`);

258
package-lock.json generated
View File

@@ -28,6 +28,8 @@
"better-auth": "1.3.27", "better-auth": "1.3.27",
"convex": "^1.28.0", "convex": "^1.28.0",
"convex-svelte": "^0.0.11", "convex-svelte": "^0.0.11",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"zod": "^4.0.17" "zod": "^4.0.17"
}, },
"devDependencies": { "devDependencies": {
@@ -46,6 +48,15 @@
"vite": "^7.1.2" "vite": "^7.1.2"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@better-auth/core": { "node_modules/@better-auth/core": {
"version": "1.3.27", "version": "1.3.27",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.3.27.tgz", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.3.27.tgz",
@@ -710,7 +721,6 @@
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -721,7 +731,6 @@
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
@@ -732,7 +741,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -742,14 +750,12 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31", "version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@@ -1310,14 +1316,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sveltejs/acorn-typescript": { "node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"acorn": "^8.9.0" "acorn": "^8.9.0"
@@ -1778,7 +1783,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@@ -1791,11 +1795,30 @@
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@@ -1808,7 +1831,6 @@
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -1870,12 +1892,21 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.20", "version": "2.8.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
@@ -2000,6 +2031,26 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -2020,7 +2071,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -2562,6 +2612,28 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.46.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.3.10", "version": "5.3.10",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz",
@@ -2623,6 +2695,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dompurify": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.240", "version": "1.5.240",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz",
@@ -2700,19 +2782,28 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz",
"integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
} }
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2731,6 +2822,12 @@
} }
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2767,6 +2864,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-network-error": { "node_modules/is-network-error": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
@@ -2783,7 +2900,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
@@ -2808,6 +2924,32 @@
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
}, },
"node_modules/jspdf": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
"integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.9",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -3092,7 +3234,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lucide-svelte": { "node_modules/lucide-svelte": {
@@ -3108,7 +3249,6 @@
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
@@ -3192,6 +3332,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3281,6 +3434,16 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -3301,6 +3464,13 @@
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/remeda": { "node_modules/remeda": {
"version": "2.32.0", "version": "2.32.0",
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.32.0.tgz", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.32.0.tgz",
@@ -3310,6 +3480,16 @@
"type-fest": "^4.41.0" "type-fest": "^4.41.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.52.5", "version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
@@ -3414,11 +3594,20 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.42.2", "version": "5.42.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz",
"integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
@@ -3464,6 +3653,16 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.16", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
@@ -3485,6 +3684,16 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -3654,7 +3863,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -3708,6 +3917,16 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.12", "version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
@@ -3811,7 +4030,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/zod": { "node_modules/zod": {

View File

@@ -16,6 +16,7 @@ import type * as betterAuth__generated_server from "../betterAuth/_generated/ser
import type * as betterAuth_adapter from "../betterAuth/adapter.js"; import type * as betterAuth_adapter from "../betterAuth/adapter.js";
import type * as betterAuth_auth from "../betterAuth/auth.js"; import type * as betterAuth_auth from "../betterAuth/auth.js";
import type * as dashboard from "../dashboard.js"; import type * as dashboard from "../dashboard.js";
import type * as documentos from "../documentos.js";
import type * as funcionarios from "../funcionarios.js"; import type * as funcionarios from "../funcionarios.js";
import type * as healthCheck from "../healthCheck.js"; import type * as healthCheck from "../healthCheck.js";
import type * as http from "../http.js"; import type * as http from "../http.js";
@@ -52,6 +53,7 @@ declare const fullApi: ApiFromModules<{
"betterAuth/adapter": typeof betterAuth_adapter; "betterAuth/adapter": typeof betterAuth_adapter;
"betterAuth/auth": typeof betterAuth_auth; "betterAuth/auth": typeof betterAuth_auth;
dashboard: typeof dashboard; dashboard: typeof dashboard;
documentos: typeof documentos;
funcionarios: typeof funcionarios; funcionarios: typeof funcionarios;
healthCheck: typeof healthCheck; healthCheck: typeof healthCheck;
http: typeof http; http: typeof http;

View File

@@ -0,0 +1,138 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
// Mutation para fazer upload de arquivo e obter o storage ID
export const generateUploadUrl = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
// Mutation para atualizar um campo de documento do funcionário
export const updateDocumento = mutation({
args: {
funcionarioId: v.id("funcionarios"),
campo: v.string(),
storageId: v.union(v.id("_storage"), v.null()),
},
returns: v.null(),
handler: async (ctx, args) => {
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) {
throw new Error("Funcionário não encontrado");
}
// Atualizar o campo específico do documento
await ctx.db.patch(args.funcionarioId, {
[args.campo]: args.storageId,
} as any);
return null;
},
});
// Query para obter URLs de todos os documentos de um funcionário
export const getDocumentosUrls = query({
args: { funcionarioId: v.id("funcionarios") },
returns: v.object({
certidaoAntecedentesPF: v.union(v.string(), v.null()),
certidaoAntecedentesJFPE: v.union(v.string(), v.null()),
certidaoAntecedentesSDS: v.union(v.string(), v.null()),
certidaoAntecedentesTJPE: v.union(v.string(), v.null()),
certidaoImprobidade: v.union(v.string(), v.null()),
rgFrente: v.union(v.string(), v.null()),
rgVerso: v.union(v.string(), v.null()),
cpfFrente: v.union(v.string(), v.null()),
cpfVerso: v.union(v.string(), v.null()),
situacaoCadastralCPF: v.union(v.string(), v.null()),
tituloEleitorFrente: v.union(v.string(), v.null()),
tituloEleitorVerso: v.union(v.string(), v.null()),
comprovanteVotacao: v.union(v.string(), v.null()),
carteiraProfissionalFrente: v.union(v.string(), v.null()),
carteiraProfissionalVerso: v.union(v.string(), v.null()),
comprovantePIS: v.union(v.string(), v.null()),
certidaoRegistroCivil: v.union(v.string(), v.null()),
certidaoNascimentoDependentes: v.union(v.string(), v.null()),
cpfDependentes: v.union(v.string(), v.null()),
reservistaDoc: v.union(v.string(), v.null()),
comprovanteEscolaridade: v.union(v.string(), v.null()),
comprovanteResidencia: v.union(v.string(), v.null()),
comprovanteContaBradesco: v.union(v.string(), v.null()),
declaracaoAcumulacaoCargo: v.union(v.string(), v.null()),
declaracaoDependentesIR: v.union(v.string(), v.null()),
declaracaoIdoneidade: v.union(v.string(), v.null()),
termoNepotismo: v.union(v.string(), v.null()),
termoOpcaoRemuneracao: v.union(v.string(), v.null()),
}),
handler: async (ctx, args) => {
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) {
throw new Error("Funcionário não encontrado");
}
// Gerar URLs para todos os documentos
const urls: Record<string, string | null> = {};
const campos = [
"certidaoAntecedentesPF",
"certidaoAntecedentesJFPE",
"certidaoAntecedentesSDS",
"certidaoAntecedentesTJPE",
"certidaoImprobidade",
"rgFrente",
"rgVerso",
"cpfFrente",
"cpfVerso",
"situacaoCadastralCPF",
"tituloEleitorFrente",
"tituloEleitorVerso",
"comprovanteVotacao",
"carteiraProfissionalFrente",
"carteiraProfissionalVerso",
"comprovantePIS",
"certidaoRegistroCivil",
"certidaoNascimentoDependentes",
"cpfDependentes",
"reservistaDoc",
"comprovanteEscolaridade",
"comprovanteResidencia",
"comprovanteContaBradesco",
"declaracaoAcumulacaoCargo",
"declaracaoDependentesIR",
"declaracaoIdoneidade",
"termoNepotismo",
"termoOpcaoRemuneracao",
];
for (const campo of campos) {
const storageId = (funcionario as any)[campo];
if (storageId) {
urls[campo] = await ctx.storage.getUrl(storageId);
} else {
urls[campo] = null;
}
}
return urls as any;
},
});
// Query para obter metadados de um documento
export const getDocumentoMetadata = query({
args: { storageId: v.id("_storage") },
returns: v.union(
v.object({
_id: v.id("_storage"),
_creationTime: v.number(),
contentType: v.optional(v.string()),
sha256: v.string(),
size: v.number(),
}),
v.null()
),
handler: async (ctx, args) => {
return await ctx.db.system.get(args.storageId);
},
});

View File

@@ -2,58 +2,43 @@ import { v } from "convex/values";
import { query, mutation } from "./_generated/server"; import { query, mutation } from "./_generated/server";
import { simboloTipo } from "./schema"; import { simboloTipo } from "./schema";
// Validadores para campos opcionais
const sexoValidator = v.optional(v.union(v.literal("masculino"), v.literal("feminino"), v.literal("outro")));
const estadoCivilValidator = v.optional(v.union(v.literal("solteiro"), v.literal("casado"), v.literal("divorciado"), v.literal("viuvo"), v.literal("uniao_estavel")));
const grauInstrucaoValidator = v.optional(v.union(v.literal("fundamental"), v.literal("medio"), v.literal("superior"), v.literal("pos_graduacao"), v.literal("mestrado"), v.literal("doutorado")));
const grupoSanguineoValidator = v.optional(v.union(v.literal("A"), v.literal("B"), v.literal("AB"), v.literal("O")));
const fatorRHValidator = v.optional(v.union(v.literal("positivo"), v.literal("negativo")));
const aposentadoValidator = v.optional(v.union(v.literal("nao"), v.literal("funape_ipsep"), v.literal("inss")));
export const getAll = query({ export const getAll = query({
args: {}, args: {},
returns: v.array(
v.object({
_id: v.id("funcionarios"),
_creationTime: v.number(),
nome: v.string(),
nascimento: v.string(),
rg: v.string(),
cpf: v.string(),
endereco: v.string(),
cep: v.string(),
cidade: v.string(),
uf: v.string(),
telefone: v.string(),
email: v.string(),
matricula: v.string(),
admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()),
simboloId: v.id("simbolos"),
simboloTipo: simboloTipo,
})
),
handler: async (ctx) => { handler: async (ctx) => {
return await ctx.db.query("funcionarios").collect(); const funcionarios = await ctx.db.query("funcionarios").collect();
// Retornar apenas os campos necessários para listagem
return funcionarios.map((f: any) => ({
_id: f._id,
_creationTime: f._creationTime,
nome: f.nome,
matricula: f.matricula,
cpf: f.cpf,
rg: f.rg,
nascimento: f.nascimento,
email: f.email,
telefone: f.telefone,
endereco: f.endereco,
cep: f.cep,
cidade: f.cidade,
uf: f.uf,
simboloId: f.simboloId,
simboloTipo: f.simboloTipo,
admissaoData: f.admissaoData,
desligamentoData: f.desligamentoData,
}));
}, },
}); });
export const getById = query({ export const getById = query({
args: { id: v.id("funcionarios") }, args: { id: v.id("funcionarios") },
returns: v.union(
v.object({
_id: v.id("funcionarios"),
_creationTime: v.number(),
nome: v.string(),
nascimento: v.string(),
rg: v.string(),
cpf: v.string(),
endereco: v.string(),
cep: v.string(),
cidade: v.string(),
uf: v.string(),
telefone: v.string(),
email: v.string(),
matricula: v.string(),
admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()),
simboloId: v.id("simbolos"),
simboloTipo: simboloTipo,
}),
v.null()
),
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db.get(args.id); return await ctx.db.get(args.id);
}, },
@@ -61,6 +46,7 @@ export const getById = query({
export const create = mutation({ export const create = mutation({
args: { args: {
// Campos obrigatórios
nome: v.string(), nome: v.string(),
matricula: v.string(), matricula: v.string(),
simboloId: v.id("simbolos"), simboloId: v.id("simbolos"),
@@ -76,6 +62,81 @@ export const create = mutation({
admissaoData: v.optional(v.string()), admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()), desligamentoData: v.optional(v.string()),
simboloTipo: simboloTipo, simboloTipo: simboloTipo,
// Dados Pessoais Adicionais
nomePai: v.optional(v.string()),
nomeMae: v.optional(v.string()),
naturalidade: v.optional(v.string()),
naturalidadeUF: v.optional(v.string()),
sexo: sexoValidator,
estadoCivil: estadoCivilValidator,
nacionalidade: v.optional(v.string()),
// Documentos Pessoais
rgOrgaoExpedidor: v.optional(v.string()),
rgDataEmissao: v.optional(v.string()),
carteiraProfissionalNumero: v.optional(v.string()),
carteiraProfissionalSerie: v.optional(v.string()),
carteiraProfissionalDataEmissao: v.optional(v.string()),
reservistaNumero: v.optional(v.string()),
reservistaSerie: v.optional(v.string()),
tituloEleitorNumero: v.optional(v.string()),
tituloEleitorZona: v.optional(v.string()),
tituloEleitorSecao: v.optional(v.string()),
pisNumero: v.optional(v.string()),
// Formação e Saúde
grauInstrucao: grauInstrucaoValidator,
formacao: v.optional(v.string()),
formacaoRegistro: v.optional(v.string()),
grupoSanguineo: grupoSanguineoValidator,
fatorRH: fatorRHValidator,
// Cargo e Vínculo
descricaoCargo: v.optional(v.string()),
nomeacaoPortaria: v.optional(v.string()),
nomeacaoData: v.optional(v.string()),
nomeacaoDOE: v.optional(v.string()),
pertenceOrgaoPublico: v.optional(v.boolean()),
orgaoOrigem: v.optional(v.string()),
aposentado: aposentadoValidator,
// Dados Bancários
contaBradescoNumero: v.optional(v.string()),
contaBradescoDV: v.optional(v.string()),
contaBradescoAgencia: v.optional(v.string()),
// Documentos Anexos (Storage IDs)
certidaoAntecedentesPF: v.optional(v.id("_storage")),
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
certidaoAntecedentesSDS: v.optional(v.id("_storage")),
certidaoAntecedentesTJPE: v.optional(v.id("_storage")),
certidaoImprobidade: v.optional(v.id("_storage")),
rgFrente: v.optional(v.id("_storage")),
rgVerso: v.optional(v.id("_storage")),
cpfFrente: v.optional(v.id("_storage")),
cpfVerso: v.optional(v.id("_storage")),
situacaoCadastralCPF: v.optional(v.id("_storage")),
tituloEleitorFrente: v.optional(v.id("_storage")),
tituloEleitorVerso: v.optional(v.id("_storage")),
comprovanteVotacao: v.optional(v.id("_storage")),
carteiraProfissionalFrente: v.optional(v.id("_storage")),
carteiraProfissionalVerso: v.optional(v.id("_storage")),
comprovantePIS: v.optional(v.id("_storage")),
certidaoRegistroCivil: v.optional(v.id("_storage")),
certidaoNascimentoDependentes: v.optional(v.id("_storage")),
cpfDependentes: v.optional(v.id("_storage")),
reservistaDoc: v.optional(v.id("_storage")),
comprovanteEscolaridade: v.optional(v.id("_storage")),
comprovanteResidencia: v.optional(v.id("_storage")),
comprovanteContaBradesco: v.optional(v.id("_storage")),
// Declarações (Storage IDs)
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
declaracaoDependentesIR: v.optional(v.id("_storage")),
declaracaoIdoneidade: v.optional(v.id("_storage")),
termoNepotismo: v.optional(v.id("_storage")),
termoOpcaoRemuneracao: v.optional(v.id("_storage")),
}, },
returns: v.id("funcionarios"), returns: v.id("funcionarios"),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -97,23 +158,7 @@ export const create = mutation({
throw new Error("Matrícula já cadastrada"); throw new Error("Matrícula já cadastrada");
} }
const novoFuncionarioId = await ctx.db.insert("funcionarios", { const novoFuncionarioId = await ctx.db.insert("funcionarios", args as any);
nome: args.nome,
nascimento: args.nascimento,
rg: args.rg,
cpf: args.cpf,
endereco: args.endereco,
cep: args.cep,
cidade: args.cidade,
uf: args.uf,
telefone: args.telefone,
email: args.email,
matricula: args.matricula,
admissaoData: args.admissaoData,
desligamentoData: args.desligamentoData,
simboloId: args.simboloId,
simboloTipo: args.simboloTipo,
});
return novoFuncionarioId; return novoFuncionarioId;
}, },
}); });
@@ -121,6 +166,7 @@ export const create = mutation({
export const update = mutation({ export const update = mutation({
args: { args: {
id: v.id("funcionarios"), id: v.id("funcionarios"),
// Campos obrigatórios
nome: v.string(), nome: v.string(),
matricula: v.string(), matricula: v.string(),
simboloId: v.id("simbolos"), simboloId: v.id("simbolos"),
@@ -136,6 +182,81 @@ export const update = mutation({
admissaoData: v.optional(v.string()), admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()), desligamentoData: v.optional(v.string()),
simboloTipo: simboloTipo, simboloTipo: simboloTipo,
// Dados Pessoais Adicionais
nomePai: v.optional(v.string()),
nomeMae: v.optional(v.string()),
naturalidade: v.optional(v.string()),
naturalidadeUF: v.optional(v.string()),
sexo: sexoValidator,
estadoCivil: estadoCivilValidator,
nacionalidade: v.optional(v.string()),
// Documentos Pessoais
rgOrgaoExpedidor: v.optional(v.string()),
rgDataEmissao: v.optional(v.string()),
carteiraProfissionalNumero: v.optional(v.string()),
carteiraProfissionalSerie: v.optional(v.string()),
carteiraProfissionalDataEmissao: v.optional(v.string()),
reservistaNumero: v.optional(v.string()),
reservistaSerie: v.optional(v.string()),
tituloEleitorNumero: v.optional(v.string()),
tituloEleitorZona: v.optional(v.string()),
tituloEleitorSecao: v.optional(v.string()),
pisNumero: v.optional(v.string()),
// Formação e Saúde
grauInstrucao: grauInstrucaoValidator,
formacao: v.optional(v.string()),
formacaoRegistro: v.optional(v.string()),
grupoSanguineo: grupoSanguineoValidator,
fatorRH: fatorRHValidator,
// Cargo e Vínculo
descricaoCargo: v.optional(v.string()),
nomeacaoPortaria: v.optional(v.string()),
nomeacaoData: v.optional(v.string()),
nomeacaoDOE: v.optional(v.string()),
pertenceOrgaoPublico: v.optional(v.boolean()),
orgaoOrigem: v.optional(v.string()),
aposentado: aposentadoValidator,
// Dados Bancários
contaBradescoNumero: v.optional(v.string()),
contaBradescoDV: v.optional(v.string()),
contaBradescoAgencia: v.optional(v.string()),
// Documentos Anexos (Storage IDs)
certidaoAntecedentesPF: v.optional(v.id("_storage")),
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
certidaoAntecedentesSDS: v.optional(v.id("_storage")),
certidaoAntecedentesTJPE: v.optional(v.id("_storage")),
certidaoImprobidade: v.optional(v.id("_storage")),
rgFrente: v.optional(v.id("_storage")),
rgVerso: v.optional(v.id("_storage")),
cpfFrente: v.optional(v.id("_storage")),
cpfVerso: v.optional(v.id("_storage")),
situacaoCadastralCPF: v.optional(v.id("_storage")),
tituloEleitorFrente: v.optional(v.id("_storage")),
tituloEleitorVerso: v.optional(v.id("_storage")),
comprovanteVotacao: v.optional(v.id("_storage")),
carteiraProfissionalFrente: v.optional(v.id("_storage")),
carteiraProfissionalVerso: v.optional(v.id("_storage")),
comprovantePIS: v.optional(v.id("_storage")),
certidaoRegistroCivil: v.optional(v.id("_storage")),
certidaoNascimentoDependentes: v.optional(v.id("_storage")),
cpfDependentes: v.optional(v.id("_storage")),
reservistaDoc: v.optional(v.id("_storage")),
comprovanteEscolaridade: v.optional(v.id("_storage")),
comprovanteResidencia: v.optional(v.id("_storage")),
comprovanteContaBradesco: v.optional(v.id("_storage")),
// Declarações (Storage IDs)
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
declaracaoDependentesIR: v.optional(v.id("_storage")),
declaracaoIdoneidade: v.optional(v.id("_storage")),
termoNepotismo: v.optional(v.id("_storage")),
termoOpcaoRemuneracao: v.optional(v.id("_storage")),
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -157,23 +278,8 @@ export const update = mutation({
throw new Error("Matrícula já cadastrada"); throw new Error("Matrícula já cadastrada");
} }
await ctx.db.patch(args.id, { const { id, ...updateData } = args;
nome: args.nome, await ctx.db.patch(id, updateData as any);
nascimento: args.nascimento,
rg: args.rg,
cpf: args.cpf,
endereco: args.endereco,
cep: args.cep,
cidade: args.cidade,
uf: args.uf,
telefone: args.telefone,
email: args.email,
matricula: args.matricula,
admissaoData: args.admissaoData,
desligamentoData: args.desligamentoData,
simboloId: args.simboloId,
simboloTipo: args.simboloTipo,
});
return null; return null;
}, },
}); });
@@ -182,7 +288,31 @@ export const remove = mutation({
args: { id: v.id("funcionarios") }, args: { id: v.id("funcionarios") },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// TODO: Talvez queiramos também remover os arquivos do storage
await ctx.db.delete(args.id); await ctx.db.delete(args.id);
return null; return null;
}, },
}); });
// Query para obter ficha completa para impressão
export const getFichaCompleta = query({
args: { id: v.id("funcionarios") },
handler: async (ctx, args) => {
const funcionario = await ctx.db.get(args.id);
if (!funcionario) {
return null;
}
// Buscar informações do símbolo
const simbolo = await ctx.db.get(funcionario.simboloId);
return {
...funcionario,
simbolo: simbolo ? {
nome: simbolo.nome,
descricao: simbolo.descricao,
valor: simbolo.valor,
} : null,
};
},
});

View File

@@ -16,6 +16,7 @@ export default defineSchema({
completed: v.boolean(), completed: v.boolean(),
}), }),
funcionarios: defineTable({ funcionarios: defineTable({
// Campos obrigatórios existentes
nome: v.string(), nome: v.string(),
nascimento: v.string(), nascimento: v.string(),
rg: v.string(), rg: v.string(),
@@ -31,6 +32,110 @@ export default defineSchema({
desligamentoData: v.optional(v.string()), desligamentoData: v.optional(v.string()),
simboloId: v.id("simbolos"), simboloId: v.id("simbolos"),
simboloTipo: simboloTipo, simboloTipo: simboloTipo,
// Dados Pessoais Adicionais (opcionais)
nomePai: v.optional(v.string()),
nomeMae: v.optional(v.string()),
naturalidade: v.optional(v.string()),
naturalidadeUF: v.optional(v.string()),
sexo: v.optional(v.union(
v.literal("masculino"),
v.literal("feminino"),
v.literal("outro")
)),
estadoCivil: v.optional(v.union(
v.literal("solteiro"),
v.literal("casado"),
v.literal("divorciado"),
v.literal("viuvo"),
v.literal("uniao_estavel")
)),
nacionalidade: v.optional(v.string()),
// Documentos Pessoais
rgOrgaoExpedidor: v.optional(v.string()),
rgDataEmissao: v.optional(v.string()),
carteiraProfissionalNumero: v.optional(v.string()),
carteiraProfissionalSerie: v.optional(v.string()),
carteiraProfissionalDataEmissao: v.optional(v.string()),
reservistaNumero: v.optional(v.string()),
reservistaSerie: v.optional(v.string()),
tituloEleitorNumero: v.optional(v.string()),
tituloEleitorZona: v.optional(v.string()),
tituloEleitorSecao: v.optional(v.string()),
pisNumero: v.optional(v.string()),
// Formação e Saúde
grauInstrucao: v.optional(v.union(
v.literal("fundamental"),
v.literal("medio"),
v.literal("superior"),
v.literal("pos_graduacao"),
v.literal("mestrado"),
v.literal("doutorado")
)),
formacao: v.optional(v.string()),
formacaoRegistro: v.optional(v.string()),
grupoSanguineo: v.optional(v.union(
v.literal("A"),
v.literal("B"),
v.literal("AB"),
v.literal("O")
)),
fatorRH: v.optional(v.union(
v.literal("positivo"),
v.literal("negativo")
)),
// Cargo e Vínculo
descricaoCargo: v.optional(v.string()),
nomeacaoPortaria: v.optional(v.string()),
nomeacaoData: v.optional(v.string()),
nomeacaoDOE: v.optional(v.string()),
pertenceOrgaoPublico: v.optional(v.boolean()),
orgaoOrigem: v.optional(v.string()),
aposentado: v.optional(v.union(
v.literal("nao"),
v.literal("funape_ipsep"),
v.literal("inss")
)),
// Dados Bancários
contaBradescoNumero: v.optional(v.string()),
contaBradescoDV: v.optional(v.string()),
contaBradescoAgencia: v.optional(v.string()),
// Documentos Anexos (Storage IDs)
certidaoAntecedentesPF: v.optional(v.id("_storage")),
certidaoAntecedentesJFPE: v.optional(v.id("_storage")),
certidaoAntecedentesSDS: v.optional(v.id("_storage")),
certidaoAntecedentesTJPE: v.optional(v.id("_storage")),
certidaoImprobidade: v.optional(v.id("_storage")),
rgFrente: v.optional(v.id("_storage")),
rgVerso: v.optional(v.id("_storage")),
cpfFrente: v.optional(v.id("_storage")),
cpfVerso: v.optional(v.id("_storage")),
situacaoCadastralCPF: v.optional(v.id("_storage")),
tituloEleitorFrente: v.optional(v.id("_storage")),
tituloEleitorVerso: v.optional(v.id("_storage")),
comprovanteVotacao: v.optional(v.id("_storage")),
carteiraProfissionalFrente: v.optional(v.id("_storage")),
carteiraProfissionalVerso: v.optional(v.id("_storage")),
comprovantePIS: v.optional(v.id("_storage")),
certidaoRegistroCivil: v.optional(v.id("_storage")),
certidaoNascimentoDependentes: v.optional(v.id("_storage")),
cpfDependentes: v.optional(v.id("_storage")),
reservistaDoc: v.optional(v.id("_storage")),
comprovanteEscolaridade: v.optional(v.id("_storage")),
comprovanteResidencia: v.optional(v.id("_storage")),
comprovanteContaBradesco: v.optional(v.id("_storage")),
// Declarações (Storage IDs)
declaracaoAcumulacaoCargo: v.optional(v.id("_storage")),
declaracaoDependentesIR: v.optional(v.id("_storage")),
declaracaoIdoneidade: v.optional(v.id("_storage")),
termoNepotismo: v.optional(v.id("_storage")),
termoOpcaoRemuneracao: v.optional(v.id("_storage")),
}) })
.index("by_matricula", ["matricula"]) .index("by_matricula", ["matricula"])
.index("by_nome", ["nome"]) .index("by_nome", ["nome"])