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; + }, +});