Feat cadastro funcinarios #2
@@ -13,7 +13,7 @@
|
|||||||
>Cadastrar Funcionários</a
|
>Cadastrar Funcionários</a
|
||||||
>
|
>
|
||||||
<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
|
class="p-4 rounded-xl border hover:shadow bgbase-100">Editar Cadastro</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,179 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { query, mutation } from "./_generated/server";
|
import { query, mutation } from "./_generated/server";
|
||||||
|
import { simboloTipo } from "./schema";
|
||||||
|
|
||||||
export const getAll = query({
|
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) => {
|
handler: async (ctx) => {
|
||||||
return await ctx.db.query("funcionarios").collect();
|
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({
|
export const create = mutation({
|
||||||
args: {
|
args: {
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
matricula: v.string(),
|
matricula: v.string(),
|
||||||
simboloId: v.id("simbolos"),
|
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) => {
|
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", {
|
const novoFuncionarioId = await ctx.db.insert("funcionarios", {
|
||||||
nome: args.nome,
|
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,
|
matricula: args.matricula,
|
||||||
|
admissaoData: args.admissaoData,
|
||||||
|
desligamentoData: args.desligamentoData,
|
||||||
simboloId: args.simboloId,
|
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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
import { Infer, v } from "convex/values";
|
import { Infer, v } from "convex/values";
|
||||||
import { tables } from "./betterAuth/schema";
|
import { tables } from "./betterAuth/schema";
|
||||||
|
import { cidrv4 } from "better-auth";
|
||||||
|
|
||||||
export const simboloTipo = v.union(
|
export const simboloTipo = v.union(
|
||||||
v.literal("cargo_comissionado"),
|
v.literal("cargo_comissionado"),
|
||||||
@@ -16,24 +17,41 @@ export default defineSchema({
|
|||||||
}),
|
}),
|
||||||
funcionarios: defineTable({
|
funcionarios: defineTable({
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
nascimento: v.optional(v.string()),
|
nascimento: v.string(),
|
||||||
rg: v.optional(v.string()),
|
rg: v.string(),
|
||||||
cpf: v.optional(v.string()),
|
cpf: v.string(),
|
||||||
endereco: v.optional(v.string()),
|
endereco: v.string(),
|
||||||
cep: v.optional(v.string()),
|
cep: v.string(),
|
||||||
cidade: v.optional(v.string()),
|
cidade: v.string(),
|
||||||
uf: v.optional(v.string()),
|
uf: v.string(),
|
||||||
telefone: v.optional(v.string()),
|
telefone: v.string(),
|
||||||
email: v.optional(v.string()),
|
email: v.string(),
|
||||||
matricula: v.string(),
|
matricula: v.string(),
|
||||||
vencimento: v.optional(v.string()),
|
admissaoData: v.optional(v.string()),
|
||||||
admissao: v.optional(v.string()),
|
desligamentoData: v.optional(v.string()),
|
||||||
desligamento: v.optional(v.string()),
|
|
||||||
ferias: v.optional(v.string()),
|
|
||||||
simboloId: v.id("simbolos"),
|
simboloId: v.id("simbolos"),
|
||||||
|
simboloTipo: simboloTipo,
|
||||||
})
|
})
|
||||||
.index("by_matricula", ["matricula"])
|
.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({
|
simbolos: defineTable({
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user