refactor: enhance employee registration form and backend validation

- Updated the employee registration form in Svelte to include additional fields for personal and banking information.
- Improved validation logic for required fields and document uploads, ensuring better user feedback.
- Refactored the backend mutation to streamline argument handling and added new fields for document storage.
- Enhanced the overall structure and readability of the code, maintaining existing functionality while improving maintainability.
This commit is contained in:
2025-11-12 16:29:18 -03:00
parent 3a783727dc
commit 94f4b23a39
2 changed files with 539 additions and 350 deletions

View File

@@ -1,202 +1,216 @@
<script lang="ts"> <script lang="ts">
import { useConvexClient } from "convex-svelte"; import { useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import { goto } from "$app/navigation"; import { goto } from '$app/navigation';
import type { SimboloTipo } from "@sgse-app/backend/convex/schema"; import type { SimboloTipo } from '@sgse-app/backend/convex/schema';
import FileUpload from "$lib/components/FileUpload.svelte"; import FileUpload from '$lib/components/FileUpload.svelte';
import { import {
maskCPF, maskCEP, maskPhone, maskDate, maskUF, onlyDigits, maskCPF,
validateCPF, validateDate maskCEP,
} from "$lib/utils/masks"; maskPhone,
import { maskDate,
SEXO_OPTIONS, ESTADO_CIVIL_OPTIONS, GRAU_INSTRUCAO_OPTIONS, onlyDigits,
GRUPO_SANGUINEO_OPTIONS, FATOR_RH_OPTIONS, APOSENTADO_OPTIONS, UFS_BRASIL validateCPF,
} from "$lib/utils/constants"; validateDate
import { documentos, categoriasDocumentos, getDocumentosByCategoria } from "$lib/utils/documentos"; } from '$lib/utils/masks';
import ModelosDeclaracoes from "$lib/components/ModelosDeclaracoes.svelte"; import {
SEXO_OPTIONS,
ESTADO_CIVIL_OPTIONS,
GRAU_INSTRUCAO_OPTIONS,
GRUPO_SANGUINEO_OPTIONS,
FATOR_RH_OPTIONS,
APOSENTADO_OPTIONS,
UFS_BRASIL
} from '$lib/utils/constants';
import { categoriasDocumentos, getDocumentosByCategoria } from '$lib/utils/documentos';
import ModelosDeclaracoes from '$lib/components/ModelosDeclaracoes.svelte';
import { resolve } from '$app/paths';
import type { CreateArgs } from '@sgse-app/backend/convex/funcionarios';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
const client = useConvexClient(); const client = useConvexClient();
let simbolos: Array<{ let simbolos: Array<{
_id: string; _id: string;
nome: string; nome: string;
tipo: SimboloTipo; tipo: SimboloTipo;
descricao: string; descricao: string;
}> = $state([]); }> = $state([]);
let tipo = $state<SimboloTipo>('cargo_comissionado'); let tipo = $state<SimboloTipo>('cargo_comissionado');
let loading = $state(false); let loading = $state(false);
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null); let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
// Cursos e Treinamentos // Campos obrigatórios
let cursos = $state<Array<{ let nome = $state('');
id: string; let matricula = $state('');
descricao: string; let cpf = $state('');
data: string; let rg = $state('');
certificadoId?: string; let nascimento = $state('');
}>>([]); let email = $state('');
let mostrarFormularioCurso = $state(false); let telefone = $state('');
let cursoAtual = $state({ descricao: "", data: "", arquivo: null as File | null }); let endereco = $state('');
let cep = $state('');
let cidade = $state('');
let uf = $state('');
let simboloId: Id<'simbolos'> | null = $derived.by(() => {
if (tipo) {
return null;
} else {
return null;
}
});
let admissaoData = $state('');
// Dependentes // Dados Pessoais Adicionais
let dependentes = $state<Array<{ id: string; parentesco: string; nome: string; cpf: string; nascimento: string; documentoId?: string; salarioFamilia?: boolean; impostoRenda?: boolean }>>([]); let nomePai = $state('');
let mostrarFormularioDependente = $state(false); let nomeMae = $state('');
let dependenteAtual = $state<{ parentesco: string; nome: string; cpf: string; nascimento: string; arquivo: File | null; documentoId?: string; salarioFamilia?: boolean; impostoRenda?: boolean }>({ let naturalidade = $state('');
parentesco: "", let naturalidadeUF = $state('');
nome: "", let sexo = $state<'masculino' | 'feminino' | 'outro' | null>(null);
cpf: "", let estadoCivil = $state<'solteiro' | 'casado' | 'divorciado' | 'viuvo' | 'uniao_estavel' | null>(
nascimento: "", null
arquivo: null, );
documentoId: undefined, let nacionalidade = $state('Brasileira');
salarioFamilia: false,
impostoRenda: false,
});
function adicionarCurso() { // Documentos Pessoais
if (!cursoAtual.descricao.trim() || !cursoAtual.data.trim()) { let rgOrgaoExpedidor = $state('');
alert("Preencha a descrição e a data do curso"); let rgDataEmissao = $state('');
return; let carteiraProfissionalNumero = $state('');
} let carteiraProfissionalSerie = $state('');
cursos.push({ let carteiraProfissionalDataEmissao = $state('');
id: crypto.randomUUID(), let reservistaNumero = $state('');
descricao: cursoAtual.descricao, let reservistaSerie = $state('');
data: cursoAtual.data, let tituloEleitorNumero = $state('');
certificadoId: undefined let tituloEleitorZona = $state('');
}); let tituloEleitorSecao = $state('');
cursoAtual = { descricao: "", data: "", arquivo: null }; let pisNumero = $state('');
}
function removerCurso(id: string) { // Formação e Saúde
cursos = cursos.filter(c => c.id !== id); let grauInstrucao = $state<
} 'fundamental' | 'medio' | 'superior' | 'pos_graduacao' | 'mestrado' | 'doutorado' | null
>(null);
let formacao = $state('');
let formacaoRegistro = $state('');
let grupoSanguineo = $state<'A' | 'B' | 'AB' | 'O' | null>(null);
let fatorRH = $state<'positivo' | 'negativo' | null>(null);
async function uploadCertificado(file: File): Promise<string> { // Cargo e Vínculo
const storageId = await client.mutation(api.documentos.generateUploadUrl, {}); let descricaoCargo = $state('');
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {}); let nomeacaoPortaria = $state('');
const response = await fetch(uploadUrl, { let nomeacaoData = $state('');
method: "POST", let nomeacaoDOE = $state('');
headers: { "Content-Type": file.type }, let pertenceOrgaoPublico = $state(false);
body: file, let orgaoOrigem = $state('');
}); let aposentado = $state<'nao' | 'funape_ipsep' | 'inss' | null>('nao');
const result = await response.json(); let regimeTrabalho = $state<
return result.storageId; 'clt' | 'estatutario_municipal' | 'estatutario_pe' | 'estatutario_federal' | null
} >(null);
async function uploadDocumentoDependente(file: File): Promise<string> { const REGIME_TRABALHO_OPTIONS = [
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {}); { value: 'clt', label: 'CLT' },
const response = await fetch(uploadUrl, { { value: 'estatutario_municipal', label: 'Estatutário Municipal' },
method: "POST", { value: 'estatutario_pe', label: 'Estatutário PE' },
headers: { "Content-Type": file.type }, { value: 'estatutario_federal', label: 'Estatutário Federal' }
body: file, ] as const;
});
const result = await response.json();
return result.storageId as string;
}
function adicionarDependente() { // Dados Bancários
if (dependentes.length >= 10) { let contaBradescoNumero = $state('');
alert("Limite de 10 dependentes atingido"); let contaBradescoDV = $state('');
return; let contaBradescoAgencia = $state('');
}
if (!dependenteAtual.parentesco || !dependenteAtual.nome.trim() || !dependenteAtual.cpf.trim() || !dependenteAtual.nascimento.trim()) {
alert("Preencha Parentesco, Nome, CPF e Data de Nascimento do dependente");
return;
}
if (!validateCPF(dependenteAtual.cpf)) {
alert("CPF do dependente inválido");
return;
}
if (!validateDate(dependenteAtual.nascimento)) {
alert("Data de nascimento do dependente inválida");
return;
}
dependentes.push({
id: crypto.randomUUID(),
parentesco: dependenteAtual.parentesco,
nome: dependenteAtual.nome.trim(),
cpf: onlyDigits(dependenteAtual.cpf),
nascimento: dependenteAtual.nascimento,
documentoId: dependenteAtual.documentoId,
salarioFamilia: !!dependenteAtual.salarioFamilia,
impostoRenda: !!dependenteAtual.impostoRenda,
});
dependenteAtual = { parentesco: "", nome: "", cpf: "", nascimento: "", arquivo: null, documentoId: undefined, salarioFamilia: false, impostoRenda: false };
}
function removerDependente(id: string) { // Documentos (Storage IDs)
dependentes = dependentes.filter((d) => d.id !== id); let documentosStorage: Record<string, string | undefined> = $state({});
}
async function loadSimbolos() { // Cursos e Treinamentos
const list = await client.query(api.simbolos.getAll, {} as any); let cursos = $state<
simbolos = list.map((s: any) => ({ Array<{
_id: s._id, id: string;
nome: s.nome, descricao: string;
tipo: s.tipo, data: string;
descricao: s.descricao certificadoId?: Id<'_storage'>;
})); }>
} >([]);
let mostrarFormularioCurso = $state(false);
let cursoAtual = $state({ descricao: '', data: '', arquivo: null as File | null });
async function fillFromCEP(cepValue: string) { // Dependentes
const cepDigits = onlyDigits(cepValue); let dependentes = $state<
if (cepDigits.length !== 8) return; Array<{
id: string;
try { parentesco: 'outro' | 'filho' | 'filha' | 'conjuge' | null;
const res = await fetch(`https://viacep.com.br/ws/${cepDigits}/json/`); nome: string;
const data = await res.json(); cpf: string;
if (!data || data.erro) return; nascimento: string;
documentoId?: Id<'_storage'>;
const enderecoFull = [data.logradouro, data.bairro].filter(Boolean).join(", "); salarioFamilia?: boolean;
endereco = enderecoFull; impostoRenda?: boolean;
cidade = data.localidade || ""; }>
uf = data.uf || ""; >([]);
} catch {} let mostrarFormularioDependente = $state(false);
} let dependenteAtual = $state<{
parentesco: 'outro' | 'filho' | 'filha' | 'conjuge' | null;
nome: string;
cpf: string;
nascimento: string;
arquivo: File | null;
documentoId?: Id<'_storage'>;
salarioFamilia?: boolean;
impostoRenda?: boolean;
}>({
parentesco: null,
nome: '',
cpf: '',
nascimento: '',
arquivo: null,
documentoId: undefined,
salarioFamilia: false,
impostoRenda: false
});
async function handleDocumentoUpload(campo: string, file: File) { function adicionarCurso() {
try { if (!cursoAtual.descricao.trim() || !cursoAtual.data.trim()) {
// Gerar URL de upload alert('Preencha a descrição e a data do curso');
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {}); return;
}
// Fazer upload do arquivo cursos.push({
const result = await fetch(uploadUrl, { id: crypto.randomUUID(),
method: "POST", descricao: cursoAtual.descricao,
headers: { "Content-Type": file.type }, data: cursoAtual.data,
body: file, certificadoId: undefined
}); });
cursoAtual = { descricao: '', data: '', arquivo: null };
const { storageId } = await result.json(); }
documentosStorage[campo] = storageId;
} catch (err: any) {
throw new Error(err?.message || "Erro ao fazer upload");
}
}
async function handleDocumentoRemove(campo: string) { function removerCurso(id: string) {
documentosStorage[campo] = undefined; cursos = cursos.filter((c) => c.id !== id);
} }
async function handleSubmit() { async function uploadCertificado(file: File): Promise<Id<'_storage'>> {
// Validação básica const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
if (!nome || !cpf || !rg || !nascimento || !email || !telefone) { const response = await fetch(uploadUrl, {
notice = { kind: "error", text: "Preencha todos os campos obrigatórios" }; method: 'POST',
return; headers: { 'Content-Type': file.type },
} body: file
});
if (!validateCPF(cpf)) { const result = await response.json();
notice = { kind: "error", text: "CPF inválido" }; if (!result.storageId) {
return; throw new Error('Erro ao fazer upload do certificado');
} }
const storageId = result.storageId as unknown as Id<'_storage'>;
if (!validateDate(nascimento)) { return storageId;
notice = { kind: "error", text: "Data de nascimento inválida" }; }
return;
} async function uploadDocumentoDependente(file: File): Promise<Id<'_storage'>> {
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
if (!simboloId) { const response = await fetch(uploadUrl, {
notice = { kind: "error", text: "Selecione um símbolo" }; method: 'POST',
return; headers: { 'Content-Type': file.type },
} body: file
});
const result = await response.json();
return result.storageId as Id<'_storage'>;
}
function adicionarDependente() { function adicionarDependente() {
if (dependentes.length >= 10) { if (dependentes.length >= 10) {
@@ -231,7 +245,7 @@
impostoRenda: !!dependenteAtual.impostoRenda impostoRenda: !!dependenteAtual.impostoRenda
}); });
dependenteAtual = { dependenteAtual = {
parentesco: '', parentesco: null,
nome: '', nome: '',
cpf: '', cpf: '',
nascimento: '', nascimento: '',
@@ -242,62 +256,232 @@
}; };
} }
const novoFuncionarioId = await client.mutation(api.funcionarios.create, payload as any); function removerDependente(id: string) {
dependentes = dependentes.filter((d) => d.id !== id);
// Salvar cursos, se houver }
for (const curso of cursos) {
let certificadoId = curso.certificadoId;
// Se houver arquivo para upload, fazer o upload
if (cursoAtual.arquivo && curso.id === cursos[cursos.length - 1].id) {
try {
certificadoId = await uploadCertificado(cursoAtual.arquivo);
} catch (err) {
console.error("Erro ao fazer upload do certificado:", err);
}
}
await client.mutation(api.cursos.criar, {
funcionarioId: novoFuncionarioId,
descricao: curso.descricao,
data: curso.data,
certificadoId: certificadoId as any,
});
}
notice = { kind: "success", text: "Funcionário cadastrado com sucesso!" };
setTimeout(() => goto("/recursos-humanos/funcionarios"), 600);
} catch (e: any) {
const msg = e?.message || String(e);
if (/CPF j[aá] cadastrado/i.test(msg)) {
notice = { kind: "error", text: "CPF já cadastrado." };
} else if (/Matr[ií]cula j[aá] cadastrada/i.test(msg)) {
notice = { kind: "error", text: "Matrícula já cadastrada." };
} else {
notice = { kind: "error", text: "Erro ao cadastrar funcionário." };
}
} finally {
loading = false;
}
}
$effect(() => { async function loadSimbolos() {
loadSimbolos(); const list = await client.query(api.simbolos.getAll, {});
}); simbolos = list.map((s) => ({
_id: s._id,
nome: s.nome,
tipo: s.tipo,
descricao: s.descricao
}));
}
// Resetar simboloId quando tipo mudar async function fillFromCEP(cepValue: string) {
$effect(() => { const cepDigits = onlyDigits(cepValue);
tipo; // track tipo if (cepDigits.length !== 8) return;
simboloId = ""; // reset selection when tipo changes
}); try {
const res = await fetch(`https://viacep.com.br/ws/${cepDigits}/json/`);
const data = await res.json();
if (!data || data.erro) return;
const enderecoFull = [data.logradouro, data.bairro].filter(Boolean).join(', ');
endereco = enderecoFull;
cidade = data.localidade || '';
uf = data.uf || '';
} catch {}
}
async function handleDocumentoUpload(campo: string, file: File) {
try {
// Gerar URL de upload
const uploadUrl = await client.mutation(api.documentos.generateUploadUrl, {});
// Fazer upload do arquivo
const result = await fetch(uploadUrl, {
method: 'POST',
headers: { 'Content-Type': file.type },
body: file
});
const { storageId } = await result.json();
documentosStorage[campo] = storageId;
} catch (err) {
if (err instanceof Error) {
throw new Error(err.message);
}
throw new Error('Erro ao fazer upload');
}
}
async function handleDocumentoRemove(campo: string) {
documentosStorage[campo] = undefined;
}
async function handleSubmit() {
// Validação básica
if (!nome || !cpf || !rg || !nascimento || !email || !telefone) {
notice = { kind: 'error', text: 'Preencha todos os campos obrigatórios' };
return;
}
if (!validateCPF(cpf)) {
notice = { kind: 'error', text: 'CPF inválido' };
return;
}
if (!validateDate(nascimento)) {
notice = { kind: 'error', text: 'Data de nascimento inválida' };
return;
}
if (!simboloId) {
notice = { kind: 'error', text: 'Selecione um símbolo' };
return;
}
if (dependentes.some((d) => d.parentesco === null)) {
notice = { kind: 'error', text: 'Preencha o parentesco de todos os dependentes' };
return;
}
if (!regimeTrabalho) {
notice = { kind: 'error', text: 'Selecione um regime de trabalho' };
return;
}
try {
loading = true;
const payload: CreateArgs = {
nome,
matricula: matricula.trim() || undefined,
cpf: onlyDigits(cpf),
rg: onlyDigits(rg),
nascimento,
email,
telefone: onlyDigits(telefone),
endereco,
cep: onlyDigits(cep),
cidade,
uf: uf.toUpperCase(),
simboloId: simboloId,
simboloTipo: tipo,
admissaoData: admissaoData || undefined,
desligamentoData: undefined,
// Dados Pessoais Adicionais
nomePai: nomePai || undefined,
nomeMae: nomeMae || undefined,
naturalidade: naturalidade || undefined,
naturalidadeUF: naturalidadeUF ? naturalidadeUF.toUpperCase() : undefined,
sexo: sexo || undefined,
estadoCivil: estadoCivil || undefined,
nacionalidade: nacionalidade || undefined,
// Documentos Pessoais
rgOrgaoExpedidor: rgOrgaoExpedidor || undefined,
rgDataEmissao: rgDataEmissao || undefined,
carteiraProfissionalNumero: carteiraProfissionalNumero || undefined,
carteiraProfissionalSerie: carteiraProfissionalSerie || undefined,
carteiraProfissionalDataEmissao: carteiraProfissionalDataEmissao || undefined,
reservistaNumero: reservistaNumero || undefined,
reservistaSerie: reservistaSerie || undefined,
tituloEleitorNumero: tituloEleitorNumero || undefined,
tituloEleitorZona: tituloEleitorZona || undefined,
tituloEleitorSecao: tituloEleitorSecao || undefined,
pisNumero: pisNumero || undefined,
// Formação e Saúde
grauInstrucao: grauInstrucao || undefined,
formacao: formacao || undefined,
formacaoRegistro: formacaoRegistro || undefined,
grupoSanguineo: grupoSanguineo || undefined,
fatorRH: fatorRH || undefined,
// Cargo e Vínculo
descricaoCargo: descricaoCargo || undefined,
regimeTrabalho: regimeTrabalho,
nomeacaoPortaria: nomeacaoPortaria || undefined,
nomeacaoData: nomeacaoData || undefined,
nomeacaoDOE: nomeacaoDOE || undefined,
pertenceOrgaoPublico: pertenceOrgaoPublico || undefined,
orgaoOrigem: orgaoOrigem || undefined,
aposentado: aposentado || undefined,
// Dados Bancários
contaBradescoNumero: contaBradescoNumero || undefined,
contaBradescoDV: contaBradescoDV || undefined,
contaBradescoAgencia: contaBradescoAgencia || undefined,
// Documentos
...Object.fromEntries(
Object.entries(documentosStorage).map(([key, value]) => [key, value])
),
// Dependentes (opcional)
dependentes: dependentes.length
? dependentes.map((d) => ({
parentesco: d.parentesco as 'outro' | 'filho' | 'filha' | 'conjuge',
nome: d.nome,
cpf: d.cpf,
nascimento: d.nascimento,
documentoId: d.documentoId,
salarioFamilia: !!d.salarioFamilia,
impostoRenda: !!d.impostoRenda
}))
: undefined
};
const novoFuncionarioId = await client.mutation(api.funcionarios.create, payload);
// Salvar cursos, se houver
for (const curso of cursos) {
let certificadoId = curso.certificadoId;
// Se houver arquivo para upload, fazer o upload
if (cursoAtual.arquivo && curso.id === cursos[cursos.length - 1].id) {
try {
certificadoId = await uploadCertificado(cursoAtual.arquivo);
} catch (err) {
console.error('Erro ao fazer upload do certificado:', err);
}
}
await client.mutation(api.cursos.criar, {
funcionarioId: novoFuncionarioId,
descricao: curso.descricao,
data: curso.data,
certificadoId: certificadoId
});
}
notice = { kind: 'success', text: 'Funcionário cadastrado com sucesso!' };
setTimeout(() => goto(resolve('/recursos-humanos/funcionarios')), 600);
} catch (e) {
if (e instanceof Error) {
const msg = e?.message || String(e);
if (/CPF j[aá] cadastrado/i.test(msg)) {
notice = { kind: 'error', text: 'CPF já cadastrado.' };
} else if (/Matr[ií]cula j[aá] cadastrada/i.test(msg)) {
notice = { kind: 'error', text: 'Matrícula já cadastrada.' };
} else {
notice = { kind: 'error', text: 'Erro ao cadastrar funcionário.' };
}
}
} finally {
loading = false;
}
}
$effect(() => {
loadSimbolos();
});
</script> </script>
<main class="container mx-auto max-w-7xl px-4 py-4"> <main class="container mx-auto max-w-7xl px-4 py-4">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="breadcrumbs mb-4 text-sm"> <div class="breadcrumbs mb-4 text-sm">
<ul> <ul>
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
<li> <li>
<a href="/recursos-humanos/funcionarios" class="text-primary hover:underline" <a href={resolve('/recursos-humanos')} class="text-primary hover:underline"
>Recursos Humanos</a
>
</li>
<li>
<a href={resolve('/recursos-humanos/funcionarios')} class="text-primary hover:underline"
>Funcionários</a >Funcionários</a
> >
</li> </li>
@@ -575,7 +759,7 @@
bind:value={naturalidadeUF} bind:value={naturalidadeUF}
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each UFS_BRASIL as ufOption} {#each UFS_BRASIL as ufOption (ufOption)}
<option value={ufOption}>{ufOption}</option> <option value={ufOption}>{ufOption}</option>
{/each} {/each}
</select> </select>
@@ -588,7 +772,7 @@
</label> </label>
<select id="sexo" class="select select-bordered w-full" bind:value={sexo}> <select id="sexo" class="select select-bordered w-full" bind:value={sexo}>
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each SEXO_OPTIONS as option} {#each SEXO_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -601,7 +785,7 @@
</label> </label>
<select id="estadoCivil" class="select select-bordered w-full" bind:value={estadoCivil}> <select id="estadoCivil" class="select select-bordered w-full" bind:value={estadoCivil}>
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each ESTADO_CIVIL_OPTIONS as option} {#each ESTADO_CIVIL_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -802,7 +986,7 @@
bind:value={grauInstrucao} bind:value={grauInstrucao}
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each GRAU_INSTRUCAO_OPTIONS as option} {#each GRAU_INSTRUCAO_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -847,7 +1031,7 @@
bind:value={grupoSanguineo} bind:value={grupoSanguineo}
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each GRUPO_SANGUINEO_OPTIONS as option} {#each GRUPO_SANGUINEO_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -860,7 +1044,7 @@
</label> </label>
<select id="fatorRH" class="select select-bordered w-full" bind:value={fatorRH}> <select id="fatorRH" class="select select-bordered w-full" bind:value={fatorRH}>
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each FATOR_RH_OPTIONS as option} {#each FATOR_RH_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -898,7 +1082,7 @@
{#if cursos.length > 0} {#if cursos.length > 0}
<div class="space-y-2"> <div class="space-y-2">
<h3 class="text-sm font-semibold">Cursos adicionados ({cursos.length}/7)</h3> <h3 class="text-sm font-semibold">Cursos adicionados ({cursos.length}/7)</h3>
{#each cursos as curso} {#each cursos as curso (curso.id)}
<div class="bg-base-200 flex items-center gap-3 rounded-lg p-3"> <div class="bg-base-200 flex items-center gap-3 rounded-lg p-3">
<div class="flex-1"> <div class="flex-1">
<p class="text-sm font-semibold">{curso.descricao}</p> <p class="text-sm font-semibold">{curso.descricao}</p>
@@ -906,7 +1090,7 @@
</div> </div>
<button <button
type="button" type="button"
class="btn btn-sm btn-error" class="btn btn-sm btn-error btn-square"
aria-label="Remover curso" aria-label="Remover curso"
onclick={() => removerCurso(curso.id)} onclick={() => removerCurso(curso.id)}
> >
@@ -1095,7 +1279,7 @@
</label> </label>
<select id="uf" class="select select-bordered w-full" bind:value={uf} required> <select id="uf" class="select select-bordered w-full" bind:value={uf} required>
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each UFS_BRASIL as ufOption} {#each UFS_BRASIL as ufOption (ufOption)}
<option value={ufOption}>{ufOption}</option> <option value={ufOption}>{ufOption}</option>
{/each} {/each}
</select> </select>
@@ -1176,7 +1360,7 @@
{#if dependentes.length > 0} {#if dependentes.length > 0}
<div class="space-y-2"> <div class="space-y-2">
<h3 class="text-sm font-semibold">Dependentes adicionados ({dependentes.length}/10)</h3> <h3 class="text-sm font-semibold">Dependentes adicionados ({dependentes.length}/10)</h3>
{#each dependentes as dep} {#each dependentes as dep (dep.id)}
<div class="bg-base-200 flex items-center gap-3 rounded-lg p-3"> <div class="bg-base-200 flex items-center gap-3 rounded-lg p-3">
<div class="flex-1 text-sm"> <div class="flex-1 text-sm">
<p class="font-semibold">{dep.nome}{dep.parentesco}</p> <p class="font-semibold">{dep.nome}{dep.parentesco}</p>
@@ -1204,7 +1388,7 @@
</div> </div>
<button <button
type="button" type="button"
class="btn btn-sm btn-error" class="btn btn-sm btn-error btn-square"
aria-label="Remover dependente" aria-label="Remover dependente"
onclick={() => removerDependente(dep.id)} onclick={() => removerDependente(dep.id)}
> >
@@ -1427,7 +1611,7 @@
required required
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each simbolos.filter((s) => s.tipo === tipo) as s} {#each simbolos.filter((s) => s.tipo === tipo) as s (s._id)}
<option value={s._id}>{s.nome} {s.descricao}</option> <option value={s._id}>{s.nome} {s.descricao}</option>
{/each} {/each}
</select> </select>
@@ -1530,7 +1714,7 @@
required required
> >
<option value="">Selecione...</option> <option value="">Selecione...</option>
{#each REGIME_TRABALHO_OPTIONS as option} {#each REGIME_TRABALHO_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -1573,7 +1757,7 @@
<span class="label-text font-medium">Se Aposentado</span> <span class="label-text font-medium">Se Aposentado</span>
</label> </label>
<select id="aposentado" class="select select-bordered w-full" bind:value={aposentado}> <select id="aposentado" class="select select-bordered w-full" bind:value={aposentado}>
{#each APOSENTADO_OPTIONS as option} {#each APOSENTADO_OPTIONS as option (option.value)}
<option value={option.value}>{option.label}</option> <option value={option.value}>{option.label}</option>
{/each} {/each}
</select> </select>
@@ -1677,11 +1861,11 @@
Anexe os documentos necessários em formato PDF ou imagem (máximo 10MB cada) Anexe os documentos necessários em formato PDF ou imagem (máximo 10MB cada)
</p> </p>
{#each categoriasDocumentos as categoria} {#each categoriasDocumentos as categoria (categoria)}
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-primary text-lg font-semibold">{categoria}</h3> <h3 class="text-primary text-lg font-semibold">{categoria}</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each getDocumentosByCategoria(categoria) as doc} {#each getDocumentosByCategoria(categoria) as doc (doc.campo)}
<FileUpload <FileUpload
label={doc.nome} label={doc.nome}
helpUrl={doc.helpUrl} helpUrl={doc.helpUrl}
@@ -1705,7 +1889,7 @@
type="button" type="button"
class="btn btn-ghost btn-lg" class="btn btn-ghost btn-lg"
disabled={loading} disabled={loading}
onclick={() => goto('/recursos-humanos/funcionarios')} onclick={() => goto(resolve('/recursos-humanos/funcionarios'))}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
import { v } from 'convex/values'; import { Infer, v } from 'convex/values';
import { query, mutation } from './_generated/server'; import { query, mutation } from './_generated/server';
import { internal } from './_generated/api'; import { internal } from './_generated/api';
import { simboloTipo } from './schema'; import { simboloTipo } from './schema';
@@ -109,119 +109,124 @@ export const getCurrent = query({
} }
}); });
export const create = mutation({ const createArgs = v.object({
args: { // Campos obrigatórios
// Campos obrigatórios nome: v.string(),
nome: v.string(), matricula: v.optional(v.string()),
matricula: v.optional(v.string()), simboloId: v.id('simbolos'),
simboloId: v.id('simbolos'), nascimento: v.string(),
nascimento: v.string(), rg: v.string(),
rg: v.string(), cpf: v.string(),
cpf: v.string(), endereco: v.string(),
endereco: v.string(), cep: v.string(),
cep: v.string(), cidade: v.string(),
cidade: v.string(), uf: v.string(),
uf: v.string(), telefone: v.string(),
telefone: v.string(), email: v.string(),
email: v.string(), admissaoData: v.optional(v.string()),
admissaoData: v.optional(v.string()), desligamentoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()), simboloTipo: simboloTipo,
simboloTipo: simboloTipo,
// Dados Pessoais Adicionais // Dados Pessoais Adicionais
nomePai: v.optional(v.string()), nomePai: v.optional(v.string()),
nomeMae: v.optional(v.string()), nomeMae: v.optional(v.string()),
naturalidade: v.optional(v.string()), naturalidade: v.optional(v.string()),
naturalidadeUF: v.optional(v.string()), naturalidadeUF: v.optional(v.string()),
sexo: sexoValidator, sexo: sexoValidator,
estadoCivil: estadoCivilValidator, estadoCivil: estadoCivilValidator,
nacionalidade: v.optional(v.string()), nacionalidade: v.optional(v.string()),
// Documentos Pessoais // Documentos Pessoais
rgOrgaoExpedidor: v.optional(v.string()), rgOrgaoExpedidor: v.optional(v.string()),
rgDataEmissao: v.optional(v.string()), rgDataEmissao: v.optional(v.string()),
carteiraProfissionalNumero: v.optional(v.string()), carteiraProfissionalNumero: v.optional(v.string()),
carteiraProfissionalSerie: v.optional(v.string()), carteiraProfissionalSerie: v.optional(v.string()),
carteiraProfissionalDataEmissao: v.optional(v.string()), carteiraProfissionalDataEmissao: v.optional(v.string()),
reservistaNumero: v.optional(v.string()), reservistaNumero: v.optional(v.string()),
reservistaSerie: v.optional(v.string()), reservistaSerie: v.optional(v.string()),
tituloEleitorNumero: v.optional(v.string()), tituloEleitorNumero: v.optional(v.string()),
tituloEleitorZona: v.optional(v.string()), tituloEleitorZona: v.optional(v.string()),
tituloEleitorSecao: v.optional(v.string()), tituloEleitorSecao: v.optional(v.string()),
pisNumero: v.optional(v.string()), pisNumero: v.optional(v.string()),
// Formação e Saúde // Formação e Saúde
grauInstrucao: grauInstrucaoValidator, grauInstrucao: grauInstrucaoValidator,
formacao: v.optional(v.string()), formacao: v.optional(v.string()),
formacaoRegistro: v.optional(v.string()), formacaoRegistro: v.optional(v.string()),
grupoSanguineo: grupoSanguineoValidator, grupoSanguineo: grupoSanguineoValidator,
fatorRH: fatorRHValidator, fatorRH: fatorRHValidator,
// Cargo e Vínculo // Cargo e Vínculo
descricaoCargo: v.optional(v.string()), descricaoCargo: v.optional(v.string()),
nomeacaoPortaria: v.optional(v.string()), nomeacaoPortaria: v.optional(v.string()),
nomeacaoData: v.optional(v.string()), nomeacaoData: v.optional(v.string()),
nomeacaoDOE: v.optional(v.string()), nomeacaoDOE: v.optional(v.string()),
pertenceOrgaoPublico: v.optional(v.boolean()), pertenceOrgaoPublico: v.optional(v.boolean()),
orgaoOrigem: v.optional(v.string()), orgaoOrigem: v.optional(v.string()),
aposentado: aposentadoValidator, aposentado: aposentadoValidator,
regimeTrabalho: regimeTrabalhoValidator,
// Dados Bancários // Dados Bancários
contaBradescoNumero: v.optional(v.string()), contaBradescoNumero: v.optional(v.string()),
contaBradescoDV: v.optional(v.string()), contaBradescoDV: v.optional(v.string()),
contaBradescoAgencia: v.optional(v.string()), contaBradescoAgencia: v.optional(v.string()),
// Documentos Anexos (Storage IDs) // Documentos Anexos (Storage IDs)
certidaoAntecedentesPF: v.optional(v.id('_storage')), certidaoAntecedentesPF: v.optional(v.id('_storage')),
certidaoAntecedentesJFPE: v.optional(v.id('_storage')), certidaoAntecedentesJFPE: v.optional(v.id('_storage')),
certidaoAntecedentesSDS: v.optional(v.id('_storage')), certidaoAntecedentesSDS: v.optional(v.id('_storage')),
certidaoAntecedentesTJPE: v.optional(v.id('_storage')), certidaoAntecedentesTJPE: v.optional(v.id('_storage')),
certidaoImprobidade: v.optional(v.id('_storage')), certidaoImprobidade: v.optional(v.id('_storage')),
rgFrente: v.optional(v.id('_storage')), rgFrente: v.optional(v.id('_storage')),
rgVerso: v.optional(v.id('_storage')), rgVerso: v.optional(v.id('_storage')),
cpfFrente: v.optional(v.id('_storage')), cpfFrente: v.optional(v.id('_storage')),
cpfVerso: v.optional(v.id('_storage')), cpfVerso: v.optional(v.id('_storage')),
situacaoCadastralCPF: v.optional(v.id('_storage')), situacaoCadastralCPF: v.optional(v.id('_storage')),
tituloEleitorFrente: v.optional(v.id('_storage')), tituloEleitorFrente: v.optional(v.id('_storage')),
tituloEleitorVerso: v.optional(v.id('_storage')), tituloEleitorVerso: v.optional(v.id('_storage')),
comprovanteVotacao: v.optional(v.id('_storage')), comprovanteVotacao: v.optional(v.id('_storage')),
carteiraProfissionalFrente: v.optional(v.id('_storage')), carteiraProfissionalFrente: v.optional(v.id('_storage')),
carteiraProfissionalVerso: v.optional(v.id('_storage')), carteiraProfissionalVerso: v.optional(v.id('_storage')),
comprovantePIS: v.optional(v.id('_storage')), comprovantePIS: v.optional(v.id('_storage')),
certidaoRegistroCivil: v.optional(v.id('_storage')), certidaoRegistroCivil: v.optional(v.id('_storage')),
certidaoNascimentoDependentes: v.optional(v.id('_storage')), certidaoNascimentoDependentes: v.optional(v.id('_storage')),
cpfDependentes: v.optional(v.id('_storage')), cpfDependentes: v.optional(v.id('_storage')),
reservistaDoc: v.optional(v.id('_storage')), reservistaDoc: v.optional(v.id('_storage')),
comprovanteEscolaridade: v.optional(v.id('_storage')), comprovanteEscolaridade: v.optional(v.id('_storage')),
comprovanteResidencia: v.optional(v.id('_storage')), comprovanteResidencia: v.optional(v.id('_storage')),
comprovanteContaBradesco: v.optional(v.id('_storage')), comprovanteContaBradesco: v.optional(v.id('_storage')),
// Declarações (Storage IDs) // Declarações (Storage IDs)
declaracaoAcumulacaoCargo: v.optional(v.id('_storage')), declaracaoAcumulacaoCargo: v.optional(v.id('_storage')),
declaracaoDependentesIR: v.optional(v.id('_storage')), declaracaoDependentesIR: v.optional(v.id('_storage')),
declaracaoIdoneidade: v.optional(v.id('_storage')), declaracaoIdoneidade: v.optional(v.id('_storage')),
termoNepotismo: v.optional(v.id('_storage')), termoNepotismo: v.optional(v.id('_storage')),
termoOpcaoRemuneracao: v.optional(v.id('_storage')), termoOpcaoRemuneracao: v.optional(v.id('_storage')),
// Dependentes (opcional) // Dependentes (opcional)
dependentes: v.optional( dependentes: v.optional(
v.array( v.array(
v.object({ v.object({
parentesco: v.union( parentesco: v.union(
v.literal('filho'), v.literal('filho'),
v.literal('filha'), v.literal('filha'),
v.literal('conjuge'), v.literal('conjuge'),
v.literal('outro') v.literal('outro')
), ),
nome: v.string(), nome: v.string(),
cpf: v.string(), cpf: v.string(),
nascimento: v.string(), nascimento: v.string(),
documentoId: v.optional(v.id('_storage')), documentoId: v.optional(v.id('_storage')),
salarioFamilia: v.optional(v.boolean()), salarioFamilia: v.optional(v.boolean()),
impostoRenda: v.optional(v.boolean()) impostoRenda: v.optional(v.boolean())
}) })
)
) )
}, )
});
export type CreateArgs = Infer<typeof createArgs>;
export const create = mutation({
args: createArgs,
returns: v.id('funcionarios'), returns: v.id('funcionarios'),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Autorização: criar // Autorização: criar