From be3522ae749cacd2f63e750bcd6426ae71f011c8 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Fri, 24 Oct 2025 17:25:46 -0300 Subject: [PATCH 1/8] feat: implement employee registration form with validation and data handling --- .../(dashboard)/recursos-humanos/+page.svelte | 2 +- .../[funcionarioId]/editar/+page.svelte | 317 ++++++++++++++++ .../funcionarios/cadastro/+page.svelte | 349 ++++++++++++++++++ packages/backend/convex/funcionarios.ts | 157 +++++++- packages/backend/convex/schema.ts | 46 ++- 5 files changed, 855 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/+page.svelte index fdf04dc..0eac597 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/+page.svelte @@ -13,7 +13,7 @@ >Cadastrar Funcionários Editar Cadastro + 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(); + + +
{ e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }}> +
+
+ {#if notice} +
+ {notice.text} +
+ {/if} + +
+

Editar Cadastro

+

Atualize os dados do funcionário.

+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCPF(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskPhone(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ + + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + +
+ {/snippet} +
+ + + {#snippet children({ name, state, handleChange })} +
+ + +
+ {/snippet} +
+
+ + ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}> + {#snippet children({ canSubmit, isSubmitting })} +
+ + +
+ {/snippet} +
+
+
+
+ + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte index e69de29..7b6545e 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte @@ -0,0 +1,349 @@ + + +
{ e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }} +> +
+
+ {#if notice} +
+ {notice.text} +
+ {/if} + +
+

Cadastro de Funcionários

+

Preencha os campos abaixo para cadastrar um novo funcionário.

+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCPF(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskDate(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskPhone(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ + + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ +
+ + {#snippet children({ name, state, handleChange })} +
+ + +
+ {/snippet} +
+ + + {#snippet children({ name, state, handleChange })} +
+ + +
+ {/snippet} +
+
+ + ({ canSubmit: s.canSubmit, isSubmitting: s.isSubmitting })}> + {#snippet children({ canSubmit, isSubmitting })} +
+ + +
+ {/snippet} +
+
+
+
+ + diff --git a/packages/backend/convex/funcionarios.ts b/packages/backend/convex/funcionarios.ts index 753e307..2ff4155 100644 --- a/packages/backend/convex/funcionarios.ts +++ b/packages/backend/convex/funcionarios.ts @@ -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; }, }); diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 7bd5455..62bd2f0 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -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(), From f0d362504515c3bcdf219b11dbffc9be0eda67ed Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 25 Oct 2025 01:24:40 -0300 Subject: [PATCH 2/8] feat: add employee management features including filtering, deletion, and improved layout --- apps/web/src/app.html | 2 +- .../web/src/routes/(dashboard)/+layout.svelte | 2 +- .../funcionarios/+page.svelte | 172 ++++++++++++++++++ .../[funcionarioId]/editar/+page.svelte | 125 +++++++++---- .../funcionarios/cadastro/+page.svelte | 155 +++++++++++----- .../funcionarios/excluir/+page.svelte | 127 +++++++++++++ .../funcionarios/relatorios/+page.svelte | 154 ++++++++++++++++ .../recursos-humanos/simbolos/+page.svelte | 119 +++++++----- .../simbolos/cadastro/+page.svelte | 2 - packages/backend/convex/funcionarios.ts | 9 + 10 files changed, 735 insertions(+), 132 deletions(-) create mode 100644 apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/excluir/+page.svelte create mode 100644 apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/relatorios/+page.svelte diff --git a/apps/web/src/app.html b/apps/web/src/app.html index 77a5ff5..bd3affa 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/apps/web/src/routes/(dashboard)/+layout.svelte b/apps/web/src/routes/(dashboard)/+layout.svelte index 62b507c..1218fb8 100644 --- a/apps/web/src/routes/(dashboard)/+layout.svelte +++ b/apps/web/src/routes/(dashboard)/+layout.svelte @@ -5,7 +5,7 @@
{@render children()}
diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte index e69de29..b6b02d6 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/+page.svelte @@ -0,0 +1,172 @@ + + +
+
+

Funcionários

+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + {#each filtered as f} + + + + + + + + + + {/each} + +
NomeCPFMatrículaTipoCidadeUFAções
{f.nome}{f.cpf}{f.matricula}{f.simboloTipo}{f.cidade}{f.uf} + +
+
+ + + + + +
+ diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte index 1fc15be..8209cf0 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/[funcionarioId]/editar/+page.svelte @@ -15,7 +15,39 @@ 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 rgFormatByUF: Record = { + RJ: [2, 3, 2, 1], SP: [2, 3, 3, 1], MG: [2, 3, 3, 1], ES: [2, 3, 3, 1], + PR: [2, 3, 3, 1], SC: [2, 3, 3, 1], RS: [2, 3, 3, 1], BA: [2, 3, 3, 1], + PE: [2, 3, 3, 1], CE: [2, 3, 3, 1], PA: [2, 3, 3, 1], AM: [2, 3, 3, 1], + AC: [2, 3, 3, 1], AP: [2, 3, 3, 1], AL: [2, 3, 3, 1], RN: [2, 3, 3, 1], + PB: [2, 3, 3, 1], MA: [2, 3, 3, 1], PI: [2, 3, 3, 1], DF: [2, 3, 3, 1], + GO: [2, 3, 3, 1], MT: [2, 3, 3, 1], MS: [2, 3, 3, 1], RO: [2, 3, 3, 1], + RR: [2, 3, 3, 1], TO: [2, 3, 3, 1], + }; + function maskRGByUF(uf: string, v: string) { + const raw = (v || "").toUpperCase().replace(/[^0-9X]/g, ""); + const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1]; + const baseMax = a + b + c; + const baseDigits = raw.replace(/X/g, "").slice(0, baseMax); + const verifier = raw.slice(baseDigits.length, baseDigits.length + dv).slice(0, 1); + const g1 = baseDigits.slice(0, a); + const g2 = baseDigits.slice(a, a + b); + const g3 = baseDigits.slice(a + b, a + b + c); + let out = g1; + if (g2) out += `.${g2}`; + if (g3) out += `.${g3}`; + if (verifier) out += `-${verifier}`; + return out; + } + function padRGLeftByUF(uf: string, v: string) { + const raw = (v || "").toUpperCase().replace(/[^0-9X]/g, ""); + const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1]; + const baseMax = a + b + c; + let base = raw.replace(/X/g, ""); + const verifier = raw.slice(base.length, base.length + dv).slice(0, 1); + if (base.length < baseMax) base = base.padStart(baseMax, "0"); + return maskRGByUF(uf, base + (verifier || "")); + } 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); @@ -45,7 +77,7 @@ 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"), + rg: z.string().min(1).refine((v) => /^\d+$/.test(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"), @@ -128,7 +160,7 @@ 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("rg", (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); @@ -141,6 +173,20 @@ form.setFieldValue("admissaoData", (doc.admissaoData ?? "") as any); } + async function fillFromCEP(cepMasked: string) { + const cep = onlyDigits(cepMasked); + if (cep.length !== 8) return; + try { + const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`); + const data = await res.json(); + if (!data || data.erro) return; + const enderecoFull = [data.logradouro, data.bairro].filter(Boolean).join(", "); + form.setFieldValue("endereco", enderecoFull as any); + form.setFieldValue("cidade", (data.localidade || "") as any); + form.setFieldValue("uf", (data.uf || "") as any); + } catch {} + } + load(); @@ -177,6 +223,42 @@
+
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} onblur={(e) => fillFromCEP((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ + + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
{#snippet children({ name, state, handleChange })} @@ -190,7 +272,7 @@ {#snippet children({ name, state, handleChange })}
- { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required /> + { const t=e.target as HTMLInputElement; const v=onlyDigits(t.value); t.value=v; handleChange(v); }} required />
{/snippet}
@@ -234,41 +316,6 @@
-
- - {#snippet children({ name, state, handleChange })} -
- - { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required /> -
- {/snippet} -
- - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
- - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
-
- - - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte index 7b6545e..31b2073 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/cadastro/+page.svelte @@ -41,12 +41,58 @@ 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"); + const rgFormatByUF: Record = { + RJ: [2, 3, 2, 1], + SP: [2, 3, 3, 1], + MG: [2, 3, 3, 1], + ES: [2, 3, 3, 1], + PR: [2, 3, 3, 1], + SC: [2, 3, 3, 1], + RS: [2, 3, 3, 1], + BA: [2, 3, 3, 1], + PE: [2, 3, 3, 1], + CE: [2, 3, 3, 1], + PA: [2, 3, 3, 1], + AM: [2, 3, 3, 1], + AC: [2, 3, 3, 1], + AP: [2, 3, 3, 1], + AL: [2, 3, 3, 1], + RN: [2, 3, 3, 1], + PB: [2, 3, 3, 1], + MA: [2, 3, 3, 1], + PI: [2, 3, 3, 1], + DF: [2, 3, 3, 1], + GO: [2, 3, 3, 1], + MT: [2, 3, 3, 1], + MS: [2, 3, 3, 1], + RO: [2, 3, 3, 1], + RR: [2, 3, 3, 1], + TO: [2, 3, 3, 1], + }; + function maskRGByUF(uf: string, v: string) { + const raw = (v || "").toUpperCase().replace(/[^0-9X]/g, ""); + const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1]; + const baseMax = a + b + c; + const baseDigits = raw.replace(/X/g, "").slice(0, baseMax); + const verifier = raw.slice(baseDigits.length, baseDigits.length + dv).slice(0, 1); + const g1 = baseDigits.slice(0, a); + const g2 = baseDigits.slice(a, a + b); + const g3 = baseDigits.slice(a + b, a + b + c); + let out = g1; + if (g2) out += `.${g2}`; + if (g3) out += `.${g3}`; + if (verifier) out += `-${verifier}`; + return out; + } + + function padRGLeftByUF(uf: string, v: string) { + const raw = (v || "").toUpperCase().replace(/[^0-9X]/g, ""); + const [a, b, c, dv] = rgFormatByUF[uf?.toUpperCase()] ?? [2, 3, 3, 1]; + const baseMax = a + b + c; + let base = raw.replace(/X/g, ""); + const verifier = raw.slice(base.length, base.length + dv).slice(0, 1); + if (base.length < baseMax) base = base.padStart(baseMax, "0"); + return maskRGByUF(uf, base + (verifier || "")); } function maskCEP(v: string) { @@ -88,7 +134,7 @@ rg: z .string() .min(1, "Obrigatório") - .refine((v) => /^\d{2}\.\d{3}\.\d{3}-\d$/.test(maskRG(v)), "RG inválido"), + .refine((v) => /^\d+$/.test(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 @@ -161,9 +207,9 @@ 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." }; + 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." }; } } })); @@ -171,6 +217,20 @@ let notice: { kind: "success" | "error"; text: string } | null = null; loadSimbolos(); + + async function fillFromCEP(cepMasked: string) { + const cep = onlyDigits(cepMasked); + if (cep.length !== 8) return; + try { + const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`); + const data = await res.json(); + if (!data || data.erro) return; + const enderecoFull = [data.logradouro, data.bairro].filter(Boolean).join(", "); + form.setFieldValue("endereco", enderecoFull as any); + form.setFieldValue("cidade", (data.localidade || "") as any); + form.setFieldValue("uf", (data.uf || "") as any); + } catch {} + }
+
+ + {#snippet children({ name, state, handleChange })} +
+ + { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} onblur={(e) => fillFromCEP((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+ + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
+ + + {#snippet children({ name, state, handleChange })} +
+ + handleChange((e.target as HTMLInputElement).value)} required /> +
+ {/snippet} +
+
{#snippet children({ name, state, handleChange })} @@ -222,7 +318,7 @@ {#snippet children({ name, state, handleChange })}
- { const t=e.target as HTMLInputElement; const v=maskRG(t.value); t.value=v; handleChange(v); }} required /> + { const t=e.target as HTMLInputElement; const v=onlyDigits(t.value); t.value=v; handleChange(v); }} required />
{/snippet}
@@ -266,48 +362,13 @@
-
- - {#snippet children({ name, state, handleChange })} -
- - { const t=e.target as HTMLInputElement; const v=maskCEP(t.value); t.value=v; handleChange(v); }} required /> -
- {/snippet} -
- - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
- - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
-
- - - {#snippet children({ name, state, handleChange })} -
- - handleChange((e.target as HTMLInputElement).value)} required /> -
- {/snippet} -
{#snippet children({ name, state, handleChange })}
- handleChange((e.target as HTMLSelectElement).value as any)} required> diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/excluir/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/excluir/+page.svelte new file mode 100644 index 0000000..7edf9ea --- /dev/null +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/excluir/+page.svelte @@ -0,0 +1,127 @@ + + +
+ {#if notice} +
+ {notice.text} +
+ {/if} + +
+

Excluir Funcionários

+ +
+ +
+ + +
+ +
+ + + + + + + + + + + {#each list.filter((f) => { + const q = (filtro || "").toLowerCase(); + return !q || (f.nome || "").toLowerCase().includes(q) || (f.cpf || "").includes(q) || (f.matricula || "").toLowerCase().includes(q); + }) as f} + + + + + + + {/each} + +
NomeCPFMatrículaAções
{f.nome}{f.cpf}{f.matricula} + +
+
+ + + + +
+ + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/relatorios/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/relatorios/+page.svelte new file mode 100644 index 0000000..44e94ed --- /dev/null +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/funcionarios/relatorios/+page.svelte @@ -0,0 +1,154 @@ + + +
+ {#if notice} +
+ {notice.text} +
+ {/if} + +
+

Relatórios

+
+ + {#if isLoading} +
+ +
+ {:else} +
+ +
+
+

Símbolo x Salário (Valor total)

+
+ + {#if rows.length === 0} + Sem dados + {:else} + {@const max = getMax(rows, (r) => r.valor)} + + {#each rows as r, i} + + {r.nome} + + {/each} + + {#each rows as r, i} + (hover = { x: e.offsetX, y: e.offsetY - 8, text: `${r.nome}: R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` })} + onmouseleave={() => (hover = null)} + /> + {/each} + {#if hover} + +
{hover.text}
+
+ {/if} + {/if} +
+
+
+
+ + +
+
+

Quantidade de Funcionários por Símbolo

+
+ + {#if rows.length === 0} + Sem dados + {:else} + {@const maxC = getMax(rows, (r) => r.count)} + {#each rows as r, i} + + {r.nome} + + {/each} + {#each rows as r, i} + (hover = { x: e.offsetX, y: e.offsetY - 8, text: `${r.nome}: ${r.count} funcionário(s)` })} + onmouseleave={() => (hover = null)} + /> + {/each} + {#if hover} + +
{hover.text}
+
+ {/if} + {/if} +
+
+
+
+
+ {/if} +
+ + diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/simbolos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/simbolos/+page.svelte index 3025b7f..a059a35 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/simbolos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/simbolos/+page.svelte @@ -1,10 +1,37 @@
+ {#if notice} +
+ {notice.text} +
+ {/if}
- {#if simbolosQuery.isLoading} +
+
+ + +
+
+ + +
+
+ + +
+
+ + {#if isLoading}
- {:else if simbolosQuery.data && simbolosQuery.data.length > 0} -
+ {:else} +
@@ -84,8 +136,9 @@ - {#each simbolosQuery.data as simbolo} - + {#if filtered.length > 0} + {#each filtered as simbolo} + + + {/each} + {:else} + + - {/each} + {/if}
{simbolo.nome} {formatMoney(simbolo.valor)} {simbolo.descricao} -
Nenhum símbolo encontrado com os filtros atuais.
- {:else} -
- - - - Nenhum símbolo encontrado. Crie um novo para começar. -
{/if}
@@ -210,12 +245,12 @@ {/if} diff --git a/packages/backend/convex/funcionarios.ts b/packages/backend/convex/funcionarios.ts index 2ff4155..26c21c6 100644 --- a/packages/backend/convex/funcionarios.ts +++ b/packages/backend/convex/funcionarios.ts @@ -177,3 +177,12 @@ export const update = mutation({ return null; }, }); + +export const remove = mutation({ + args: { id: v.id("funcionarios") }, + returns: v.null(), + handler: async (ctx, args) => { + await ctx.db.delete(args.id); + return null; + }, +}); From 5dd00b63e175bb141f7d72fe62e82aa00ff17b3a Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 25 Oct 2025 07:53:08 -0300 Subject: [PATCH 3/8] refactor: update Sidebar and layout styles for improved responsiveness, adjust chart dimensions and remove tooltip functionality in employee reports --- apps/web/src/lib/components/Sidebar.svelte | 28 ++++----- .../web/src/routes/(dashboard)/+layout.svelte | 4 +- .../[funcionarioId]/editar/+page.svelte | 2 +- .../funcionarios/relatorios/+page.svelte | 63 +++++++++++-------- 4 files changed, 54 insertions(+), 43 deletions(-) diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 9e50b8c..b756b49 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -23,7 +23,7 @@ -