- 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.
374 lines
12 KiB
Svelte
374 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import ActionGuard from '$lib/components/ActionGuard.svelte';
|
|
import { goto } from '$app/navigation';
|
|
|
|
const client = useConvexClient();
|
|
|
|
// Estado dos filtros
|
|
let statusFilter = $state<'active' | 'completed' | 'cancelled' | undefined>(undefined);
|
|
|
|
// Query de instâncias
|
|
const instancesQuery = useQuery(
|
|
api.flows.listInstances,
|
|
() => (statusFilter ? { status: statusFilter } : {})
|
|
);
|
|
|
|
// Query de templates publicados (para o modal de criação)
|
|
const publishedTemplatesQuery = useQuery(api.flows.listTemplates, { status: 'published' });
|
|
|
|
// Modal de criação
|
|
let showCreateModal = $state(false);
|
|
let selectedTemplateId = $state<Id<'flowTemplates'> | ''>('');
|
|
let targetType = $state('');
|
|
let targetId = $state('');
|
|
let managerId = $state<Id<'usuarios'> | ''>('');
|
|
let isCreating = $state(false);
|
|
let createError = $state<string | null>(null);
|
|
|
|
// Query de usuários (para seleção de gerente)
|
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
|
|
|
function openCreateModal() {
|
|
selectedTemplateId = '';
|
|
targetType = '';
|
|
targetId = '';
|
|
managerId = '';
|
|
createError = null;
|
|
showCreateModal = true;
|
|
}
|
|
|
|
function closeCreateModal() {
|
|
showCreateModal = false;
|
|
}
|
|
|
|
async function handleCreate() {
|
|
if (!selectedTemplateId || !targetType.trim() || !targetId.trim() || !managerId) {
|
|
createError = 'Todos os campos são obrigatórios';
|
|
return;
|
|
}
|
|
|
|
isCreating = true;
|
|
createError = null;
|
|
|
|
try {
|
|
const instanceId = await client.mutation(api.flows.instantiateFlow, {
|
|
flowTemplateId: selectedTemplateId as Id<'flowTemplates'>,
|
|
targetType: targetType.trim(),
|
|
targetId: targetId.trim(),
|
|
managerId: managerId as Id<'usuarios'>
|
|
});
|
|
closeCreateModal();
|
|
goto(`/licitacoes/fluxos/${instanceId}`);
|
|
} catch (e) {
|
|
createError = e instanceof Error ? e.message : 'Erro ao criar instância';
|
|
} finally {
|
|
isCreating = false;
|
|
}
|
|
}
|
|
|
|
function getStatusBadge(status: 'active' | 'completed' | 'cancelled') {
|
|
switch (status) {
|
|
case 'active':
|
|
return { class: 'badge-info', label: 'Em Andamento' };
|
|
case 'completed':
|
|
return { class: 'badge-success', label: 'Concluído' };
|
|
case 'cancelled':
|
|
return { class: 'badge-error', label: 'Cancelado' };
|
|
}
|
|
}
|
|
|
|
function formatDate(timestamp: number): string {
|
|
return new Date(timestamp).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
function getProgressPercentage(completed: number, total: number): number {
|
|
if (total === 0) return 0;
|
|
return Math.round((completed / total) * 100);
|
|
}
|
|
</script>
|
|
|
|
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-10">
|
|
<!-- Header -->
|
|
<section
|
|
class="border-info/25 from-info/10 via-base-100 to-secondary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-8 shadow-2xl"
|
|
>
|
|
<div class="bg-info/20 absolute top-10 -left-10 h-40 w-40 rounded-full blur-3xl"></div>
|
|
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
|
|
<div class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
|
<div class="max-w-3xl space-y-4">
|
|
<div class="flex items-center gap-4">
|
|
<a href="/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>
|
|
Templates
|
|
</a>
|
|
<span
|
|
class="border-info/40 bg-info/10 text-info inline-flex w-fit items-center gap-2 rounded-full border px-4 py-1 text-xs font-semibold tracking-[0.28em] uppercase"
|
|
>
|
|
Execução
|
|
</span>
|
|
</div>
|
|
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
|
|
Instâncias de Fluxo
|
|
</h1>
|
|
<p class="text-base-content/70 text-base leading-relaxed sm:text-lg">
|
|
Acompanhe e gerencie as execuções de fluxos de trabalho. Visualize o progresso,
|
|
documentos e responsáveis de cada etapa.
|
|
</p>
|
|
</div>
|
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center">
|
|
<!-- Filtro de status -->
|
|
<select
|
|
class="select select-bordered"
|
|
bind:value={statusFilter}
|
|
>
|
|
<option value={undefined}>Todos os status</option>
|
|
<option value="active">Em Andamento</option>
|
|
<option value="completed">Concluído</option>
|
|
<option value="cancelled">Cancelado</option>
|
|
</select>
|
|
|
|
<ActionGuard recurso="fluxos_instancias" acao="criar">
|
|
<button class="btn btn-info shadow-lg" onclick={openCreateModal}>
|
|
<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="M12 4v16m8-8H4"
|
|
/>
|
|
</svg>
|
|
Nova Instância
|
|
</button>
|
|
</ActionGuard>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Lista de Instâncias -->
|
|
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg">
|
|
{#if instancesQuery.isLoading}
|
|
<div class="flex items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-info"></span>
|
|
</div>
|
|
{:else if !instancesQuery.data || instancesQuery.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"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="1.5"
|
|
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>
|
|
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Nenhuma instância encontrada</h3>
|
|
<p class="text-base-content/50 mt-2">
|
|
{statusFilter ? 'Não há instâncias com este status.' : 'Clique em "Nova Instância" para iniciar um fluxo.'}
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="overflow-x-auto">
|
|
<table class="table w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>Template</th>
|
|
<th>Alvo</th>
|
|
<th>Gerente</th>
|
|
<th>Progresso</th>
|
|
<th>Status</th>
|
|
<th>Iniciado em</th>
|
|
<th class="text-right">Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each instancesQuery.data as instance (instance._id)}
|
|
{@const statusBadge = getStatusBadge(instance.status)}
|
|
{@const progressPercent = getProgressPercentage(instance.progress.completed, instance.progress.total)}
|
|
<tr class="hover">
|
|
<td>
|
|
<div class="font-medium">{instance.templateName ?? 'Template desconhecido'}</div>
|
|
</td>
|
|
<td>
|
|
<div class="text-sm">
|
|
<span class="badge badge-outline badge-sm">{instance.targetType}</span>
|
|
<span class="text-base-content/60 ml-1">{instance.targetId}</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-base-content/70">{instance.managerName ?? '-'}</td>
|
|
<td>
|
|
<div class="flex items-center gap-2">
|
|
<progress
|
|
class="progress progress-info w-20"
|
|
value={progressPercent}
|
|
max="100"
|
|
></progress>
|
|
<span class="text-xs text-base-content/60">
|
|
{instance.progress.completed}/{instance.progress.total}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge {statusBadge.class}">{statusBadge.label}</span>
|
|
</td>
|
|
<td class="text-base-content/60 text-sm">{formatDate(instance.startedAt)}</td>
|
|
<td class="text-right">
|
|
<a
|
|
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">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
Ver
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Modal de Criação -->
|
|
{#if showCreateModal}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box max-w-2xl">
|
|
<h3 class="text-lg font-bold">Nova Instância de Fluxo</h3>
|
|
|
|
{#if createError}
|
|
<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>{createError}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<form onsubmit={(e) => { e.preventDefault(); handleCreate(); }} class="mt-4 space-y-4">
|
|
<div class="form-control">
|
|
<label class="label" for="template-select">
|
|
<span class="label-text">Template de Fluxo</span>
|
|
</label>
|
|
<select
|
|
id="template-select"
|
|
bind:value={selectedTemplateId}
|
|
class="select select-bordered w-full"
|
|
required
|
|
>
|
|
<option value="">Selecione um template</option>
|
|
{#if publishedTemplatesQuery.data}
|
|
{#each publishedTemplatesQuery.data as template (template._id)}
|
|
<option value={template._id}>{template.name}</option>
|
|
{/each}
|
|
{/if}
|
|
</select>
|
|
<p class="label">
|
|
<span class="label-text-alt text-base-content/60">Apenas templates publicados podem ser instanciados</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<div class="form-control">
|
|
<label class="label" for="target-type">
|
|
<span class="label-text">Tipo do Alvo</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="target-type"
|
|
bind:value={targetType}
|
|
class="input input-bordered w-full"
|
|
placeholder="Ex: contrato, projeto"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="target-id">
|
|
<span class="label-text">Identificador do Alvo</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="target-id"
|
|
bind:value={targetId}
|
|
class="input input-bordered w-full"
|
|
placeholder="Ex: CT-2024-001"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="manager-select">
|
|
<span class="label-text">Gerente Responsável</span>
|
|
</label>
|
|
<select
|
|
id="manager-select"
|
|
bind:value={managerId}
|
|
class="select select-bordered w-full"
|
|
required
|
|
>
|
|
<option value="">Selecione um gerente</option>
|
|
{#if usuariosQuery.data}
|
|
{#each usuariosQuery.data as usuario (usuario._id)}
|
|
<option value={usuario._id}>{usuario.nome}</option>
|
|
{/each}
|
|
{/if}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" onclick={closeCreateModal} disabled={isCreating}>
|
|
Cancelar
|
|
</button>
|
|
<button type="submit" class="btn btn-info" disabled={isCreating}>
|
|
{#if isCreating}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Iniciar Fluxo
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<button type="button" class="modal-backdrop" onclick={closeCreateModal} aria-label="Fechar modal"></button>
|
|
</div>
|
|
{/if}
|
|
|