328 lines
10 KiB
Svelte
328 lines
10 KiB
Svelte
<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 { resolve } from '$app/paths';
|
|
import { Award, Plus, CheckCircle, XCircle, Filter, X, MoreVertical, Edit, Trash2, AlertTriangle } from 'lucide-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;
|
|
}
|
|
});
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
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';
|
|
}
|
|
</script>
|
|
|
|
<main class="container mx-auto px-4 py-4">
|
|
<!-- Breadcrumb -->
|
|
<div class="breadcrumbs mb-4 text-sm">
|
|
<ul>
|
|
<li>
|
|
<a href={resolve('/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 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">
|
|
<Award class="h-8 w-8 text-green-600" strokeWidth={2} />
|
|
</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={resolve('/recursos-humanos/simbolos/cadastro')} class="btn btn-primary btn-lg gap-2">
|
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
|
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'}
|
|
>
|
|
{#if notice.kind === 'success'}
|
|
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{:else}
|
|
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
{/if}
|
|
<span>{notice.text}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Filtros -->
|
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4 text-lg">
|
|
<Filter class="h-5 w-5" strokeWidth={2} />
|
|
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 = '';
|
|
}}
|
|
>
|
|
<X class="h-4 w-4" strokeWidth={2} />
|
|
Limpar Filtros
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#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)}
|
|
>
|
|
<MoreVertical class="h-5 w-5" strokeWidth={2} />
|
|
</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={resolve(`/recursos-humanos/simbolos/${simbolo._id}/editar`)}>
|
|
<Edit class="h-4 w-4" strokeWidth={2} />
|
|
Editar
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
onclick={() => openDeleteModal(simbolo._id, simbolo.nome)}
|
|
class="text-error"
|
|
>
|
|
<Trash2 class="h-4 w-4" strokeWidth={2} />
|
|
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="mb-4 text-lg font-bold">Confirmar Exclusão</h3>
|
|
<div class="alert alert-warning mb-4">
|
|
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
|
<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}
|
|
<Trash2 class="h-5 w-5" strokeWidth={2} />
|
|
Confirmar Exclusão
|
|
{/if}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|