diff --git a/.cursor/plans/sistema-de-documentos-e-impress-o-de0a1ea6.plan.md b/.cursor/plans/sistema-de-documentos-e-impress-o-de0a1ea6.plan.md new file mode 100644 index 0000000..f5362a5 --- /dev/null +++ b/.cursor/plans/sistema-de-documentos-e-impress-o-de0a1ea6.plan.md @@ -0,0 +1,352 @@ + +# 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` +- `onRemove: () => Promise` + +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 `` 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 \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index a5cfc5f..c457e7f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,6 +34,8 @@ "better-auth": "1.3.27", "convex": "^1.28.0", "convex-svelte": "^0.0.11", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", "zod": "^4.0.17" } } diff --git a/apps/web/src/lib/components/FileUpload.svelte b/apps/web/src/lib/components/FileUpload.svelte new file mode 100644 index 0000000..4782e2a --- /dev/null +++ b/apps/web/src/lib/components/FileUpload.svelte @@ -0,0 +1,273 @@ + + +
+ + + + + {#if value || fileName} +
+ +
+ {#if previewUrl} + Preview + {:else if fileType === "application/pdf" || fileName.endsWith(".pdf")} +
+ + + +
+ {:else} +
+ + + +
+ {/if} +
+ + +
+

{fileName || "Arquivo anexado"}

+

+ {#if uploading} + Carregando... + {:else} + Enviado com sucesso + {/if} +

+
+ + +
+ {#if fileUrl} + + {/if} + + +
+
+ {:else} + + {/if} + + {#if error} + + {/if} +
+ diff --git a/apps/web/src/lib/components/ModelosDeclaracoes.svelte b/apps/web/src/lib/components/ModelosDeclaracoes.svelte new file mode 100644 index 0000000..0760935 --- /dev/null +++ b/apps/web/src/lib/components/ModelosDeclaracoes.svelte @@ -0,0 +1,162 @@ + + +
+
+

+ + + + Modelos de Declarações +

+ +
+ + + +
+

Baixe os modelos, preencha, assine e faça upload no sistema

+

Estes documentos são necessários para completar o cadastro do funcionário

+
+
+ +
+ {#each modelosDeclaracoes as modelo} +
+
+
+ +
+ + + +
+ + +
+

{modelo.nome}

+

{modelo.descricao}

+ + +
+ + + {#if showPreencherButton && modelo.podePreencherAutomaticamente && funcionario} + + {/if} +
+
+
+
+
+ {/each} +
+ +
+

💡 Dica: Após preencher e assinar os documentos, faça upload na seção "Documentação Anexa"

+
+
+
+ diff --git a/apps/web/src/lib/components/PrintModal.svelte b/apps/web/src/lib/components/PrintModal.svelte new file mode 100644 index 0000000..d411b57 --- /dev/null +++ b/apps/web/src/lib/components/PrintModal.svelte @@ -0,0 +1,463 @@ + + + + + + + + diff --git a/apps/web/src/lib/utils/constants.ts b/apps/web/src/lib/utils/constants.ts new file mode 100644 index 0000000..40dd160 --- /dev/null +++ b/apps/web/src/lib/utils/constants.ts @@ -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" +]; + diff --git a/apps/web/src/lib/utils/declaracoesGenerator.ts b/apps/web/src/lib/utils/declaracoesGenerator.ts new file mode 100644 index 0000000..d4fd33a --- /dev/null +++ b/apps/web/src/lib/utils/declaracoesGenerator.ts @@ -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 { + try { + // Criar uma promise para carregar a imagem + const logoImg = await new Promise((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 { + 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 { + 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 { + 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 { + 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 { + 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); +} + diff --git a/apps/web/src/lib/utils/documentos.ts b/apps/web/src/lib/utils/documentos.ts new file mode 100644 index 0000000..671b00c --- /dev/null +++ b/apps/web/src/lib/utils/documentos.ts @@ -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); +} + diff --git a/apps/web/src/lib/utils/masks.ts b/apps/web/src/lib/utils/masks.ts new file mode 100644 index 0000000..2aab943 --- /dev/null +++ b/apps/web/src/lib/utils/masks.ts @@ -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 = { + 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(); +}; + diff --git a/apps/web/src/lib/utils/modelosDeclaracoes.ts b/apps/web/src/lib/utils/modelosDeclaracoes.ts new file mode 100644 index 0000000..84f9f8a --- /dev/null +++ b/apps/web/src/lib/utils/modelosDeclaracoes.ts @@ -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); +} + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte index c90730f..c6637d2 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte @@ -3,6 +3,7 @@ import { api } from "@sgse-app/backend/convex/_generated/api"; import { goto } from "$app/navigation"; import type { SimboloTipo } from "@sgse-app/backend/convex/schema"; + import PrintModal from "$lib/components/PrintModal.svelte"; const client = useConvexClient(); @@ -12,6 +13,7 @@ let deletingId: string | null = null; let toDelete: { id: string; nome: string } | null = null; let openMenuId: string | null = null; + let funcionarioParaImprimir: any = null; let filtroNome = ""; let filtroCPF = ""; @@ -48,6 +50,18 @@ toDelete = null; (document.getElementById("delete_modal_func") as HTMLDialogElement)?.close(); } + + async function openPrintModal(funcionarioId: string) { + try { + const data = await client.query(api.funcionarios.getFichaCompleta, { + id: funcionarioId as any + }); + funcionarioParaImprimir = data; + } catch (err) { + console.error("Erro ao carregar funcionário:", err); + alert("Erro ao carregar dados para impressão"); + } + } async function confirmDelete() { if (!toDelete) return; try { @@ -213,8 +227,11 @@ @@ -261,5 +278,12 @@ - + + {#if funcionarioParaImprimir} + funcionarioParaImprimir = null} + /> + {/if} + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/+page.svelte index e69de29..b842688 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/+page.svelte @@ -0,0 +1,434 @@ + + +{#if loading} +
+ +
+{:else if funcionario} +
+ + + + +
+
+
+
+ + + +
+
+

{funcionario.nome}

+

Matrícula: {funcionario.matricula}

+
+
+ +
+ + + +
+
+
+ + +
+ +
+ +
+
+

Informações Pessoais

+
+
CPF: {maskCPF(funcionario.cpf)}
+
RG: {funcionario.rg}
+ {#if funcionario.rgOrgaoExpedidor} +
Órgão Expedidor: {funcionario.rgOrgaoExpedidor}
+ {/if} + {#if funcionario.rgDataEmissao} +
Data Emissão RG: {funcionario.rgDataEmissao}
+ {/if} +
Data Nascimento: {funcionario.nascimento}
+ {#if funcionario.sexo} +
Sexo: {getLabelFromOptions(funcionario.sexo, SEXO_OPTIONS)}
+ {/if} + {#if funcionario.estadoCivil} +
Estado Civil: {getLabelFromOptions(funcionario.estadoCivil, ESTADO_CIVIL_OPTIONS)}
+ {/if} + {#if funcionario.nacionalidade} +
Nacionalidade: {funcionario.nacionalidade}
+ {/if} +
+
+
+ + + {#if funcionario.nomePai || funcionario.nomeMae} +
+
+

Filiação

+
+ {#if funcionario.nomePai} +
Pai: {funcionario.nomePai}
+ {/if} + {#if funcionario.nomeMae} +
Mãe: {funcionario.nomeMae}
+ {/if} +
+
+
+ {/if} + + + {#if funcionario.naturalidade || funcionario.naturalidadeUF} +
+
+

Naturalidade

+
+ {#if funcionario.naturalidade} +
Cidade: {funcionario.naturalidade}
+ {/if} + {#if funcionario.naturalidadeUF} +
UF: {funcionario.naturalidadeUF}
+ {/if} +
+
+
+ {/if} +
+ + +
+ +
+
+

Documentos Pessoais

+
+ {#if funcionario.carteiraProfissionalNumero} +
Cart. Profissional: {funcionario.carteiraProfissionalNumero} + {#if funcionario.carteiraProfissionalSerie} + - Série: {funcionario.carteiraProfissionalSerie} + {/if} +
+ {/if} + {#if funcionario.reservistaNumero} +
Reservista: {funcionario.reservistaNumero} + {#if funcionario.reservistaSerie} + - Série: {funcionario.reservistaSerie} + {/if} +
+ {/if} + {#if funcionario.tituloEleitorNumero} +
Título Eleitor: {funcionario.tituloEleitorNumero}
+ {#if funcionario.tituloEleitorZona || funcionario.tituloEleitorSecao} +
+ {#if funcionario.tituloEleitorZona}Zona: {funcionario.tituloEleitorZona}{/if} + {#if funcionario.tituloEleitorSecao} - Seção: {funcionario.tituloEleitorSecao}{/if} +
+ {/if} + {/if} + {#if funcionario.pisNumero} +
PIS/PASEP: {funcionario.pisNumero}
+ {/if} +
+
+
+ + + {#if funcionario.grauInstrucao || funcionario.formacao} +
+
+

Formação

+
+ {#if funcionario.grauInstrucao} +
Grau Instrução: {getLabelFromOptions(funcionario.grauInstrucao, GRAU_INSTRUCAO_OPTIONS)}
+ {/if} + {#if funcionario.formacao} +
Formação: {funcionario.formacao}
+ {/if} + {#if funcionario.formacaoRegistro} +
Registro Nº: {funcionario.formacaoRegistro}
+ {/if} +
+
+
+ {/if} + + + {#if funcionario.grupoSanguineo || funcionario.fatorRH} +
+
+

Saúde

+
+ {#if funcionario.grupoSanguineo} +
Grupo Sanguíneo: {funcionario.grupoSanguineo}
+ {/if} + {#if funcionario.fatorRH} +
Fator RH: {getLabelFromOptions(funcionario.fatorRH, FATOR_RH_OPTIONS)}
+ {/if} +
+
+
+ {/if} + + +
+
+

Contato

+
+
E-mail: {funcionario.email}
+
Telefone: {maskPhone(funcionario.telefone)}
+
+
+
+
+ + +
+ +
+
+

Cargo e Vínculo

+
+
Tipo: {funcionario.simboloTipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}
+ {#if simbolo} +
Símbolo: {simbolo.nome}
+
{simbolo.descricao}
+ {/if} + {#if funcionario.descricaoCargo} +
Descrição: {funcionario.descricaoCargo}
+ {/if} + {#if funcionario.admissaoData} +
Data Admissão: {funcionario.admissaoData}
+ {/if} + {#if funcionario.nomeacaoPortaria} +
Portaria: {funcionario.nomeacaoPortaria}
+ {/if} + {#if funcionario.nomeacaoData} +
Data Nomeação: {funcionario.nomeacaoData}
+ {/if} + {#if funcionario.nomeacaoDOE} +
DOE: {funcionario.nomeacaoDOE}
+ {/if} + {#if funcionario.pertenceOrgaoPublico} +
Pertence Órgão Público: Sim
+ {#if funcionario.orgaoOrigem} +
Órgão Origem: {funcionario.orgaoOrigem}
+ {/if} + {/if} + {#if funcionario.aposentado && funcionario.aposentado !== 'nao'} +
Aposentado: {getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)}
+ {/if} +
+
+
+ + +
+
+

Endereço

+
+
{funcionario.endereco}
+
{funcionario.cidade} - {funcionario.uf}
+
CEP: {maskCEP(funcionario.cep)}
+
+
+
+ + + {#if funcionario.contaBradescoNumero} +
+
+

Dados Bancários - Bradesco

+
+
Conta: {funcionario.contaBradescoNumero} + {#if funcionario.contaBradescoDV}-{funcionario.contaBradescoDV}{/if} +
+ {#if funcionario.contaBradescoAgencia} +
Agência: {funcionario.contaBradescoAgencia}
+ {/if} +
+
+
+ {/if} +
+
+ + +
+
+

+ + + + Documentos Anexados +

+ +
+ {#each documentos as doc} + {@const temDocumento = documentosUrls[doc.campo]} +
+
+
+
+ {#if temDocumento} + + + + {:else} + + + + {/if} +
+
+

{doc.nome}

+

+ {temDocumento ? 'Enviado' : 'Pendente'} +

+ {#if temDocumento} + + {/if} +
+
+
+
+ {/each} +
+ +
+ +
+
+
+
+ + + {#if showPrintModal} + showPrintModal = false} + /> + {/if} +{/if} diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/documentos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/documentos/+page.svelte new file mode 100644 index 0000000..5e02825 --- /dev/null +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/documentos/+page.svelte @@ -0,0 +1,277 @@ + + +{#if loading} +
+ +
+{:else if funcionario} +
+ + + + +
+
+
+
+ + + +
+
+

Gerenciar Documentos

+

{funcionario.nome} - Matrícula: {funcionario.matricula}

+
+
+ + +
+
+ + +
+
+
+
+ + + +
+
Total de Documentos
+
{contarDocumentos().total}
+
+
+ +
+
+
+ + + +
+
Documentos Enviados
+
{contarDocumentos().enviados}
+
+
+ +
+
+
+ + + +
+
Documentos Pendentes
+
{contarDocumentos().pendentes}
+
+
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+ + + {#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} +
+
+

+ {categoria} +
{docsCategoria.length}
+

+ +
+ {#each docsCategoria as doc} + handleDocumentoUpload(doc.campo, file)} + onRemove={() => handleDocumentoRemove(doc.campo)} + /> + {/each} +
+
+
+ {/if} + {/each} + + {#if documentosFiltrados().length === 0} +
+ + + + Nenhum documento encontrado com o filtro selecionado. +
+ {/if} +
+{/if} + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte index f76ca85..e07eb3c 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte @@ -1,240 +1,373 @@ -
- -
+{/if} \ No newline at end of file diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte index c31976a..0c72b74 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte @@ -1,10 +1,19 @@ -
+
- - diff --git a/apps/web/static/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf b/apps/web/static/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf new file mode 100644 index 0000000..af7a802 Binary files /dev/null and b/apps/web/static/modelos/declaracoes/Declaração de Acumulação de Cargo, Emprego, Função Pública ou Proventos.pdf differ diff --git a/apps/web/static/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf b/apps/web/static/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf new file mode 100644 index 0000000..84060c6 Binary files /dev/null and b/apps/web/static/modelos/declaracoes/Declaração de Dependentes para Fins de Imposto de Renda.pdf differ diff --git a/apps/web/static/modelos/declaracoes/Declaração de Idoneidade.pdf b/apps/web/static/modelos/declaracoes/Declaração de Idoneidade.pdf new file mode 100644 index 0000000..2f93a57 Binary files /dev/null and b/apps/web/static/modelos/declaracoes/Declaração de Idoneidade.pdf differ diff --git a/apps/web/static/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf b/apps/web/static/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf new file mode 100644 index 0000000..724900b Binary files /dev/null and b/apps/web/static/modelos/declaracoes/Termo de Declaração de Nepotismo.pdf differ diff --git a/apps/web/static/modelos/declaracoes/Termo de Opção - Remuneração.pdf b/apps/web/static/modelos/declaracoes/Termo de Opção - Remuneração.pdf new file mode 100644 index 0000000..58deff1 Binary files /dev/null and b/apps/web/static/modelos/declaracoes/Termo de Opção - Remuneração.pdf differ diff --git a/fix-editar.js b/fix-editar.js new file mode 100644 index 0000000..0b2d98b --- /dev/null +++ b/fix-editar.js @@ -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 = ` + +{#if loadingData} +
+ +
+{:else} +
+ + + + +
+
+
+ + + +
+
+

Editar Funcionário

+

Atualize as informações do funcionário

+
+
+
+ + + {#if notice} +
+ + {#if notice.kind === "success"} + + {:else} + + {/if} + + {notice.text} +
+ {/if} + + +
{ 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
\n
\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}`); + diff --git a/package-lock.json b/package-lock.json index f3d4396..35b761c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,8 @@ "better-auth": "1.3.27", "convex": "^1.28.0", "convex-svelte": "^0.0.11", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", "zod": "^4.0.17" }, "devDependencies": { @@ -46,6 +48,15 @@ "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": { "version": "1.3.27", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.3.27.tgz", @@ -710,7 +721,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -721,7 +731,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -732,7 +741,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -742,14 +750,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1310,14 +1316,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1778,7 +1783,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1791,11 +1795,30 @@ "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": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1808,7 +1831,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1870,12 +1892,21 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "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": { "version": "2.8.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", @@ -2000,6 +2031,26 @@ ], "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2020,7 +2071,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2562,6 +2612,28 @@ "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": { "version": "5.3.10", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz", @@ -2623,6 +2695,16 @@ "dev": true, "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": { "version": "1.5.240", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", @@ -2700,19 +2782,28 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", - "dev": true, "license": "MIT", "dependencies": { "@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": { "version": "6.5.0", "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": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2767,6 +2864,26 @@ "dev": true, "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", @@ -2783,7 +2900,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -2808,6 +2924,32 @@ "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": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -3092,7 +3234,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/lucide-svelte": { @@ -3108,7 +3249,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -3192,6 +3332,19 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3281,6 +3434,16 @@ "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3301,6 +3464,13 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "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": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.32.0.tgz", @@ -3310,6 +3480,16 @@ "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": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", @@ -3414,11 +3594,20 @@ "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": { "version": "5.42.2", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz", "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -3464,6 +3653,16 @@ "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": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", @@ -3485,6 +3684,16 @@ "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": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3654,7 +3863,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3708,6 +3917,16 @@ "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": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", @@ -3811,7 +4030,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, "license": "MIT" }, "node_modules/zod": { diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index ed1d16e..55bca73 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -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_auth from "../betterAuth/auth.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 healthCheck from "../healthCheck.js"; import type * as http from "../http.js"; @@ -52,6 +53,7 @@ declare const fullApi: ApiFromModules<{ "betterAuth/adapter": typeof betterAuth_adapter; "betterAuth/auth": typeof betterAuth_auth; dashboard: typeof dashboard; + documentos: typeof documentos; funcionarios: typeof funcionarios; healthCheck: typeof healthCheck; http: typeof http; diff --git a/packages/backend/convex/documentos.ts b/packages/backend/convex/documentos.ts new file mode 100644 index 0000000..4115e51 --- /dev/null +++ b/packages/backend/convex/documentos.ts @@ -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 = {}; + 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); + }, +}); + diff --git a/packages/backend/convex/funcionarios.ts b/packages/backend/convex/funcionarios.ts index 26c21c6..1d7c8e1 100644 --- a/packages/backend/convex/funcionarios.ts +++ b/packages/backend/convex/funcionarios.ts @@ -2,58 +2,43 @@ import { v } from "convex/values"; import { query, mutation } from "./_generated/server"; 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({ 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) => { - 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({ 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) => { return await ctx.db.get(args.id); }, @@ -61,6 +46,7 @@ export const getById = query({ export const create = mutation({ args: { + // Campos obrigatórios nome: v.string(), matricula: v.string(), simboloId: v.id("simbolos"), @@ -76,6 +62,81 @@ export const create = mutation({ admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()), 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"), handler: async (ctx, args) => { @@ -97,23 +158,7 @@ export const create = mutation({ throw new Error("Matrícula já cadastrada"); } - const novoFuncionarioId = await ctx.db.insert("funcionarios", { - 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, - }); + const novoFuncionarioId = await ctx.db.insert("funcionarios", args as any); return novoFuncionarioId; }, }); @@ -121,6 +166,7 @@ export const create = mutation({ export const update = mutation({ args: { id: v.id("funcionarios"), + // Campos obrigatórios nome: v.string(), matricula: v.string(), simboloId: v.id("simbolos"), @@ -136,6 +182,81 @@ export const update = mutation({ admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()), 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(), handler: async (ctx, args) => { @@ -157,23 +278,8 @@ export const update = mutation({ throw new Error("Matrícula já cadastrada"); } - await ctx.db.patch(args.id, { - 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, - }); + const { id, ...updateData } = args; + await ctx.db.patch(id, updateData as any); return null; }, }); @@ -182,7 +288,31 @@ export const remove = mutation({ args: { id: v.id("funcionarios") }, returns: v.null(), handler: async (ctx, args) => { + // TODO: Talvez queiramos também remover os arquivos do storage await ctx.db.delete(args.id); 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, + }; + }, +}); diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index cb17b15..8787946 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -16,6 +16,7 @@ export default defineSchema({ completed: v.boolean(), }), funcionarios: defineTable({ + // Campos obrigatórios existentes nome: v.string(), nascimento: v.string(), rg: v.string(), @@ -31,6 +32,110 @@ export default defineSchema({ desligamentoData: v.optional(v.string()), simboloId: v.id("simbolos"), 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_nome", ["nome"])