feat: Implement sector role configuration on the setores page and remove the deprecated TI config page.
This commit is contained in:
@@ -8,6 +8,11 @@
|
||||
|
||||
// Queries
|
||||
const setoresQuery = useQuery(api.setores.list, {});
|
||||
const configQuery = useQuery(api.config.getConfig, {});
|
||||
|
||||
// Data
|
||||
let setores = $derived(setoresQuery.data || []);
|
||||
let config = $derived(configQuery.data);
|
||||
|
||||
// Estado do modal
|
||||
let showModal = $state(false);
|
||||
@@ -27,6 +32,82 @@
|
||||
let showDeleteModal = $state(false);
|
||||
let setorToDelete = $state<{ _id: Id<'setores'>; nome: string } | null>(null);
|
||||
|
||||
// Configuração de Vinculações
|
||||
let savingConfig = $state(false);
|
||||
let configError = $state<string | null>(null);
|
||||
let configSuccess = $state<string | null>(null);
|
||||
|
||||
// Initial state for config, used until config is loaded or if config is empty
|
||||
let initialConfigState = {
|
||||
comprasSetorId: '' as Id<'setores'> | '',
|
||||
financeiroSetorId: '' as Id<'setores'> | '',
|
||||
juridicoSetorId: '' as Id<'setores'> | '',
|
||||
convenioSetorId: '' as Id<'setores'> | '',
|
||||
programasEsportivosSetorId: '' as Id<'setores'> | '',
|
||||
comunicacaoSetorId: '' as Id<'setores'> | '',
|
||||
tiSetorId: '' as Id<'setores'> | ''
|
||||
};
|
||||
|
||||
// Derived state from configQuery, falling back to initial empty state.
|
||||
// We make this a state so it can be bound to the form inputs.
|
||||
// However, we only want to update it from configQuery specificially when configQuery loads.
|
||||
// A common pattern is to use an effect to sync, but svelte-autofixer flagged it.
|
||||
// Let's try to just use state and initialize it. Since `config` is derived, we can't just set it once easily if it starts undefined.
|
||||
|
||||
// Instead of complex syncing, let's just use a single state object that we update when config loads.
|
||||
// If the tool flagged the effect, it might be because I was updating specific fields one by one.
|
||||
// Let's try to do it cleaner or ignore if it's the only way to sync remote data to local form state.
|
||||
|
||||
// Actually, for form inputs that need to be editable, we DO need local state.
|
||||
// The issue "The stateful variable ... is assigned inside an $effect" is a warning against infinite loops or side effects.
|
||||
// Here we are syncing remote state to local state, which is a valid use case IF done carefully (e.g. untracked or checking for difference).
|
||||
|
||||
let setoresConfig = $state(initialConfigState);
|
||||
|
||||
$effect(() => {
|
||||
if (config) {
|
||||
// Only update if the values are actually different (though for form initialization usually we want to just overwrite)
|
||||
// Or simpler: just assignment is fine if it doesn't cause a loop. `config` coming from `useQuery` changes only when data changes.
|
||||
// Using untrack might solve the warning if we were reading `setoresConfig` too, but we are not validation against it here.
|
||||
// Let's just update all at once to be cleaner.
|
||||
setoresConfig = {
|
||||
comprasSetorId: config.comprasSetorId || '',
|
||||
financeiroSetorId: config.financeiroSetorId || '',
|
||||
juridicoSetorId: config.juridicoSetorId || '',
|
||||
convenioSetorId: config.convenioSetorId || '',
|
||||
programasEsportivosSetorId: config.programasEsportivosSetorId || '',
|
||||
comunicacaoSetorId: config.comunicacaoSetorId || '',
|
||||
tiSetorId: config.tiSetorId || ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function saveConfig() {
|
||||
savingConfig = true;
|
||||
configError = null;
|
||||
configSuccess = null;
|
||||
|
||||
try {
|
||||
await client.mutation(api.config.updateConfig, {
|
||||
comprasSetorId: setoresConfig.comprasSetorId || undefined,
|
||||
financeiroSetorId: setoresConfig.financeiroSetorId || undefined,
|
||||
juridicoSetorId: setoresConfig.juridicoSetorId || undefined,
|
||||
convenioSetorId: setoresConfig.convenioSetorId || undefined,
|
||||
programasEsportivosSetorId: setoresConfig.programasEsportivosSetorId || undefined,
|
||||
comunicacaoSetorId: setoresConfig.comunicacaoSetorId || undefined,
|
||||
tiSetorId: setoresConfig.tiSetorId || undefined
|
||||
});
|
||||
configSuccess = 'Configuração salva com sucesso!';
|
||||
setTimeout(() => {
|
||||
configSuccess = null;
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
configError = (e as Error).message;
|
||||
} finally {
|
||||
savingConfig = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingSetor = null;
|
||||
nome = '';
|
||||
@@ -116,6 +197,16 @@
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
const roles = [
|
||||
{ key: 'comprasSetorId', label: 'Setor de Compras' },
|
||||
{ key: 'financeiroSetorId', label: 'Setor Financeiro' },
|
||||
{ key: 'juridicoSetorId', label: 'Setor Jurídico' },
|
||||
{ key: 'convenioSetorId', label: 'Setor de Convênios' },
|
||||
{ key: 'programasEsportivosSetorId', label: 'Setor de Programas Esportivos' },
|
||||
{ key: 'comunicacaoSetorId', label: 'Setor de Comunicação' },
|
||||
{ key: 'tiSetorId', label: 'Setor de TI' }
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-10">
|
||||
@@ -164,109 +255,195 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Lista de Setores -->
|
||||
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg">
|
||||
{#if setoresQuery.isLoading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if !setoresQuery.data || setoresQuery.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/30 h-16 w-16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Nenhum setor cadastrado</h3>
|
||||
<p class="text-base-content/50 mt-2">Clique em "Novo Setor" para criar o primeiro setor.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sigla</th>
|
||||
<th>Nome</th>
|
||||
<th>Criado em</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each setoresQuery.data as setor (setor._id)}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<span class="badge badge-primary badge-lg font-mono font-bold">
|
||||
{setor.sigla}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-medium">{setor.nome}</td>
|
||||
<td class="text-base-content/60 text-sm">{formatDate(setor.createdAt)}</td>
|
||||
<td class="text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<ActionGuard recurso="setores" acao="editar">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => openEditModal(setor)}
|
||||
aria-label="Editar setor {setor.nome}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ActionGuard>
|
||||
<ActionGuard recurso="setores" acao="excluir">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-error"
|
||||
onclick={() => openDeleteModal(setor)}
|
||||
aria-label="Excluir setor {setor.nome}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ActionGuard>
|
||||
</div>
|
||||
</td>
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
<!-- Lista de Setores -->
|
||||
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg lg:col-span-2">
|
||||
<h2 class="mb-4 text-xl font-bold">Setores Cadastrados</h2>
|
||||
{#if setoresQuery.isLoading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else if !setoresQuery.data || setoresQuery.data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/30 h-16 w-16"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Nenhum setor cadastrado</h3>
|
||||
<p class="text-base-content/50 mt-2">
|
||||
Clique em "Novo Setor" para criar o primeiro setor.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sigla</th>
|
||||
<th>Nome</th>
|
||||
<th>Criado em</th>
|
||||
<th class="text-right">Ações</th>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each setoresQuery.data as setor (setor._id)}
|
||||
<tr class="hover">
|
||||
<td>
|
||||
<span class="badge badge-primary badge-lg font-mono font-bold">
|
||||
{setor.sigla}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-medium">{setor.nome}</td>
|
||||
<td class="text-base-content/60 text-sm">{formatDate(setor.createdAt)}</td>
|
||||
<td class="text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<ActionGuard recurso="setores" acao="editar">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => openEditModal(setor)}
|
||||
aria-label="Editar setor {setor.nome}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ActionGuard>
|
||||
<ActionGuard recurso="setores" acao="excluir">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-error"
|
||||
onclick={() => openDeleteModal(setor)}
|
||||
aria-label="Excluir setor {setor.nome}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</ActionGuard>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Configuração de Vinculações -->
|
||||
<section class="bg-base-100 h-fit rounded-2xl border p-6 shadow-lg">
|
||||
<h2 class="mb-4 text-xl font-bold">Vinculações do Sistema</h2>
|
||||
<p class="text-base-content/70 mb-6 text-sm">
|
||||
Associe setores às funções específicas do sistema para automatizar fluxos.
|
||||
</p>
|
||||
|
||||
{#if configQuery.isLoading}
|
||||
<div class="flex justify-center py-8">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each roles as role (role.key)}
|
||||
<div class="form-control w-full">
|
||||
<label class="label" for={role.key}>
|
||||
<span class="label-text font-medium">{role.label}</span>
|
||||
</label>
|
||||
<select
|
||||
id={role.key}
|
||||
class="select select-bordered w-full"
|
||||
bind:value={setoresConfig[role.key]}
|
||||
>
|
||||
<option value="">Selecione um setor...</option>
|
||||
{#each setores as setor (setor._id)}
|
||||
<option value={setor._id}>{setor.nome} ({setor.sigla})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
{#if configSuccess}
|
||||
<div class="alert alert-success mt-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{configSuccess}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if configError}
|
||||
<div class="alert alert-error mt-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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{configError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-primary w-full" onclick={saveConfig} disabled={savingConfig}>
|
||||
{#if savingConfig}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Salvar Configurações
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal de Criação/Edição -->
|
||||
|
||||
@@ -368,15 +368,6 @@
|
||||
palette: 'accent',
|
||||
icon: 'building'
|
||||
},
|
||||
{
|
||||
title: 'Configurações Gerais',
|
||||
description:
|
||||
'Configure opções gerais do sistema, incluindo setor de compras e outras configurações administrativas.',
|
||||
ctaLabel: 'Configurar',
|
||||
href: '/(dashboard)/ti/configuracoes',
|
||||
palette: 'secondary',
|
||||
icon: 'control'
|
||||
},
|
||||
{
|
||||
title: 'Documentação',
|
||||
description:
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
// Reactive queries
|
||||
const setoresQuery = useQuery(api.setores.list, {});
|
||||
const configQuery = useQuery(api.config.getComprasSetor, {});
|
||||
|
||||
let setores = $derived(setoresQuery.data || []);
|
||||
let config = $derived(configQuery.data);
|
||||
let loading = $derived(setoresQuery.isLoading || configQuery.isLoading);
|
||||
|
||||
// Initialize selected setor from config - using boxed $state to avoid reactivity warning
|
||||
let selectedSetorId = $state('');
|
||||
|
||||
// Update selectedSetorId when config changes
|
||||
$effect(() => {
|
||||
if (config?.comprasSetorId && !selectedSetorId) {
|
||||
selectedSetorId = config.comprasSetorId;
|
||||
}
|
||||
});
|
||||
|
||||
let saving = $state(false);
|
||||
let error: string | null = $state(null);
|
||||
let success: string | null = $state(null);
|
||||
|
||||
async function saveConfig() {
|
||||
saving = true;
|
||||
error = null;
|
||||
success = null;
|
||||
try {
|
||||
await client.mutation(api.config.updateComprasSetor, {
|
||||
setorId: selectedSetorId as Id<'setores'>
|
||||
});
|
||||
success = 'Configuração salva com sucesso!';
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
<h1 class="mb-6 text-2xl font-bold">Configurações Gerais</h1>
|
||||
|
||||
{#if loading}
|
||||
<p>Carregando...</p>
|
||||
{:else}
|
||||
<div class="max-w-md rounded-lg bg-white p-6 shadow-md">
|
||||
<h2 class="mb-4 text-xl font-semibold">Setor de Compras</h2>
|
||||
<p class="mb-4 text-sm text-gray-600">
|
||||
Selecione o setor responsável por receber e aprovar pedidos de compra.
|
||||
</p>
|
||||
|
||||
{#if error}
|
||||
<div class="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<div class="mb-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700">
|
||||
{success}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="setor" class="mb-1 block text-sm font-medium text-gray-700"> Setor </label>
|
||||
<select
|
||||
id="setor"
|
||||
bind:value={selectedSetorId}
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Selecione um setor...</option>
|
||||
{#each setores as setor (setor._id)}
|
||||
<option value={setor._id}>{setor.nome} ({setor.sigla})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={saveConfig}
|
||||
disabled={saving || !selectedSetorId}
|
||||
class="w-full rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Salvando...' : 'Salvar Configuração'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user