feat: implement sub-steps management in workflow editor
- Added functionality for creating, updating, and deleting sub-steps within the workflow editor. - Introduced a modal for adding new sub-steps, including fields for name and description. - Enhanced the UI to display sub-steps with status indicators and options for updating their status. - Updated navigation links to reflect changes in the workflow structure, ensuring consistency across the application. - Refactored related components to accommodate the new sub-steps feature, improving overall workflow management.
This commit is contained in:
@@ -1,23 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { SimboloTipo } from '@sgse-app/backend/convex/schema';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import PrintModal from '$lib/components/PrintModal.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let list: Array<any> = [];
|
||||
let filtered: Array<any> = [];
|
||||
let selectedId: string | null = null;
|
||||
let openMenuId: string | null = null;
|
||||
let funcionarioParaImprimir: any = null;
|
||||
// Estado reativo
|
||||
let list = $state<Array<{
|
||||
_id: Id<'funcionarios'>;
|
||||
nome: string;
|
||||
matricula?: string;
|
||||
cpf: string;
|
||||
cidade?: string;
|
||||
uf?: string;
|
||||
simboloTipo?: SimboloTipo;
|
||||
}>>([]);
|
||||
let filtered = $state<typeof list>([]);
|
||||
let openMenuId = $state<string | null>(null);
|
||||
let funcionarioParaImprimir = $state<unknown>(null);
|
||||
|
||||
let filtroNome = '';
|
||||
let filtroCPF = '';
|
||||
let filtroMatricula = '';
|
||||
let filtroTipo: SimboloTipo | '' = '';
|
||||
// Estado do modal de setores
|
||||
let showSetoresModal = $state(false);
|
||||
let funcionarioParaSetores = $state<{ _id: Id<'funcionarios'>; nome: string } | null>(null);
|
||||
let setoresSelecionados = $state<Id<'setores'>[]>([]);
|
||||
let isSavingSetores = $state(false);
|
||||
let setoresError = $state<string | null>(null);
|
||||
|
||||
// Queries
|
||||
const todosSetoresQuery = useQuery(api.setores.list, {});
|
||||
|
||||
let filtroNome = $state('');
|
||||
let filtroCPF = $state('');
|
||||
let filtroMatricula = $state('');
|
||||
let filtroTipo = $state<SimboloTipo | ''>('');
|
||||
|
||||
function applyFilters() {
|
||||
const nome = filtroNome.toLowerCase();
|
||||
@@ -33,18 +52,15 @@
|
||||
}
|
||||
|
||||
async function load() {
|
||||
list = await client.query(api.funcionarios.getAll, {} as any);
|
||||
const data = await client.query(api.funcionarios.getAll, {});
|
||||
list = data ?? [];
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function editSelected() {
|
||||
if (selectedId) goto(resolve(`/recursos-humanos/funcionarios/${selectedId}/editar`));
|
||||
}
|
||||
|
||||
async function openPrintModal(funcionarioId: string) {
|
||||
try {
|
||||
const data = await client.query(api.funcionarios.getFichaCompleta, {
|
||||
id: funcionarioId as any
|
||||
id: funcionarioId as Id<'funcionarios'>
|
||||
});
|
||||
funcionarioParaImprimir = data;
|
||||
} catch (err) {
|
||||
@@ -62,12 +78,64 @@
|
||||
function toggleMenu(id: string) {
|
||||
openMenuId = openMenuId === id ? null : id;
|
||||
}
|
||||
$: needsScroll = filtered.length > 8;
|
||||
|
||||
async function openSetoresModal(funcionarioId: Id<'funcionarios'>, nome: string) {
|
||||
funcionarioParaSetores = { _id: funcionarioId, nome };
|
||||
setoresSelecionados = [];
|
||||
setoresError = null;
|
||||
showSetoresModal = true;
|
||||
openMenuId = null;
|
||||
|
||||
// Carregar setores do funcionário
|
||||
try {
|
||||
const setores = await client.query(api.setores.getSetoresByFuncionario, {
|
||||
funcionarioId
|
||||
});
|
||||
setoresSelecionados = setores.map((s) => s._id);
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar setores do funcionário:', err);
|
||||
setoresError = 'Erro ao carregar setores do funcionário';
|
||||
}
|
||||
}
|
||||
|
||||
function closeSetoresModal() {
|
||||
showSetoresModal = false;
|
||||
funcionarioParaSetores = null;
|
||||
setoresSelecionados = [];
|
||||
setoresError = null;
|
||||
}
|
||||
|
||||
function toggleSetor(setorId: Id<'setores'>) {
|
||||
if (setoresSelecionados.includes(setorId)) {
|
||||
setoresSelecionados = setoresSelecionados.filter((id) => id !== setorId);
|
||||
} else {
|
||||
setoresSelecionados = [...setoresSelecionados, setorId];
|
||||
}
|
||||
}
|
||||
|
||||
async function salvarSetores() {
|
||||
if (!funcionarioParaSetores) return;
|
||||
|
||||
isSavingSetores = true;
|
||||
setoresError = null;
|
||||
|
||||
try {
|
||||
await client.mutation(api.setores.atualizarSetoresFuncionario, {
|
||||
funcionarioId: funcionarioParaSetores._id,
|
||||
setorIds: setoresSelecionados
|
||||
});
|
||||
closeSetoresModal();
|
||||
} catch (err) {
|
||||
setoresError = err instanceof Error ? err.message : 'Erro ao salvar setores';
|
||||
} finally {
|
||||
isSavingSetores = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-4 max-w-7xl flex flex-col" style="height: calc(100vh - 8rem); min-height: 600px;">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumbs mb-4 text-sm flex-shrink-0">
|
||||
<div class="breadcrumbs mb-4 text-sm shrink-0">
|
||||
<ul>
|
||||
<li><a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||
<li>Funcionários</li>
|
||||
@@ -75,7 +143,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho -->
|
||||
<div class="mb-6 flex-shrink-0">
|
||||
<div class="mb-6 shrink-0">
|
||||
<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-blue-500/20 p-3">
|
||||
@@ -118,7 +186,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 mb-4 shadow-xl flex-shrink-0">
|
||||
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 mb-4 shadow-xl shrink-0">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4 text-lg">
|
||||
<svg
|
||||
@@ -232,7 +300,7 @@
|
||||
<div class="flex-1 overflow-hidden flex flex-col">
|
||||
<div class="overflow-x-auto flex-1 overflow-y-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300 to-base-200">
|
||||
<thead class="sticky top-0 z-10 shadow-md bg-linear-to-r from-base-300 to-base-200">
|
||||
<tr>
|
||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Nome</th>
|
||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">CPF</th>
|
||||
@@ -277,7 +345,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each filtered as f}
|
||||
{#each filtered as f (f._id)}
|
||||
<tr class="hover:bg-base-200/50 transition-colors">
|
||||
<td class="whitespace-nowrap font-medium">{f.nome}</td>
|
||||
<td class="whitespace-nowrap">{f.cpf}</td>
|
||||
@@ -314,20 +382,28 @@
|
||||
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-20 w-52 border p-2 shadow-xl"
|
||||
>
|
||||
<li>
|
||||
<a href={`/recursos-humanos/funcionarios/${f._id}`} class="hover:bg-primary/10">
|
||||
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}`)} class="hover:bg-primary/10">
|
||||
Ver Detalhes
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`/recursos-humanos/funcionarios/${f._id}/editar`} class="hover:bg-primary/10">
|
||||
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}/editar`)} class="hover:bg-primary/10">
|
||||
Editar
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`/recursos-humanos/funcionarios/${f._id}/documentos`} class="hover:bg-primary/10">
|
||||
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}/documentos`)} class="hover:bg-primary/10">
|
||||
Ver Documentos
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onclick={() => openSetoresModal(f._id, f.nome)}
|
||||
class="hover:bg-primary/10"
|
||||
>
|
||||
Atribuir Setores
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onclick={() => openPrintModal(f._id)} class="hover:bg-primary/10">
|
||||
Imprimir Ficha
|
||||
@@ -347,7 +423,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Informação sobre resultados -->
|
||||
<div class="text-base-content/70 mt-3 text-center text-sm flex-shrink-0 border-t border-base-300 pt-3 bg-base-100/50">
|
||||
<div class="text-base-content/70 mt-3 text-center text-sm shrink-0 border-t border-base-300 pt-3 bg-base-100/50">
|
||||
Exibindo <span class="font-semibold">{filtered.length}</span> de <span class="font-semibold">{list.length}</span> funcionário(s)
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,4 +435,85 @@
|
||||
onClose={() => (funcionarioParaImprimir = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Atribuição de Setores -->
|
||||
{#if showSetoresModal && funcionarioParaSetores}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="text-lg font-bold">Atribuir Setores</h3>
|
||||
<p class="text-base-content/60 mt-2">
|
||||
Selecione os setores para <strong>{funcionarioParaSetores.nome}</strong>
|
||||
</p>
|
||||
|
||||
{#if setoresError}
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>{setoresError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 max-h-96 overflow-y-auto">
|
||||
{#if todosSetoresQuery.isLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if todosSetoresQuery.data && todosSetoresQuery.data.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each todosSetoresQuery.data as setor (setor._id)}
|
||||
{@const isSelected = setoresSelecionados.includes(setor._id)}
|
||||
<label class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 hover:bg-base-200 {isSelected ? 'border-primary bg-primary/5' : 'border-base-300'}">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={isSelected}
|
||||
onchange={() => toggleSetor(setor._id)}
|
||||
aria-label="Selecionar setor {setor.nome}"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">{setor.nome}</div>
|
||||
<div class="text-base-content/60 text-sm">Sigla: {setor.sigla}</div>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-base-content/60 py-8 text-center">
|
||||
<p>Nenhum setor cadastrado</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" onclick={closeSetoresModal} disabled={isSavingSetores}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick={salvarSetores} disabled={isSavingSetores}>
|
||||
{#if isSavingSetores}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Salvar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="modal-backdrop"
|
||||
onclick={closeSetoresModal}
|
||||
aria-label="Fechar modal"
|
||||
></button>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user