feat: implement employee registration form with validation and data handling

This commit is contained in:
2025-10-24 17:25:46 -03:00
parent 316877e1bb
commit be3522ae74
5 changed files with 855 additions and 16 deletions

View File

@@ -13,7 +13,7 @@
>Cadastrar Funcionários</a
>
<a
href={resolve("/recursos-humanos/funcionarios/editar")}
href={resolve("/recursos-humanos/funcionarios")}
class="p-4 rounded-xl border hover:shadow bgbase-100">Editar Cadastro</a
>
<a

View File

@@ -0,0 +1,317 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { createForm } from "@tanstack/svelte-form";
import z from "zod";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
const client = useConvexClient();
$: funcionarioId = $page.params.funcionarioId as string;
let simbolos: Array<{ _id: string; nome: string; tipo: SimboloTipo; descricao: string }> = [];
let tipo: SimboloTipo = "cargo_comissionado";
const onlyDigits = (s: string) => (s || "").replace(/\D/g, "");
const maskCPF = (v: string) => onlyDigits(v).slice(0, 11).replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d{1,2})$/, "$1-$2");
const maskRG = (v: string) => onlyDigits(v).slice(0, 9).replace(/(\d{2})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d{1})$/, "$1-$2");
const maskCEP = (v: string) => onlyDigits(v).slice(0, 8).replace(/(\d{5})(\d{1,3})$/, "$1-$2");
const maskPhone = (v: string) => {
const d = onlyDigits(v).slice(0, 11);
if (d.length <= 10) return d.replace(/(\d{2})(\d)/, "($1) $2").replace(/(\d{4})(\d{1,4})$/, "$1-$2");
return d.replace(/(\d{2})(\d)/, "($1) $2").replace(/(\d{5})(\d{1,4})$/, "$1-$2");
};
const maskDate = (v: string) => onlyDigits(v).slice(0, 8).replace(/(\d{2})(\d)/, "$1/$2").replace(/(\d{2})(\d{1,4})$/, "$1/$2");
const isValidDateBR = (v: string) => {
const m = v.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!m) return false;
const dd = Number(m[1]), mm = Number(m[2]) - 1, yyyy = Number(m[3]);
const dt = new Date(yyyy, mm, dd);
return dt.getFullYear() === yyyy && dt.getMonth() === mm && dt.getDate() === dd;
};
const isValidCPF = (raw: string) => {
const d = onlyDigits(raw);
if (d.length !== 11 || /^([0-9])\1+$/.test(d)) return false;
const calc = (base: string, factor: 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 d1 = calc(d.slice(0, 9), 10); const d2 = calc(d.slice(0, 10), 11);
return d[9] === String(d1) && d[10] === String(d2);
};
const schema = z.object({
nome: z.string().min(3),
matricula: z.string().min(1),
cpf: z.string().min(1).refine(isValidCPF, "CPF inválido"),
rg: z.string().min(1).refine((v) => /^\d{2}\.\d{3}\.\d{3}-\d$/.test(maskRG(v)), "RG inválido"),
nascimento: z.string().refine(isValidDateBR, "Data inválida"),
email: z.string().email(),
telefone: z.string().min(1).refine((v) => /\(\d{2}\) \d{4,5}-\d{4}/.test(maskPhone(v)), "Telefone inválido"),
endereco: z.string().min(1),
cep: z.string().min(1).refine((v) => /^\d{5}-\d{3}$/.test(maskCEP(v)), "CEP inválido"),
cidade: z.string().min(1),
uf: z.string().length(2).transform((s) => s.toUpperCase()),
simboloTipo: z.enum(["cargo_comissionado", "funcao_gratificada"]),
simboloId: z.string().min(1),
admissaoData: z.string().refine(isValidDateBR, "Data inválida"),
}).superRefine((val, ctx) => {
if (val.cep && (!val.cidade || !val.uf)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["cidade"], message: "Cidade obrigatória" });
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["uf"], message: "UF obrigatória" });
}
});
let notice: { kind: "success" | "error"; text: string } | null = null;
const form = createForm(() => ({
defaultValues: {
nome: "",
matricula: "",
cpf: "",
rg: "",
nascimento: "",
email: "",
telefone: "",
endereco: "",
cep: "",
cidade: "",
uf: "",
simboloTipo: tipo as SimboloTipo,
simboloId: "",
admissaoData: "",
},
onSubmit: async ({ value, formApi }) => {
const payload = {
id: funcionarioId as any,
nome: value.nome,
matricula: value.matricula,
simboloId: value.simboloId as any,
nascimento: value.nascimento,
rg: onlyDigits(value.rg),
cpf: onlyDigits(value.cpf),
endereco: value.endereco,
cep: onlyDigits(value.cep),
cidade: value.cidade,
uf: value.uf.toUpperCase(),
telefone: onlyDigits(value.telefone),
email: value.email,
admissaoData: value.admissaoData,
desligamentoData: undefined,
simboloTipo: value.simboloTipo as SimboloTipo,
};
try {
await client.mutation(api.funcionarios.update, payload as any);
notice = { kind: "success", text: "Cadastro atualizado 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 atualizar cadastro." };
}
}
}));
async function load() {
const list = await client.query(api.simbolos.getAll, {} as any);
simbolos = list.map((s: any) => ({ _id: s._id, nome: s.nome, tipo: s.tipo, descricao: s.descricao }));
const doc = await client.query(api.funcionarios.getById, { id: funcionarioId as any });
if (!doc) {
notice = { kind: "error", text: "Funcionário não encontrado." };
return;
}
tipo = doc.simboloTipo as SimboloTipo;
// set defaults
form.setFieldValue("nome", doc.nome as any);
form.setFieldValue("matricula", doc.matricula as any);
form.setFieldValue("cpf", maskCPF(doc.cpf) as any);
form.setFieldValue("rg", maskRG(doc.rg) as any);
form.setFieldValue("nascimento", doc.nascimento as any);
form.setFieldValue("email", doc.email as any);
form.setFieldValue("telefone", maskPhone(doc.telefone) as any);
form.setFieldValue("endereco", doc.endereco as any);
form.setFieldValue("cep", maskCEP(doc.cep) as any);
form.setFieldValue("cidade", doc.cidade as any);
form.setFieldValue("uf", doc.uf as any);
form.setFieldValue("simboloTipo", doc.simboloTipo as any);
form.setFieldValue("simboloId", (doc.simboloId as unknown as string) as any);
form.setFieldValue("admissaoData", (doc.admissaoData ?? "") as any);
}
load();
</script>
<form class="max-w-3xl mx-auto p-4" onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}>
<div class="card bg-base-100 shadow-xl">
<div class="card-body space-y-6">
{#if notice}
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
<span>{notice.text}</span>
</div>
{/if}
<div>
<h2 class="card-title text-3xl">Editar Cadastro</h2>
<p class="opacity-70">Atualize os dados do funcionário.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Nome <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="matricula" validators={{ onChange: schema.shape.matricula }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Matrícula <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="cpf" validators={{ onChange: schema.shape.cpf }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">CPF <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskCPF(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="rg" validators={{ onChange: schema.shape.rg }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">RG <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="nascimento" validators={{ onChange: schema.shape.nascimento }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Nascimento <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" placeholder="dd/mm/aaaa" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="admissaoData" validators={{ onChange: schema.shape.admissaoData }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Admissão <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" placeholder="dd/mm/aaaa" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="email" validators={{ onChange: schema.shape.email }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">E-mail <span class="text-error">*</span></span></label>
<input {name} value={state.value} type="email" class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="telefone" validators={{ onChange: schema.shape.telefone }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Telefone <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskPhone(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<form.Field name="cep" validators={{ onChange: schema.shape.cep }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">CEP <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="cidade" validators={{ onChange: schema.shape.cidade }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Cidade <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="uf" validators={{ onChange: schema.shape.uf }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">UF <span class="text-error">*</span></span></label>
<input {name} value={state.value} maxlength={2} class="input input-bordered w-full uppercase" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
</div>
<form.Field name="endereco" validators={{ onChange: schema.shape.endereco }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Endereço <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="simboloTipo" validators={{ onChange: schema.shape.simboloTipo }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Símbolo Tipo <span class="text-error">*</span></span></label>
<select {name} class="select select-bordered w-full" bind:value={tipo} oninput={(e) => handleChange((e.target as HTMLSelectElement).value as any)} required>
<option value="cargo_comissionado">Cargo comissionado</option>
<option value="funcao_gratificada">Função gratificada</option>
</select>
</div>
{/snippet}
</form.Field>
<form.Field name="simboloId" validators={{ onChange: schema.shape.simboloId }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Símbolo <span class="text-error">*</span></span></label>
<select {name} class="select select-bordered w-full" value={state.value} oninput={(e) => handleChange((e.target as HTMLSelectElement).value)} required>
<option value="" disabled selected>Selecione...</option>
{#each simbolos.filter((s) => s.tipo === tipo) as s}
<option value={s._id}>{s.nome} {s.descricao}</option>
{/each}
</select>
</div>
{/snippet}
</form.Field>
</div>
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
{#snippet children({ canSubmit, isSubmitting })}
<div class="card-actions justify-end pt-2">
<button type="button" class="btn btn-ghost" disabled={isSubmitting} onclick={() => goto("/recursos-humanos/funcionarios")}>
Cancelar
</button>
<button type="submit" class="btn btn-primary" class:loading={isSubmitting} disabled={isSubmitting || !canSubmit}>
Salvar Alterações
</button>
</div>
{/snippet}
</form.Subscribe>
</div>
</div>
</form>

View File

@@ -0,0 +1,349 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { createForm } from "@tanstack/svelte-form";
import z from "zod";
import { goto } from "$app/navigation";
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
const client = useConvexClient();
let simbolos: Array<{
_id: string;
nome: string;
tipo: SimboloTipo;
descricao: string;
}> = [];
let tipo: SimboloTipo = "cargo_comissionado";
// Helpers: masks
const onlyDigits = (s: string) => (s || "").replace(/\D/g, "");
function maskCPF(v: string) {
const d = onlyDigits(v).slice(0, 11);
return d
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
}
function isValidCPF(raw: string) {
const d = onlyDigits(raw);
if (d.length !== 11 || /^([0-9])\1+$/.test(d)) return false;
const calc = (base: string, factor: 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 d1 = calc(d.slice(0, 9), 10);
const d2 = calc(d.slice(0, 10), 11);
return d[9] === String(d1) && d[10] === String(d2);
}
function maskRG(v: string) {
const d = onlyDigits(v).slice(0, 9);
return d
.replace(/(\d{2})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1})$/, "$1-$2");
}
function maskCEP(v: string) {
const d = onlyDigits(v).slice(0, 8);
return d.replace(/(\d{5})(\d{1,3})$/, "$1-$2");
}
function maskPhone(v: string) {
const d = onlyDigits(v).slice(0, 11);
if (d.length <= 10) {
return d
.replace(/(\d{2})(\d)/, "($1) $2")
.replace(/(\d{4})(\d{1,4})$/, "$1-$2");
}
return d
.replace(/(\d{2})(\d)/, "($1) $2")
.replace(/(\d{5})(\d{1,4})$/, "$1-$2");
}
function maskDate(v: string) {
const d = onlyDigits(v).slice(0, 8);
return d
.replace(/(\d{2})(\d)/, "$1/$2")
.replace(/(\d{2})(\d{1,4})$/, "$1/$2");
}
function isValidDateBR(v: string) {
const m = v.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!m) return false;
const dd = Number(m[1]), mm = Number(m[2]) - 1, yyyy = Number(m[3]);
const dt = new Date(yyyy, mm, dd);
return dt.getFullYear() === yyyy && dt.getMonth() === mm && dt.getDate() === dd;
}
// Schema
const schema = z.object({
nome: z.string().min(3, "Mínimo 3 caracteres"),
matricula: z.string().min(1, "Obrigatório"),
cpf: z.string().min(1, "Obrigatório").refine(isValidCPF, "CPF inválido"),
rg: z
.string()
.min(1, "Obrigatório")
.refine((v) => /^\d{2}\.\d{3}\.\d{3}-\d$/.test(maskRG(v)), "RG inválido"),
nascimento: z.string().refine(isValidDateBR, "Data inválida (dd/mm/aaaa)"),
email: z.string().email("E-mail inválido"),
telefone: z
.string()
.min(1, "Obrigatório")
.refine((v) => /\(\d{2}\) \d{4,5}-\d{4}/.test(maskPhone(v)), "Telefone inválido"),
endereco: z.string().min(1, "Obrigatório"),
cep: z
.string()
.min(1, "Obrigatório")
.refine((v) => /^\d{5}-\d{3}$/.test(maskCEP(v)), "CEP inválido"),
cidade: z.string().min(1, "Obrigatório"),
uf: z.string().length(2, "UF inválida").transform((s) => s.toUpperCase()),
simboloTipo: z.enum(["cargo_comissionado", "funcao_gratificada"]),
simboloId: z.string().min(1, "Obrigatório"),
admissaoData: z.string().refine(isValidDateBR, "Data inválida (dd/mm/aaaa)"),
}).superRefine((val, ctx) => {
if (val.cep && (!val.cidade || !val.uf)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["cidade"], message: "Cidade obrigatória" });
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["uf"], message: "UF obrigatória" });
}
});
const defaultValues = {
nome: "",
matricula: "",
cpf: "",
rg: "",
nascimento: "",
email: "",
telefone: "",
endereco: "",
cep: "",
cidade: "",
uf: "",
simboloTipo: tipo as SimboloTipo,
simboloId: "",
admissaoData: "",
};
async function loadSimbolos() {
const list = await client.query(api.simbolos.getAll, {} as any);
simbolos = list.map((s: any) => ({ _id: s._id, nome: s.nome, tipo: s.tipo, descricao: s.descricao }));
}
const form = createForm(() => ({
defaultValues,
onSubmit: async ({ value, formApi }) => {
const payload = {
nome: value.nome,
matricula: value.matricula,
simboloId: value.simboloId as any,
nascimento: value.nascimento,
rg: onlyDigits(value.rg),
cpf: onlyDigits(value.cpf),
endereco: value.endereco,
cep: onlyDigits(value.cep),
cidade: value.cidade,
uf: value.uf.toUpperCase(),
telefone: onlyDigits(value.telefone),
email: value.email,
admissaoData: value.admissaoData,
desligamentoData: undefined,
simboloTipo: value.simboloTipo as SimboloTipo,
};
try {
await client.mutation(api.funcionarios.create, payload 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)) formApi.setFieldError("cpf", "CPF já cadastrado");
if (/Matr[ií]cula j[aá] cadastrada/i.test(msg)) formApi.setFieldError("matricula", "Matrícula já cadastrada");
notice = { kind: "error", text: "Erro ao cadastrar funcionário." };
}
}
}));
let notice: { kind: "success" | "error"; text: string } | null = null;
loadSimbolos();
</script>
<form
class="max-w-3xl mx-auto p-4"
onsubmit={(e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}
>
<div class="card bg-base-100 shadow-xl">
<div class="card-body space-y-6">
{#if notice}
<div class="alert" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
<span>{notice.text}</span>
</div>
{/if}
<div>
<h2 class="card-title text-3xl">Cadastro de Funcionários</h2>
<p class="opacity-70">Preencha os campos abaixo para cadastrar um novo funcionário.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Nome <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="matricula" validators={{ onChange: schema.shape.matricula }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Matrícula <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="cpf" validators={{ onChange: schema.shape.cpf }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">CPF <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskCPF(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="rg" validators={{ onChange: schema.shape.rg }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">RG <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="nascimento" validators={{ onChange: schema.shape.nascimento }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Nascimento <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" placeholder="dd/mm/aaaa" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="admissaoData" validators={{ onChange: schema.shape.admissaoData }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Admissão <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" placeholder="dd/mm/aaaa" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="email" validators={{ onChange: schema.shape.email }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">E-mail <span class="text-error">*</span></span></label>
<input {name} value={state.value} type="email" class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="telefone" validators={{ onChange: schema.shape.telefone }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Telefone <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskPhone(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<form.Field name="cep" validators={{ onChange: schema.shape.cep }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">CEP <span class="text-error">*</span></span></label>
<input {name} value={state.value} inputmode="numeric" class="input input-bordered w-full" oninput={(e) => { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required />
</div>
{/snippet}
</form.Field>
<form.Field name="cidade" validators={{ onChange: schema.shape.cidade }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Cidade <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<form.Field name="uf" validators={{ onChange: schema.shape.uf }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">UF <span class="text-error">*</span></span></label>
<input {name} value={state.value} maxlength={2} class="input input-bordered w-full uppercase" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
</div>
<form.Field name="endereco" validators={{ onChange: schema.shape.endereco }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Endereço <span class="text-error">*</span></span></label>
<input {name} value={state.value} class="input input-bordered w-full" oninput={(e) => handleChange((e.target as HTMLInputElement).value)} required />
</div>
{/snippet}
</form.Field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field name="simboloTipo" validators={{ onChange: schema.shape.simboloTipo }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Símbolo Tipo <span class="text-error">*</span></span></label>
<select {name} class="select select-bordered w-full" bind:value={tipo} oninput={(e) => handleChange((e.target as HTMLSelectElement).value)} required>
<option value="cargo_comissionado">Cargo comissionado</option>
<option value="funcao_gratificada">Função gratificada</option>
</select>
</div>
{/snippet}
</form.Field>
<form.Field name="simboloId" validators={{ onChange: schema.shape.simboloId }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for={name}><span class="label-text font-medium">Símbolo <span class="text-error">*</span></span></label>
<select {name} class="select select-bordered w-full" value={state.value} oninput={(e) => handleChange((e.target as HTMLSelectElement).value)} required>
<option value="" disabled selected>Selecione...</option>
{#each simbolos.filter((s) => s.tipo === tipo) as s}
<option value={s._id}>{s.nome} {s.descricao}</option>
{/each}
</select>
</div>
{/snippet}
</form.Field>
</div>
<form.Subscribe selector={(s) => ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}>
{#snippet children({ canSubmit, isSubmitting })}
<div class="card-actions justify-end pt-2">
<button type="button" class="btn btn-ghost" disabled={isSubmitting} onclick={() => goto("/recursos-humanos/funcionarios")}>
Cancelar
</button>
<button type="submit" class="btn btn-primary" class:loading={isSubmitting} disabled={isSubmitting || !canSubmit}>
Cadastrar Funcionário
</button>
</div>
{/snippet}
</form.Subscribe>
</div>
</div>
</form>

View File

@@ -1,24 +1,179 @@
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { simboloTipo } from "./schema";
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();
},
});
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);
},
});
export const create = mutation({
args: {
nome: v.string(),
matricula: v.string(),
simboloId: v.id("simbolos"),
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(),
admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()),
simboloTipo: simboloTipo,
},
returns: v.id("funcionarios"),
handler: async (ctx, args) => {
// Unicidade: CPF
const cpfExists = await ctx.db
.query("funcionarios")
.withIndex("by_cpf", (q) => q.eq("cpf", args.cpf))
.unique();
if (cpfExists) {
throw new Error("CPF já cadastrado");
}
// Unicidade: Matrícula
const matriculaExists = await ctx.db
.query("funcionarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.unique();
if (matriculaExists) {
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,
});
return await ctx.db.get(novoFuncionarioId);
return novoFuncionarioId;
},
});
export const update = mutation({
args: {
id: v.id("funcionarios"),
nome: v.string(),
matricula: v.string(),
simboloId: v.id("simbolos"),
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(),
admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()),
simboloTipo: simboloTipo,
},
returns: v.null(),
handler: async (ctx, args) => {
// Unicidade: CPF (excluindo o próprio registro)
const cpfExists = await ctx.db
.query("funcionarios")
.withIndex("by_cpf", (q) => q.eq("cpf", args.cpf))
.unique();
if (cpfExists && cpfExists._id !== args.id) {
throw new Error("CPF já cadastrado");
}
// Unicidade: Matrícula (excluindo o próprio registro)
const matriculaExists = await ctx.db
.query("funcionarios")
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
.unique();
if (matriculaExists && matriculaExists._id !== args.id) {
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,
});
return null;
},
});

View File

@@ -1,6 +1,7 @@
import { defineSchema, defineTable } from "convex/server";
import { Infer, v } from "convex/values";
import { tables } from "./betterAuth/schema";
import { cidrv4 } from "better-auth";
export const simboloTipo = v.union(
v.literal("cargo_comissionado"),
@@ -16,24 +17,41 @@ export default defineSchema({
}),
funcionarios: defineTable({
nome: v.string(),
nascimento: v.optional(v.string()),
rg: v.optional(v.string()),
cpf: v.optional(v.string()),
endereco: v.optional(v.string()),
cep: v.optional(v.string()),
cidade: v.optional(v.string()),
uf: v.optional(v.string()),
telefone: v.optional(v.string()),
email: v.optional(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(),
vencimento: v.optional(v.string()),
admissao: v.optional(v.string()),
desligamento: v.optional(v.string()),
ferias: v.optional(v.string()),
admissaoData: v.optional(v.string()),
desligamentoData: v.optional(v.string()),
simboloId: v.id("simbolos"),
simboloTipo: simboloTipo,
})
.index("by_matricula", ["matricula"])
.index("by_nome", ["nome"]),
.index("by_nome", ["nome"])
.index("by_simboloId", ["simboloId"])
.index("by_simboloTipo", ["simboloTipo"])
.index("by_cpf", ["cpf"])
.index("by_rg", ["rg"]),
atestados: defineTable({
funcionarioId: v.id("funcionarios"),
dataInicio: v.string(),
dataFim: v.string(),
cid: v.string(),
descricao: v.string(),
}),
ferias: defineTable({
funcionarioId: v.id("funcionarios"),
dataInicio: v.string(),
dataFim: v.string(),
}),
simbolos: defineTable({
nome: v.string(),