adiciona funcionarios pagina

This commit is contained in:
2025-10-24 08:53:15 -03:00
parent 9d17ad1271
commit aafee4b654
14 changed files with 564 additions and 9 deletions

View File

@@ -28,6 +28,7 @@
"@mmailaender/convex-better-auth-svelte": "^0.2.0",
"@sgse-app/backend": "workspace:*",
"@tanstack/svelte-form": "^1.19.2",
"better-auth": "^1.3.29",
"convex": "catalog:",
"convex-svelte": "^0.0.11",
"zod": "^4.0.17"

View File

@@ -1 +1,39 @@
<h1 class="text-2xl font-bold">Recursos Humanos</h1>
<script>
import { resolve } from "$app/paths";
</script>
<div class="space-y-4">
<h2 class="text-3xl font-bold text-brand-dark">Recursos Humanos</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
<h3 class="text-lg font-bold text-brand-dark col-span-4">Funcionários</h3>
<a
href={resolve("/recursos-humanos/funcionarios/cadastro")}
class="p-4 rounded-xl border hover:shadow bgbase-100"
>Cadastrar Funcionários</a
>
<a
href={resolve("/recursos-humanos/funcionarios/editar")}
class="p-4 rounded-xl border hover:shadow bgbase-100">Editar Cadastro</a
>
<a
href={resolve("/recursos-humanos/funcionarios/excluir")}
class="p-4 rounded-xl border hover:shadow bgbase-100">Excluir Cadastro</a
>
<a
href={resolve("/recursos-humanos/funcionarios/relatorios")}
class="p-4 rounded-xl border hover:shadow bgbase-100">Relatórios</a
>
<h3 class="text-lg font-bold text-brand-dark col-span-4">Simbolos</h3>
<a
href={resolve("/recursos-humanos/simbolos/cadastro")}
class="p-4 rounded-xl border hover:shadow bgbase-100"
>Cadastrar Simbolos</a
>
<a
href={resolve("/recursos-humanos/simbolos")}
class="p-4 rounded-xl border hover:shadow bgbase-100">Listar Simbolos</a
>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
const simbolosQuery = useQuery(api.simbolos.getAll, {});
</script>
<div class="space-y-4">
<h2 class="text-3xl font-bold text-brand-dark">Simbolos</h2>
{#each simbolosQuery.data as simbolo}
<div class="p-4 rounded-xl border hover:shadow bgbase-100">
<h3 class="text-lg font-bold text-brand-dark">{simbolo.nome}</h3>
<p class="text-sm text-gray-500">{simbolo.vencValor}</p>
<p class="text-sm text-gray-500">{simbolo.repValor}</p>
<p class="text-sm text-gray-500">{simbolo.descricao}</p>
</div>
{:else}
<p>Nenhum simbolo encontrado</p>
{/each}
</div>

View File

@@ -0,0 +1,381 @@
<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 { Plus } from "lucide-svelte";
import { goto } from "$app/navigation";
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
const client = useConvexClient();
let tipo = $state<SimboloTipo>("cargo_comissionado");
const defaultValues = {
nome: "",
refValor: "",
vencValor: "",
descricao: "",
tipo: "cargo_comissionado",
valor: "",
};
const schema = z.object({
nome: z.string().min(1),
refValor: z.string().optional(),
vencValor: z.string().optional(),
descricao: z.string().min(1),
tipo: z.enum(["cargo_comissionado", "funcao_gratificada"]),
valor: z.string().optional(),
});
function formatCurrencyBR(raw: string): string {
const digits = (raw || "").replace(/\D/g, "");
if (!digits) return "";
const number = parseInt(digits, 10);
const cents = (number / 100).toFixed(2);
const [intPart, decPart] = cents.split(".");
const intWithThousands = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
return `${intWithThousands},${decPart}`;
}
function unmaskCurrencyToDotDecimal(masked: string): string {
if (!masked) return "";
const digits = masked.replace(/\D/g, "");
if (!digits) return "";
const value = (parseInt(digits, 10) / 100).toFixed(2);
return value;
}
function formatDotDecimalToBR(value: string): string {
if (!value) return "";
const [intPart, decRaw] = value.split(".");
const intDigits = (intPart || "0").replace(/\D/g, "");
const intWithThousands = intDigits.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
const decPart = (decRaw ?? "00").padEnd(2, "0").slice(0, 2);
return `${intWithThousands},${decPart}`;
}
let notice = $state<{ kind: "success" | "error"; text: string } | null>(null);
function getTotalPreview(): string {
if (tipo !== "cargo_comissionado") return "";
const r = unmaskCurrencyToDotDecimal(form.getFieldValue("refValor"));
const v = unmaskCurrencyToDotDecimal(form.getFieldValue("vencValor"));
if (!r || !v) return "";
const sum = (Number(r) + Number(v)).toFixed(2);
return formatDotDecimalToBR(sum);
}
const form = createForm(() => ({
onSubmit: async ({ value, formApi }) => {
const isCargo = value.tipo === "cargo_comissionado";
const payload = {
nome: value.nome,
refValor: isCargo ? unmaskCurrencyToDotDecimal(value.refValor) : "",
vencValor: isCargo ? unmaskCurrencyToDotDecimal(value.vencValor) : "",
descricao: value.descricao,
tipo: value.tipo as SimboloTipo,
valor: !isCargo ? unmaskCurrencyToDotDecimal(value.valor) : undefined,
};
const res = await client.mutation(api.simbolos.create, payload);
if (res) {
formApi.reset();
notice = { kind: "success", text: "Símbolo cadastrado com sucesso." };
setTimeout(() => goto("/recursos-humanos/simbolos"), 600);
} else {
console.log("erro ao registrar cliente");
notice = { kind: "error", text: "Erro ao cadastrar símbolo." };
}
},
defaultValues,
}));
</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 Símbolos</h2>
<p class="opacity-70">
Preencha os campos abaixo para cadastrar um novo símbolo.
</p>
</div>
<form.Field name="nome" validators={{ onChange: schema.shape.nome }}>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium"
>Símbolo <span class="text-error">*</span></span
>
</label>
<input
{name}
value={state.value}
placeholder="Ex.: DAS-1"
class="input input-bordered w-full"
autocomplete="off"
oninput={(e) => {
const target = e.target as HTMLInputElement;
const value = target.value;
handleChange(value);
}}
required
aria-required="true"
/>
<div class="label">
<span class="label-text-alt opacity-60"
>Informe o nome identificador do símbolo.</span
>
</div>
</div>
{/snippet}
</form.Field>
<form.Field
name="descricao"
validators={{ onChange: schema.shape.descricao }}
>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="descricao">
<span class="label-text font-medium"
>Descrição <span class="text-error">*</span></span
>
</label>
<input
{name}
value={state.value}
placeholder="Ex.: Cargo de Apoio 1"
class="input input-bordered w-full"
autocomplete="off"
oninput={(e) => {
const target = e.target as HTMLInputElement;
const value = target.value;
handleChange(value);
}}
required
aria-required="true"
/>
<div class="label">
<span class="label-text-alt opacity-60"
>Descreva brevemente o símbolo.</span
>
</div>
</div>
{/snippet}
</form.Field>
<form.Field
name="tipo"
validators={{
onChange: ({ value }) => (value ? undefined : "Obrigatório"),
}}
>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="tipo">
<span class="label-text font-medium"
>type <span class="text-error">*</span></span
>
</label>
<select
{name}
class="select select-bordered w-full"
value={tipo}
oninput={(e) => {
const target = e.target as HTMLSelectElement;
const value = target.value;
handleChange(value);
}}
required
aria-required="true"
>
<option value="cargo_comissionado">Cargo comissionado</option>
<option value="funcao_gratificada">Função gratificada</option>
</select>
</div>
{/snippet}
</form.Field>
{#if tipo === "cargo_comissionado"}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<form.Field
name="vencValor"
validators={{
onChange: ({ value }) =>
form.getFieldValue("tipo") === "cargo_comissionado" && !value
? "Obrigatório"
: undefined,
}}
>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="vencValor">
<span class="label-text font-medium"
>Valor de Vencimento <span class="text-error">*</span></span
>
</label>
<input
{name}
value={state.value}
placeholder="Ex.: 1200,00"
class="input input-bordered w-full"
inputmode="decimal"
autocomplete="off"
oninput={(e) => {
const target = e.target as HTMLInputElement;
const formatted = formatCurrencyBR(target.value);
target.value = formatted;
handleChange(formatted);
}}
required
aria-required="true"
/>
<div class="label">
<span class="label-text-alt opacity-60"
>Valor efetivo de vencimento.</span
>
</div>
</div>
{/snippet}
</form.Field>
<form.Field
name="refValor"
validators={{
onChange: ({ value }) =>
form.getFieldValue("tipo") === "cargo_comissionado" && !value
? "Obrigatório"
: undefined,
}}
>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="refValor">
<span class="label-text font-medium"
>Valor de Referência <span class="text-error">*</span></span
>
</label>
<input
{name}
value={state.value}
placeholder="Ex.: 1000,00"
class="input input-bordered w-full"
inputmode="decimal"
autocomplete="off"
oninput={(e) => {
const target = e.target as HTMLInputElement;
const formatted = formatCurrencyBR(target.value);
target.value = formatted;
handleChange(formatted);
}}
required
aria-required="true"
/>
<div class="label">
<span class="label-text-alt opacity-60"
>Valor base de referência.</span
>
</div>
</div>
{/snippet}
</form.Field>
</div>
{#if getTotalPreview()}
<div class="alert bg-base-200">
<span>Total previsto: R$ {getTotalPreview()}</span>
</div>
{/if}
{:else}
<form.Field
name="valor"
validators={{
onChange: ({ value }) =>
form.getFieldValue("tipo") === "funcao_gratificada" && !value
? "Obrigatório"
: undefined,
}}
>
{#snippet children({ name, state, handleChange })}
<div class="form-control">
<label class="label" for="valor">
<span class="label-text font-medium"
>Valor <span class="text-error">*</span></span
>
</label>
<input
{name}
value={state.value}
placeholder="Ex.: 1.500,00"
class="input input-bordered w-full"
inputmode="decimal"
autocomplete="off"
oninput={(e) => {
const target = e.target as HTMLInputElement;
const formatted = formatCurrencyBR(target.value);
target.value = formatted;
handleChange(formatted);
}}
required
aria-required="true"
/>
<div class="label">
<span class="label-text-alt opacity-60"
>Informe o valor da função gratificada.</span
>
</div>
</div>
{/snippet}
</form.Field>
{/if}
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.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/simbolos")}
>
Cancelar
</button>
<button
type="submit"
class="btn btn-primary"
class:loading={isSubmitting}
disabled={isSubmitting || !canSubmit}
>
<Plus class="h-5 w-5" />
<span>Cadastrar Símbolo</span>
</button>
</div>
{/snippet}
</form.Subscribe>
</div>
</div>
</form>

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import "../app.css";
import Header from "$lib/components/Header.svelte";
import Sidebar from "$lib/components/Sidebar.svelte";
import { PUBLIC_CONVEX_URL } from "$env/static/public";
import { setupConvex } from "convex-svelte";
import { createSvelteAuthClient } from "@mmailaender/convex-better-auth-svelte/svelte";
import { authClient } from "$lib/auth";
const { children } = $props();
setupConvex(PUBLIC_CONVEX_URL);
createSvelteAuthClient({ authClient });
</script>
<div>