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:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user