adiciona funcionarios pagina
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user