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:
2025-11-25 14:14:43 -03:00
parent f8d9c17f63
commit 6128c20da0
12 changed files with 3503 additions and 88 deletions

View File

@@ -302,11 +302,11 @@
<!-- Link para Instâncias -->
<section class="flex justify-center">
<a href="/fluxos/instancias" class="btn btn-outline btn-lg">
<a href="/licitacoes/fluxos" class="btn btn-outline btn-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
Ver Instâncias de Fluxo
Ver Fluxos de Trabalho
</a>
</section>
</main>

View File

@@ -12,8 +12,11 @@
// Query da instância com passos
const instanceQuery = useQuery(api.flows.getInstanceWithSteps, () => ({ id: instanceId }));
// Query de usuários (para reatribuição)
// Query de usuários (para reatribuição) - será filtrado por setor no modal
const usuariosQuery = useQuery(api.usuarios.listar, {});
// Query de usuários por setor para atribuição
let usuariosPorSetorQuery = $state<ReturnType<typeof useQuery<typeof api.flows.getUsuariosBySetorForAssignment>> | null>(null);
// Estado de operações
let isProcessing = $state(false);
@@ -286,8 +289,8 @@
<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" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Instância não encontrada</h3>
<a href={resolve('/(dashboard)/fluxos/instancias')} class="btn btn-ghost mt-4">Voltar para lista</a>
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Fluxo não encontrado</h3>
<a href={resolve('/(dashboard)/licitacoes/fluxos')} class="btn btn-ghost mt-4">Voltar para lista</a>
</div>
{:else}
{@const instance = instanceQuery.data.instance}
@@ -302,7 +305,7 @@
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
<div class="relative z-10">
<div class="flex items-center gap-4 mb-6">
<a href={resolve('/(dashboard)/fluxos/instancias')} class="btn btn-ghost btn-sm">
<a href={resolve('/(dashboard)/licitacoes/fluxos')} class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
@@ -317,10 +320,12 @@
{instance.templateName ?? 'Fluxo'}
</h1>
<div class="flex flex-wrap gap-4">
<div class="flex items-center gap-2">
<span class="badge badge-outline">{instance.targetType}</span>
<span class="text-base-content/70 font-medium">{instance.targetId}</span>
</div>
{#if instance.contratoId}
<div class="flex items-center gap-2">
<span class="badge badge-outline">Contrato</span>
<span class="text-base-content/70 font-medium">{instance.contratoId}</span>
</div>
{/if}
<div class="flex items-center gap-2 text-base-content/60">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
@@ -693,7 +698,7 @@
<div class="modal-box">
<h3 class="text-lg font-bold text-error">Cancelar Fluxo</h3>
<p class="py-4">
Tem certeza que deseja cancelar esta instância de fluxo?
Tem certeza que deseja cancelar este fluxo?
</p>
<p class="text-base-content/60 text-sm">
Esta ação não pode ser desfeita. Todos os passos pendentes serão marcados como cancelados.

View File

@@ -13,6 +13,12 @@
const templateQuery = useQuery(api.flows.getTemplate, () => ({ id: templateId }));
const stepsQuery = useQuery(api.flows.listStepsByTemplate, () => ({ flowTemplateId: templateId }));
const setoresQuery = useQuery(api.setores.list, {});
// Query de sub-etapas (reativa baseada no step selecionado)
const subEtapasQuery = useQuery(
api.flows.listarSubEtapas,
() => selectedStepId ? { flowStepId: selectedStepId } : 'skip'
);
// Estado local para drag and drop
let localSteps = $state<NonNullable<typeof stepsQuery.data>>([]);
@@ -48,6 +54,13 @@
} | null>(null);
let isSavingStep = $state(false);
// Estado de sub-etapas
let showSubEtapaModal = $state(false);
let subEtapaNome = $state('');
let subEtapaDescricao = $state('');
let isCriandoSubEtapa = $state(false);
let subEtapaError = $state<string | null>(null);
// Inicializar edição quando selecionar passo
$effect(() => {
if (selectedStep) {
@@ -215,6 +228,67 @@
);
}
}
// Funções de sub-etapas
function openSubEtapaModal() {
subEtapaNome = '';
subEtapaDescricao = '';
subEtapaError = null;
showSubEtapaModal = true;
}
function closeSubEtapaModal() {
showSubEtapaModal = false;
subEtapaNome = '';
subEtapaDescricao = '';
subEtapaError = null;
}
async function handleCriarSubEtapa() {
if (!selectedStepId || !subEtapaNome.trim()) {
subEtapaError = 'O nome é obrigatório';
return;
}
isCriandoSubEtapa = true;
subEtapaError = null;
try {
await client.mutation(api.flows.criarSubEtapa, {
flowStepId: selectedStepId,
name: subEtapaNome.trim(),
description: subEtapaDescricao.trim() || undefined
});
closeSubEtapaModal();
} catch (e) {
subEtapaError = e instanceof Error ? e.message : 'Erro ao criar sub-etapa';
} finally {
isCriandoSubEtapa = false;
}
}
async function handleDeletarSubEtapa(subEtapaId: Id<'flowSubSteps'>) {
if (!confirm('Tem certeza que deseja excluir esta sub-etapa?')) {
return;
}
try {
await client.mutation(api.flows.deletarSubEtapa, { subEtapaId });
} catch (e) {
alert(e instanceof Error ? e.message : 'Erro ao deletar sub-etapa');
}
}
async function handleAtualizarStatusSubEtapa(subEtapaId: Id<'flowSubSteps'>, novoStatus: 'pending' | 'in_progress' | 'completed' | 'blocked') {
try {
await client.mutation(api.flows.atualizarSubEtapa, {
subEtapaId,
status: novoStatus
});
} catch (e) {
alert(e instanceof Error ? e.message : 'Erro ao atualizar status');
}
}
</script>
<main class="flex h-[calc(100vh-4rem)] flex-col">
@@ -472,6 +546,73 @@
</div>
</div>
<!-- Sub-etapas -->
<div class="form-control">
<div class="label">
<span class="label-text font-semibold">Sub-etapas</span>
<button
type="button"
class="btn btn-ghost btn-xs"
onclick={openSubEtapaModal}
aria-label="Adicionar sub-etapa"
>
<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="M12 4v16m8-8H4" />
</svg>
Adicionar
</button>
</div>
<div class="space-y-2">
{#if subEtapasQuery.isLoading}
<div class="flex items-center justify-center py-4">
<span class="loading loading-spinner loading-sm"></span>
</div>
{:else if subEtapasQuery.data && subEtapasQuery.data.length > 0}
{#each subEtapasQuery.data as subEtapa (subEtapa._id)}
<div class="flex items-center gap-2 rounded-lg border border-base-300 bg-base-100 p-2">
<div class="flex-1">
<div class="font-medium text-sm">{subEtapa.name}</div>
{#if subEtapa.description}
<div class="text-base-content/60 text-xs">{subEtapa.description}</div>
{/if}
<div class="mt-1">
<span class="badge badge-xs {subEtapa.status === 'completed' ? 'badge-success' : subEtapa.status === 'in_progress' ? 'badge-info' : subEtapa.status === 'blocked' ? 'badge-error' : 'badge-ghost'}">
{subEtapa.status === 'completed' ? 'Concluída' : subEtapa.status === 'in_progress' ? 'Em Andamento' : subEtapa.status === 'blocked' ? 'Bloqueada' : 'Pendente'}
</span>
</div>
</div>
<div class="flex gap-1">
<select
class="select select-xs select-bordered"
value={subEtapa.status}
onchange={(e) => handleAtualizarStatusSubEtapa(subEtapa._id, e.currentTarget.value as 'pending' | 'in_progress' | 'completed' | 'blocked')}
>
<option value="pending">Pendente</option>
<option value="in_progress">Em Andamento</option>
<option value="completed">Concluída</option>
<option value="blocked">Bloqueada</option>
</select>
<button
type="button"
class="btn btn-ghost btn-xs text-error"
onclick={() => handleDeletarSubEtapa(subEtapa._id)}
aria-label="Deletar sub-etapa"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/each}
{:else}
<div class="text-base-content/40 rounded-lg border border-dashed border-base-300 bg-base-200/50 p-4 text-center text-sm">
Nenhuma sub-etapa adicionada
</div>
{/if}
</div>
</div>
<div class="flex gap-2 pt-4">
<button
class="btn btn-error btn-outline flex-1"
@@ -597,3 +738,63 @@
</div>
{/if}
<!-- Modal de Nova Sub-etapa -->
{#if showSubEtapaModal}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="text-lg font-bold">Nova Sub-etapa</h3>
{#if subEtapaError}
<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>{subEtapaError}</span>
</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleCriarSubEtapa(); }} class="mt-4 space-y-4">
<div class="form-control">
<label class="label" for="sub-etapa-nome">
<span class="label-text">Nome da Sub-etapa</span>
</label>
<input
type="text"
id="sub-etapa-nome"
bind:value={subEtapaNome}
class="input input-bordered w-full"
placeholder="Ex: Revisar documentação"
required
/>
</div>
<div class="form-control">
<label class="label" for="sub-etapa-descricao">
<span class="label-text">Descrição (opcional)</span>
</label>
<textarea
id="sub-etapa-descricao"
bind:value={subEtapaDescricao}
class="textarea textarea-bordered w-full"
placeholder="Descreva a sub-etapa..."
rows="2"
></textarea>
</div>
<div class="modal-action">
<button type="button" class="btn" onclick={closeSubEtapaModal} disabled={isCriandoSubEtapa}>
Cancelar
</button>
<button type="submit" class="btn btn-secondary" disabled={isCriandoSubEtapa}>
{#if isCriandoSubEtapa}
<span class="loading loading-spinner loading-sm"></span>
{/if}
Criar Sub-etapa
</button>
</div>
</form>
</div>
<button type="button" class="modal-backdrop" onclick={closeSubEtapaModal} aria-label="Fechar modal"></button>
</div>
{/if}

View File

@@ -61,7 +61,7 @@
managerId: managerId as Id<'usuarios'>
});
closeCreateModal();
goto(`/fluxos/instancias/${instanceId}`);
goto(`/licitacoes/fluxos/${instanceId}`);
} catch (e) {
createError = e instanceof Error ? e.message : 'Erro ao criar instância';
} finally {
@@ -237,7 +237,7 @@
<td class="text-base-content/60 text-sm">{formatDate(instance.startedAt)}</td>
<td class="text-right">
<a
href="/fluxos/instancias/{instance._id}"
href="/licitacoes/fluxos/{instance._id}"
class="btn btn-ghost btn-sm"
>
<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">