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>

View File

@@ -3,6 +3,10 @@
"workspaces": {
"": {
"name": "sgse-app",
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"lucide-svelte": "^0.546.0",
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"turbo": "^2.5.4",
@@ -16,6 +20,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",
@@ -67,6 +72,8 @@
"packages": {
"@better-auth/core": ["@better-auth/core@1.3.27", "", { "dependencies": { "better-call": "1.0.19", "zod": "^4.1.5" } }, "sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.3.29", "", { "dependencies": { "@better-auth/core": "1.3.29", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" } }, "sha512-1BFh3YulYDrwWcUkfEWddcrcApACyI4wtrgq3NBd9y+tilBRjWTCWEPuRqJrfM3a5F1ZSqsvOYfFG1XZbkxlVw=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
@@ -285,11 +292,13 @@
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.3", "", {}, "sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg=="],
"@tanstack/form-core": ["@tanstack/form-core@1.24.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/store": "^0.7.7" } }, "sha512-e+HzSD49NWr4aIqJWtPPzmi+/phBJAP3nSPN8dvxwmJWqAxuB/cH138EcmCFf3+oA7j3BXvwvTY0I+8UweGPjQ=="],
"@tanstack/form-core": ["@tanstack/form-core@1.24.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w=="],
"@tanstack/pacer": ["@tanstack/pacer@0.15.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/store": "^0.7.5" } }, "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg=="],
"@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
"@tanstack/svelte-form": ["@tanstack/svelte-form@1.23.7", "", { "dependencies": { "@tanstack/form-core": "1.24.3", "@tanstack/svelte-store": "^0.7.7" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-OrUH1nR+icLm3fTgyHj34fHHZS9F2o+Z/7a/EJE3PhH4YV3XOOmc/okes28UVeWypuBv7Iu/vCc9cEAD/6ltpA=="],
"@tanstack/svelte-form": ["@tanstack/svelte-form@1.23.8", "", { "dependencies": { "@tanstack/form-core": "1.24.4", "@tanstack/svelte-store": "^0.7.7" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-ZH17T/gOQ9sBpI/38zBCBiuceLsa9c9rOgwB7CRt/FBFunIkaG2gY02IiUBpjZfm1fiKBcTryaJGfR3XAtIH/g=="],
"@tanstack/svelte-store": ["@tanstack/svelte-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-JeDyY7SxBi6EKzkf2wWoghdaC2bvmwNL9X/dgkx7LKEvJVle+te7tlELI3cqRNGbjXt9sx+97jx9M5dCCHcuog=="],
@@ -389,6 +398,8 @@
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"lucide-svelte": ["lucide-svelte@0.546.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-vCvBUlFapD59ivX1b/i7wdUadSgC/3gQGvrGEZjSecOlThT+UR+X5UxdVEakHuhniTrSX0nJ2WrY5r25SVDtyQ=="],
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
@@ -487,6 +498,8 @@
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
"@better-auth/telemetry/@better-auth/core": ["@better-auth/core@1.3.29", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-Ka2mg4qZACFaLY7DOGFXv1Ma8CkF17k0ClUd2U/ZJbbSoEPI5gnVguEmakJB6HFYswszeZh2295IFORtW9wf7A=="],
"@convex-dev/better-auth/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
@@ -505,6 +518,8 @@
"vite/esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
"web/better-auth": ["better-auth@1.3.29", "", { "dependencies": { "@better-auth/core": "1.3.29", "@better-auth/telemetry": "1.3.29", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-1va1XZLTQme3DX33PgHqwwVyOJya5H0+ozT6BhOjTnwecC50I75F0OqqTwINq4XZ0+GuD3bl3I55RiFP49jStw=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="],
@@ -554,5 +569,7 @@
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="],
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="],
"web/better-auth/@better-auth/core": ["@better-auth/core@1.3.29", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-Ka2mg4qZACFaLY7DOGFXv1Ma8CkF17k0ClUd2U/ZJbbSoEPI5gnVguEmakJB6HFYswszeZh2295IFORtW9wf7A=="],
}
}

View File

@@ -27,5 +27,9 @@
"turbo": "^2.5.4",
"@biomejs/biome": "^2.2.0"
},
"packageManager": "bun@1.3.0"
"packageManager": "bun@1.3.0",
"dependencies": {
"@tanstack/svelte-form": "^1.23.8",
"lucide-svelte": "^0.546.0"
}
}

View File

@@ -13,8 +13,10 @@ import type * as betterAuth__generated_api from "../betterAuth/_generated/api.js
import type * as betterAuth__generated_server from "../betterAuth/_generated/server.js";
import type * as betterAuth_adapter from "../betterAuth/adapter.js";
import type * as betterAuth_auth from "../betterAuth/auth.js";
import type * as funcionarios from "../funcionarios.js";
import type * as healthCheck from "../healthCheck.js";
import type * as http from "../http.js";
import type * as simbolos from "../simbolos.js";
import type * as todos from "../todos.js";
import type {
@@ -37,8 +39,10 @@ declare const fullApi: ApiFromModules<{
"betterAuth/_generated/server": typeof betterAuth__generated_server;
"betterAuth/adapter": typeof betterAuth_adapter;
"betterAuth/auth": typeof betterAuth_auth;
funcionarios: typeof funcionarios;
healthCheck: typeof healthCheck;
http: typeof http;
simbolos: typeof simbolos;
todos: typeof todos;
}>;
declare const fullApiWithMounts: typeof fullApi;

View File

@@ -0,0 +1,24 @@
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
export const getAll = query({
handler: async (ctx) => {
return await ctx.db.query("funcionarios").collect();
},
});
export const create = mutation({
args: {
nome: v.string(),
matricula: v.string(),
simboloId: v.id("simbolos"),
},
handler: async (ctx, args) => {
const novoFuncionarioId = await ctx.db.insert("funcionarios", {
nome: args.nome,
matricula: args.matricula,
simboloId: args.simboloId,
});
return await ctx.db.get(novoFuncionarioId);
},
});

View File

@@ -1,7 +1,13 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { Infer, v } from "convex/values";
import { tables } from "./betterAuth/schema";
export const simboloTipo = v.union(
v.literal("cargo_comissionado"),
v.literal("funcao_gratificada")
);
export type SimboloTipo = Infer<typeof simboloTipo>;
export default defineSchema({
...tables,
todos: defineTable({
@@ -20,13 +26,21 @@ export default defineSchema({
telefone: v.optional(v.string()),
email: v.optional(v.string()),
matricula: v.string(),
simbolo: v.optional(v.string()),
vencimento: v.optional(v.string()),
admissao: v.optional(v.string()),
desligamento: v.optional(v.string()),
ferias: v.optional(v.string()),
atestado: v.optional(v.string()),
simboloId: v.id("simbolos"),
})
.index("by_matricula", ["matricula"])
.index("by_nome", ["nome"]),
simbolos: defineTable({
nome: v.string(),
tipo: simboloTipo,
descricao: v.string(),
vencValor: v.string(),
repValor: v.string(),
valor: v.string(),
}),
});

View File

@@ -0,0 +1,50 @@
import { v } from "convex/values";
import { query, mutation } from "./_generated/server";
import { simboloTipo } from "./schema";
export const getAll = query({
handler: async (ctx) => {
return await ctx.db.query("simbolos").collect();
},
});
export const create = mutation({
args: {
nome: v.string(),
tipo: simboloTipo,
refValor: v.string(),
vencValor: v.string(),
descricao: v.string(),
valor: v.optional(v.string()),
},
handler: async (ctx, args) => {
let refValor = args.refValor;
let vencValor = args.vencValor;
let valor = args.valor ?? "";
if (args.tipo === "cargo_comissionado") {
if (!refValor || !vencValor) {
throw new Error(
"Valor de referência e valor de vencimento são obrigatórios para cargo comissionado"
);
}
valor = (Number(refValor) + Number(vencValor)).toFixed(2);
} else {
if (!args.valor) {
throw new Error("Valor é obrigatório para função gratificada");
}
refValor = "";
vencValor = "";
valor = args.valor;
}
const novoSimboloId = await ctx.db.insert("simbolos", {
nome: args.nome,
descricao: args.descricao,
repValor: refValor,
vencValor: vencValor,
tipo: args.tipo,
valor,
});
return await ctx.db.get(novoSimboloId);
},
});