refactor: streamline Svelte components and enhance user feedback
- Refactored multiple Svelte components, including AprovarAusencias, AprovarFerias, and ErrorModal, to improve code clarity and maintainability. - Updated modal interactions and error handling messages for better user feedback. - Cleaned up component structures and standardized formatting for consistency across the codebase. - Enhanced styling and layout adjustments to align with the new design system.
This commit is contained in:
@@ -1,359 +1,440 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { onMount } from "svelte";
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
let isLoading = true;
|
||||
let list: Array<any> = [];
|
||||
let filtroNome = "";
|
||||
let filtroTipo: "" | "cargo_comissionado" | "funcao_gratificada" = "";
|
||||
let filtroDescricao = "";
|
||||
let filtered: Array<any> = [];
|
||||
let notice: { kind: "success" | "error"; text: string } | null = null;
|
||||
$: needsScroll = filtered.length > 8;
|
||||
let openMenuId: string | null = null;
|
||||
function toggleMenu(id: string) {
|
||||
openMenuId = openMenuId === id ? null : id;
|
||||
}
|
||||
$: filtered = (list ?? []).filter((s) => {
|
||||
const nome = (filtroNome || "").toLowerCase();
|
||||
const desc = (filtroDescricao || "").toLowerCase();
|
||||
const okNome = !nome || (s.nome || "").toLowerCase().includes(nome);
|
||||
const okDesc = !desc || (s.descricao || "").toLowerCase().includes(desc);
|
||||
const okTipo = !filtroTipo || s.tipo === filtroTipo;
|
||||
return okNome && okDesc && okTipo;
|
||||
});
|
||||
onMount(async () => {
|
||||
try {
|
||||
list = await client.query(api.simbolos.getAll, {} as any);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
const client = useConvexClient();
|
||||
let isLoading = true;
|
||||
let list: Array<any> = [];
|
||||
let filtroNome = '';
|
||||
let filtroTipo: '' | 'cargo_comissionado' | 'funcao_gratificada' = '';
|
||||
let filtroDescricao = '';
|
||||
let filtered: Array<any> = [];
|
||||
let notice: { kind: 'success' | 'error'; text: string } | null = null;
|
||||
$: needsScroll = filtered.length > 8;
|
||||
let openMenuId: string | null = null;
|
||||
function toggleMenu(id: string) {
|
||||
openMenuId = openMenuId === id ? null : id;
|
||||
}
|
||||
$: filtered = (list ?? []).filter((s) => {
|
||||
const nome = (filtroNome || '').toLowerCase();
|
||||
const desc = (filtroDescricao || '').toLowerCase();
|
||||
const okNome = !nome || (s.nome || '').toLowerCase().includes(nome);
|
||||
const okDesc = !desc || (s.descricao || '').toLowerCase().includes(desc);
|
||||
const okTipo = !filtroTipo || s.tipo === filtroTipo;
|
||||
return okNome && okDesc && okTipo;
|
||||
});
|
||||
onMount(async () => {
|
||||
try {
|
||||
list = await client.query(api.simbolos.getAll, {} as any);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
let deletingId: Id<"simbolos"> | null = null;
|
||||
let simboloToDelete: { id: Id<"simbolos">; nome: string } | null = null;
|
||||
let deletingId: Id<'simbolos'> | null = null;
|
||||
let simboloToDelete: { id: Id<'simbolos'>; nome: string } | null = null;
|
||||
|
||||
function openDeleteModal(id: Id<"simbolos">, nome: string) {
|
||||
simboloToDelete = { id, nome };
|
||||
(document.getElementById("delete_modal") as HTMLDialogElement)?.showModal();
|
||||
}
|
||||
function openDeleteModal(id: Id<'simbolos'>, nome: string) {
|
||||
simboloToDelete = { id, nome };
|
||||
(document.getElementById('delete_modal') as HTMLDialogElement)?.showModal();
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
simboloToDelete = null;
|
||||
(document.getElementById("delete_modal") as HTMLDialogElement)?.close();
|
||||
}
|
||||
function closeDeleteModal() {
|
||||
simboloToDelete = null;
|
||||
(document.getElementById('delete_modal') as HTMLDialogElement)?.close();
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!simboloToDelete) return;
|
||||
try {
|
||||
deletingId = simboloToDelete.id;
|
||||
await client.mutation(api.simbolos.remove, { id: simboloToDelete.id });
|
||||
// reload list
|
||||
list = await client.query(api.simbolos.getAll, {} as any);
|
||||
notice = { kind: "success", text: "Símbolo excluído com sucesso." };
|
||||
closeDeleteModal();
|
||||
} catch (error) {
|
||||
notice = { kind: "error", text: "Erro ao excluir símbolo." };
|
||||
} finally {
|
||||
deletingId = null;
|
||||
}
|
||||
}
|
||||
async function confirmDelete() {
|
||||
if (!simboloToDelete) return;
|
||||
try {
|
||||
deletingId = simboloToDelete.id;
|
||||
await client.mutation(api.simbolos.remove, { id: simboloToDelete.id });
|
||||
// reload list
|
||||
list = await client.query(api.simbolos.getAll, {} as any);
|
||||
notice = { kind: 'success', text: 'Símbolo excluído com sucesso.' };
|
||||
closeDeleteModal();
|
||||
} catch (error) {
|
||||
notice = { kind: 'error', text: 'Erro ao excluir símbolo.' };
|
||||
} finally {
|
||||
deletingId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMoney(value: string) {
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return "R$ 0,00";
|
||||
return `R$ ${num.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
function formatMoney(value: string) {
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return 'R$ 0,00';
|
||||
return `R$ ${num.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function getTipoLabel(tipo: string) {
|
||||
return tipo === "cargo_comissionado" ? "Cargo Comissionado" : "Função Gratificada";
|
||||
}
|
||||
function getTipoLabel(tipo: string) {
|
||||
return tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada';
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-4">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>Símbolos</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>Símbolos</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-green-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Símbolos Cadastrados</h1>
|
||||
<p class="text-base-content/70">Gerencie cargos comissionados e funções gratificadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/recursos-humanos/simbolos/cadastro" class="btn btn-primary btn-lg gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Novo Símbolo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-xl bg-green-500/20 p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-primary text-3xl font-bold">Símbolos Cadastrados</h1>
|
||||
<p class="text-base-content/70">Gerencie cargos comissionados e funções gratificadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/recursos-humanos/simbolos/cadastro" class="btn btn-primary btn-lg gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Novo Símbolo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div class="alert mb-6 shadow-lg" class:alert-success={notice.kind === "success"} class:alert-error={notice.kind === "error"}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div
|
||||
class="alert mb-6 shadow-lg"
|
||||
class:alert-success={notice.kind === 'success'}
|
||||
class:alert-error={notice.kind === 'error'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if notice.kind === 'success'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 shadow-xl mb-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
Filtros de Pesquisa
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_nome">
|
||||
<span class="label-text font-semibold">Nome do Símbolo</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_nome"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar por nome..."
|
||||
bind:value={filtroNome}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_tipo">
|
||||
<span class="label-text font-semibold">Tipo</span>
|
||||
</label>
|
||||
<select id="symbol_tipo" class="select select-bordered focus:select-primary" bind:value={filtroTipo}>
|
||||
<option value="">Todos os tipos</option>
|
||||
<option value="cargo_comissionado">Cargo Comissionado</option>
|
||||
<option value="funcao_gratificada">Função Gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_desc">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_desc"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar na descrição..."
|
||||
bind:value={filtroDescricao}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if filtroNome || filtroTipo || filtroDescricao}
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm gap-2"
|
||||
onclick={() => {
|
||||
filtroNome = "";
|
||||
filtroTipo = "";
|
||||
filtroDescricao = "";
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 mb-6 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4 text-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||
/>
|
||||
</svg>
|
||||
Filtros de Pesquisa
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_nome">
|
||||
<span class="label-text font-semibold">Nome do Símbolo</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_nome"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar por nome..."
|
||||
bind:value={filtroNome}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_tipo">
|
||||
<span class="label-text font-semibold">Tipo</span>
|
||||
</label>
|
||||
<select
|
||||
id="symbol_tipo"
|
||||
class="select select-bordered focus:select-primary"
|
||||
bind:value={filtroTipo}
|
||||
>
|
||||
<option value="">Todos os tipos</option>
|
||||
<option value="cargo_comissionado">Cargo Comissionado</option>
|
||||
<option value="funcao_gratificada">Função Gratificada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="symbol_desc">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<input
|
||||
id="symbol_desc"
|
||||
class="input input-bordered focus:input-primary"
|
||||
placeholder="Buscar na descrição..."
|
||||
bind:value={filtroDescricao}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if filtroNome || filtroTipo || filtroDescricao}
|
||||
<div class="mt-4">
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
onclick={() => {
|
||||
filtroNome = '';
|
||||
filtroTipo = '';
|
||||
filtroDescricao = '';
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Limpar Filtros
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Tabela de Símbolos -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-y-auto" style="max-height: {filtered.length > 8 ? '600px' : 'none'};">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="sticky top-0 bg-base-200 z-10">
|
||||
<tr>
|
||||
<th class="font-bold">Nome</th>
|
||||
<th class="font-bold">Tipo</th>
|
||||
<th class="font-bold">Valor Referência</th>
|
||||
<th class="font-bold">Valor Vencimento</th>
|
||||
<th class="font-bold">Valor Total</th>
|
||||
<th class="font-bold">Descrição</th>
|
||||
<th class="text-right font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if filtered.length > 0}
|
||||
{#each filtered as simbolo}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{simbolo.nome}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
class:badge-primary={simbolo.tipo === "cargo_comissionado"}
|
||||
class:badge-secondary={simbolo.tipo === "funcao_gratificada"}
|
||||
>
|
||||
{getTipoLabel(simbolo.tipo)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{simbolo.repValor ? formatMoney(simbolo.repValor) : "—"}</td>
|
||||
<td>{simbolo.vencValor ? formatMoney(simbolo.vencValor) : "—"}</td>
|
||||
<td class="font-semibold">{formatMoney(simbolo.valor)}</td>
|
||||
<td class="max-w-xs truncate">{simbolo.descricao}</td>
|
||||
<td class="text-right">
|
||||
<div class="dropdown dropdown-end" class:dropdown-open={openMenuId === simbolo._id}>
|
||||
<button type="button" class="btn btn-ghost btn-sm" onclick={() => toggleMenu(simbolo._id)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="dropdown-content menu bg-base-100 rounded-box z-10 w-52 p-2 shadow-lg border border-base-300">
|
||||
<li>
|
||||
<a href={"/recursos-humanos/simbolos/" + simbolo._id + "/editar"}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"
|
||||
/>
|
||||
</svg>
|
||||
Editar
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" onclick={() => openDeleteModal(simbolo._id, simbolo.nome)} class="text-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Excluir
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center opacity-70 py-8">Nenhum símbolo encontrado com os filtros atuais.</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre resultados -->
|
||||
<div class="mt-4 text-sm text-base-content/70 text-center">
|
||||
Exibindo {filtered.length} de {list.length} símbolo(s)
|
||||
</div>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Tabela de Símbolos -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<div
|
||||
class="overflow-y-auto"
|
||||
style="max-height: {filtered.length > 8 ? '600px' : 'none'};"
|
||||
>
|
||||
<table class="table-zebra table w-full">
|
||||
<thead class="bg-base-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="font-bold">Nome</th>
|
||||
<th class="font-bold">Tipo</th>
|
||||
<th class="font-bold">Valor Referência</th>
|
||||
<th class="font-bold">Valor Vencimento</th>
|
||||
<th class="font-bold">Valor Total</th>
|
||||
<th class="font-bold">Descrição</th>
|
||||
<th class="text-right font-bold">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if filtered.length > 0}
|
||||
{#each filtered as simbolo}
|
||||
<tr class="hover">
|
||||
<td class="font-medium">{simbolo.nome}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
class:badge-primary={simbolo.tipo === 'cargo_comissionado'}
|
||||
class:badge-secondary={simbolo.tipo === 'funcao_gratificada'}
|
||||
>
|
||||
{getTipoLabel(simbolo.tipo)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{simbolo.repValor ? formatMoney(simbolo.repValor) : '—'}</td>
|
||||
<td>{simbolo.vencValor ? formatMoney(simbolo.vencValor) : '—'}</td>
|
||||
<td class="font-semibold">{formatMoney(simbolo.valor)}</td>
|
||||
<td class="max-w-xs truncate">{simbolo.descricao}</td>
|
||||
<td class="text-right">
|
||||
<div
|
||||
class="dropdown dropdown-end"
|
||||
class:dropdown-open={openMenuId === simbolo._id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
onclick={() => toggleMenu(simbolo._id)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-10 w-52 border p-2 shadow-lg"
|
||||
>
|
||||
<li>
|
||||
<a href={'/recursos-humanos/simbolos/' + simbolo._id + '/editar'}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"
|
||||
/>
|
||||
</svg>
|
||||
Editar
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openDeleteModal(simbolo._id, simbolo.nome)}
|
||||
class="text-error"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Excluir
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="7" class="py-8 text-center opacity-70"
|
||||
>Nenhum símbolo encontrado com os filtros atuais.</td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre resultados -->
|
||||
<div class="text-base-content/70 mt-4 text-center text-sm">
|
||||
Exibindo {filtered.length} de {list.length} símbolo(s)
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
<dialog id="delete_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Confirmar Exclusão</h3>
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Esta ação não pode ser desfeita!</span>
|
||||
</div>
|
||||
{#if simboloToDelete}
|
||||
<p class="py-2">
|
||||
Tem certeza que deseja excluir o símbolo <strong class="text-error"
|
||||
>{simboloToDelete.nome}</strong
|
||||
>?
|
||||
</p>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-2">
|
||||
<button class="btn btn-ghost" onclick={closeDeleteModal} type="button">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={confirmDelete}
|
||||
disabled={deletingId !== null}
|
||||
type="button"
|
||||
>
|
||||
{#if deletingId}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Excluindo...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Confirmar Exclusão
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
<div class="modal-box">
|
||||
<h3 class="mb-4 text-lg font-bold">Confirmar Exclusão</h3>
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Esta ação não pode ser desfeita!</span>
|
||||
</div>
|
||||
{#if simboloToDelete}
|
||||
<p class="py-2">
|
||||
Tem certeza que deseja excluir o símbolo <strong class="text-error"
|
||||
>{simboloToDelete.nome}</strong
|
||||
>?
|
||||
</p>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<form method="dialog" class="flex gap-2">
|
||||
<button class="btn" onclick={closeDeleteModal} type="button"> Cancelar </button>
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={confirmDelete}
|
||||
disabled={deletingId !== null}
|
||||
type="button"
|
||||
>
|
||||
{#if deletingId}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Excluindo...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Confirmar Exclusão
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -1,431 +1,415 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { createForm } from "@tanstack/svelte-form";
|
||||
import z from "zod";
|
||||
import { Save } from "lucide-svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { createForm } from '@tanstack/svelte-form';
|
||||
import z from 'zod';
|
||||
import { Save } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { SimboloTipo } from '@sgse-app/backend/convex/schema';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
const client = useConvexClient();
|
||||
const simboloId = $page.params.simboloId as Id<"simbolos">;
|
||||
const client = useConvexClient();
|
||||
const simboloId = $page.params.simboloId as Id<'simbolos'>;
|
||||
|
||||
const simboloQuery = useQuery(api.simbolos.getById, { id: simboloId });
|
||||
const simboloQuery = useQuery(api.simbolos.getById, { id: simboloId });
|
||||
|
||||
let tipo = $state<SimboloTipo>("cargo_comissionado");
|
||||
let isFormInitialized = $state(false);
|
||||
let tipo = $state<SimboloTipo>('cargo_comissionado');
|
||||
let isFormInitialized = $state(false);
|
||||
|
||||
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(),
|
||||
});
|
||||
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 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 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}`;
|
||||
}
|
||||
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);
|
||||
}
|
||||
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const form = createForm(() => ({
|
||||
onSubmit: async ({ value }) => {
|
||||
const isCargo = value.tipo === "cargo_comissionado";
|
||||
const payload = {
|
||||
id: simboloId,
|
||||
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,
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
await client.mutation(api.simbolos.update, payload);
|
||||
notice = { kind: "success", text: "Símbolo atualizado com sucesso." };
|
||||
setTimeout(() => goto("/recursos-humanos/simbolos"), 600);
|
||||
} catch (error) {
|
||||
console.error("erro ao atualizar símbolo", error);
|
||||
notice = { kind: "error", text: "Erro ao atualizar símbolo." };
|
||||
}
|
||||
},
|
||||
defaultValues: {
|
||||
nome: "",
|
||||
refValor: "",
|
||||
vencValor: "",
|
||||
descricao: "",
|
||||
tipo: "cargo_comissionado",
|
||||
valor: "",
|
||||
},
|
||||
}));
|
||||
const form = createForm(() => ({
|
||||
onSubmit: async ({ value }) => {
|
||||
const isCargo = value.tipo === 'cargo_comissionado';
|
||||
const payload = {
|
||||
id: simboloId,
|
||||
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
|
||||
};
|
||||
|
||||
// Inicializar o formulário quando os dados estiverem disponíveis
|
||||
$effect(() => {
|
||||
if (simboloQuery.data && !isFormInitialized) {
|
||||
const simbolo = simboloQuery.data;
|
||||
|
||||
tipo = simbolo.tipo;
|
||||
|
||||
form.setFieldValue("nome", simbolo.nome);
|
||||
form.setFieldValue("descricao", simbolo.descricao);
|
||||
form.setFieldValue("tipo", simbolo.tipo);
|
||||
|
||||
if (simbolo.tipo === "cargo_comissionado") {
|
||||
form.setFieldValue("refValor", formatDotDecimalToBR(simbolo.repValor));
|
||||
form.setFieldValue("vencValor", formatDotDecimalToBR(simbolo.vencValor));
|
||||
} else {
|
||||
form.setFieldValue("valor", formatDotDecimalToBR(simbolo.valor));
|
||||
}
|
||||
|
||||
isFormInitialized = true;
|
||||
}
|
||||
});
|
||||
try {
|
||||
await client.mutation(api.simbolos.update, payload);
|
||||
notice = { kind: 'success', text: 'Símbolo atualizado com sucesso.' };
|
||||
setTimeout(() => goto('/recursos-humanos/simbolos'), 600);
|
||||
} catch (error) {
|
||||
console.error('erro ao atualizar símbolo', error);
|
||||
notice = { kind: 'error', text: 'Erro ao atualizar símbolo.' };
|
||||
}
|
||||
},
|
||||
defaultValues: {
|
||||
nome: '',
|
||||
refValor: '',
|
||||
vencValor: '',
|
||||
descricao: '',
|
||||
tipo: 'cargo_comissionado',
|
||||
valor: ''
|
||||
}
|
||||
}));
|
||||
|
||||
// Inicializar o formulário quando os dados estiverem disponíveis
|
||||
$effect(() => {
|
||||
if (simboloQuery.data && !isFormInitialized) {
|
||||
const simbolo = simboloQuery.data;
|
||||
|
||||
tipo = simbolo.tipo;
|
||||
|
||||
form.setFieldValue('nome', simbolo.nome);
|
||||
form.setFieldValue('descricao', simbolo.descricao);
|
||||
form.setFieldValue('tipo', simbolo.tipo);
|
||||
|
||||
if (simbolo.tipo === 'cargo_comissionado') {
|
||||
form.setFieldValue('refValor', formatDotDecimalToBR(simbolo.repValor));
|
||||
form.setFieldValue('vencValor', formatDotDecimalToBR(simbolo.vencValor));
|
||||
} else {
|
||||
form.setFieldValue('valor', formatDotDecimalToBR(simbolo.valor));
|
||||
}
|
||||
|
||||
isFormInitialized = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if simboloQuery.isLoading}
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if !simboloQuery.data}
|
||||
<div class="alert alert-error max-w-3xl mx-auto m-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Símbolo não encontrado.</span>
|
||||
</div>
|
||||
<div class="alert alert-error m-4 mx-auto max-w-3xl">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Símbolo não encontrado.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<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">Editar Símbolo</h2>
|
||||
<p class="opacity-70">
|
||||
Atualize os campos abaixo para editar o símbolo.
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="mx-auto max-w-3xl 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">Editar Símbolo</h2>
|
||||
<p class="opacity-70">Atualize os campos abaixo para editar o 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="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="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"
|
||||
>Tipo <span class="text-error">*</span></span
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
{name}
|
||||
class="select select-bordered w-full"
|
||||
bind: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>
|
||||
<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">Tipo <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select
|
||||
{name}
|
||||
class="select select-bordered w-full"
|
||||
bind: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>
|
||||
{#if tipo === 'cargo_comissionado'}
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<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.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}
|
||||
>
|
||||
<Save class="h-5 w-5" />
|
||||
<span>Salvar Alterações</span>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<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"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto('/recursos-humanos/simbolos')}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
class:loading={isSubmitting}
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
<Save class="h-5 w-5" />
|
||||
<span>Salvar Alterações</span>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,477 +1,539 @@
|
||||
<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 { goto } from "$app/navigation";
|
||||
import type { SimboloTipo } from "@sgse-app/backend/convex/schema";
|
||||
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 type { SimboloTipo } from '@sgse-app/backend/convex/schema';
|
||||
|
||||
const client = useConvexClient();
|
||||
const client = useConvexClient();
|
||||
|
||||
let tipo = $state<SimboloTipo>("cargo_comissionado");
|
||||
let tipo = $state<SimboloTipo>('cargo_comissionado');
|
||||
|
||||
const defaultValues = {
|
||||
nome: "",
|
||||
refValor: "",
|
||||
vencValor: "",
|
||||
descricao: "",
|
||||
tipo: "cargo_comissionado",
|
||||
valor: "",
|
||||
};
|
||||
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(),
|
||||
});
|
||||
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 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 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}`;
|
||||
}
|
||||
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);
|
||||
}
|
||||
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
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,
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
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"), 1500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
notice = { kind: "error", text: error.message || "Erro ao cadastrar símbolo." };
|
||||
}
|
||||
},
|
||||
defaultValues,
|
||||
}));
|
||||
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
|
||||
};
|
||||
|
||||
try {
|
||||
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'), 1500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
notice = { kind: 'error', text: error.message || 'Erro ao cadastrar símbolo.' };
|
||||
}
|
||||
},
|
||||
defaultValues
|
||||
}));
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-4 max-w-4xl">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li><a href="/recursos-humanos/simbolos" class="text-primary hover:underline">Símbolos</a></li>
|
||||
<li>Cadastrar</li>
|
||||
</ul>
|
||||
</div>
|
||||
<main class="container mx-auto max-w-4xl px-4 py-4">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumbs mb-4 text-sm">
|
||||
<ul>
|
||||
<li><a href="/recursos-humanos" class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>
|
||||
<a href="/recursos-humanos/simbolos" class="text-primary hover:underline">Símbolos</a>
|
||||
</li>
|
||||
<li>Cadastrar</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<div class="p-3 bg-green-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">Cadastro de Símbolo</h1>
|
||||
<p class="text-base-content/70">Preencha os campos abaixo para cadastrar um novo cargo ou função</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-2 flex items-center gap-4">
|
||||
<div class="rounded-xl bg-green-500/20 p-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-green-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-primary text-3xl font-bold">Cadastro de Símbolo</h1>
|
||||
<p class="text-base-content/70">
|
||||
Preencha os campos abaixo para cadastrar um novo cargo ou função
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div
|
||||
class="alert mb-6 shadow-lg"
|
||||
class:alert-success={notice.kind === "success"}
|
||||
class:alert-error={notice.kind === "error"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
{#if notice.kind === "success"}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Alertas -->
|
||||
{#if notice}
|
||||
<div
|
||||
class="alert mb-6 shadow-lg"
|
||||
class:alert-success={notice.kind === 'success'}
|
||||
class:alert-error={notice.kind === 'error'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{#if notice.kind === 'success'}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{:else}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<form
|
||||
class="space-y-6"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title text-xl border-b pb-3">Informações Básicas</h2>
|
||||
<!-- Formulário -->
|
||||
<form
|
||||
class="space-y-6"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title border-b pb-3 text-xl">Informações Básicas</h2>
|
||||
|
||||
<!-- Nome do Símbolo -->
|
||||
<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-semibold">
|
||||
Nome do Símbolo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
id="nome"
|
||||
value={state.value}
|
||||
placeholder="Ex.: DAS-1, CAA-2, FDA-3"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Informe o código identificador do símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
<!-- Nome do Símbolo -->
|
||||
<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-semibold">
|
||||
Nome do Símbolo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
{name}
|
||||
id="nome"
|
||||
value={state.value}
|
||||
placeholder="Ex.: DAS-1, CAA-2, FDA-3"
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Informe o código identificador do símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Descrição -->
|
||||
<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-semibold">
|
||||
Descrição <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
{name}
|
||||
id="descricao"
|
||||
value={state.value}
|
||||
placeholder="Ex.: Cargo de Direção e Assessoramento Superior - Nível 1"
|
||||
class="textarea textarea-bordered w-full h-24 focus:textarea-primary"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Descreva detalhadamente o símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
<!-- Descrição -->
|
||||
<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-semibold">
|
||||
Descrição <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
{name}
|
||||
id="descricao"
|
||||
value={state.value}
|
||||
placeholder="Ex.: Cargo de Direção e Assessoramento Superior - Nível 1"
|
||||
class="textarea textarea-bordered focus:textarea-primary h-24 w-full"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Descreva detalhadamente o símbolo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Tipo -->
|
||||
<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-semibold">
|
||||
Tipo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
{name}
|
||||
id="tipo"
|
||||
class="select select-bordered w-full focus:select-primary"
|
||||
bind:value={tipo}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="cargo_comissionado">Cargo Comissionado (CC)</option>
|
||||
<option value="funcao_gratificada">Função Gratificada (FG)</option>
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Selecione se é um cargo comissionado ou função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tipo -->
|
||||
<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-semibold">
|
||||
Tipo <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
{name}
|
||||
id="tipo"
|
||||
class="select select-bordered focus:select-primary w-full"
|
||||
bind:value={tipo}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
handleChange(target.value);
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="cargo_comissionado">Cargo Comissionado (CC)</option>
|
||||
<option value="funcao_gratificada">Função Gratificada (FG)</option>
|
||||
</select>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Selecione se é um cargo comissionado ou função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card de Valores -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title text-xl border-b pb-3">
|
||||
Valores Financeiros
|
||||
<span class="badge badge-primary badge-lg ml-2">
|
||||
{tipo === "cargo_comissionado" ? "Cargo Comissionado" : "Função Gratificada"}
|
||||
</span>
|
||||
</h2>
|
||||
<!-- Card de Valores -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<h2 class="card-title border-b pb-3 text-xl">
|
||||
Valores Financeiros
|
||||
<span class="badge badge-primary badge-lg ml-2">
|
||||
{tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{#if tipo === "cargo_comissionado"}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Valor de Vencimento -->
|
||||
<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-semibold">
|
||||
Valor de Vencimento <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="vencValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor base de vencimento
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
{#if tipo === 'cargo_comissionado'}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Valor de Vencimento -->
|
||||
<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-semibold">
|
||||
Valor de Vencimento <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="vencValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered focus:input-primary 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
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor base de vencimento
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
|
||||
<!-- Valor de Referência -->
|
||||
<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-semibold">
|
||||
Valor de Referência <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="refValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor de referência do cargo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
<!-- Valor de Referência -->
|
||||
<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-semibold">
|
||||
Valor de Referência <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="refValor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered focus:input-primary 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
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor de referência do cargo
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
</div>
|
||||
|
||||
<!-- Preview do Total -->
|
||||
{#if getTotalPreview()}
|
||||
<div class="alert alert-info shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Valor Total Calculado</h3>
|
||||
<div class="text-2xl font-bold mt-1">R$ {getTotalPreview()}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Valor da Função Gratificada -->
|
||||
<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-semibold">
|
||||
Valor da Função Gratificada <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="valor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered w-full focus:input-primary"
|
||||
inputmode="decimal"
|
||||
autocomplete="off"
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const formatted = formatCurrencyBR(target.value);
|
||||
target.value = formatted;
|
||||
handleChange(formatted);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor mensal da função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Preview do Total -->
|
||||
{#if getTotalPreview()}
|
||||
<div class="alert alert-info shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Valor Total Calculado</h3>
|
||||
<div class="mt-1 text-2xl font-bold">R$ {getTotalPreview()}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Valor da Função Gratificada -->
|
||||
<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-semibold">
|
||||
Valor da Função Gratificada <span class="text-error">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-group">
|
||||
<span>R$</span>
|
||||
<input
|
||||
{name}
|
||||
id="valor"
|
||||
value={state.value}
|
||||
placeholder="0,00"
|
||||
class="input input-bordered focus:input-primary 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
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Valor mensal da função gratificada
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Field>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botões de Ação -->
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
canSubmit: state.canSubmit,
|
||||
isSubmitting: state.isSubmitting,
|
||||
})}
|
||||
>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-lg"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto("/recursos-humanos/simbolos")}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Cadastrando...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Cadastrar Símbolo
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
<!-- Botões de Ação -->
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
canSubmit: state.canSubmit,
|
||||
isSubmitting: state.isSubmitting
|
||||
})}
|
||||
>
|
||||
{#snippet children({ canSubmit, isSubmitting })}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-col justify-end gap-3 sm:flex-row">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-lg"
|
||||
disabled={isSubmitting}
|
||||
onclick={() => goto('/recursos-humanos/simbolos')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Cadastrando...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Cadastrar Símbolo
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</form.Subscribe>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
.card {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user