- 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.
434 lines
14 KiB
Svelte
434 lines
14 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 do filtro
|
|
let statusFilter = $state<'draft' | 'published' | 'archived' | undefined>(undefined);
|
|
|
|
// Query de templates
|
|
const templatesQuery = useQuery(
|
|
api.flows.listTemplates,
|
|
() => (statusFilter ? { status: statusFilter } : {})
|
|
);
|
|
|
|
// Modal de criação
|
|
let showCreateModal = $state(false);
|
|
let newTemplateName = $state('');
|
|
let newTemplateDescription = $state('');
|
|
let isCreating = $state(false);
|
|
let createError = $state<string | null>(null);
|
|
|
|
// Modal de confirmação de exclusão
|
|
let showDeleteModal = $state(false);
|
|
let templateToDelete = $state<{ _id: Id<'flowTemplates'>; name: string } | null>(null);
|
|
let isDeleting = $state(false);
|
|
let deleteError = $state<string | null>(null);
|
|
|
|
function openCreateModal() {
|
|
newTemplateName = '';
|
|
newTemplateDescription = '';
|
|
createError = null;
|
|
showCreateModal = true;
|
|
}
|
|
|
|
function closeCreateModal() {
|
|
showCreateModal = false;
|
|
newTemplateName = '';
|
|
newTemplateDescription = '';
|
|
createError = null;
|
|
}
|
|
|
|
async function handleCreate() {
|
|
if (!newTemplateName.trim()) {
|
|
createError = 'O nome é obrigatório';
|
|
return;
|
|
}
|
|
|
|
isCreating = true;
|
|
createError = null;
|
|
|
|
try {
|
|
const templateId = await client.mutation(api.flows.createTemplate, {
|
|
name: newTemplateName.trim(),
|
|
description: newTemplateDescription.trim() || undefined
|
|
});
|
|
closeCreateModal();
|
|
// Navegar para o editor
|
|
goto(`/fluxos/${templateId}/editor`);
|
|
} catch (e) {
|
|
createError = e instanceof Error ? e.message : 'Erro ao criar template';
|
|
} finally {
|
|
isCreating = false;
|
|
}
|
|
}
|
|
|
|
function openDeleteModal(template: { _id: Id<'flowTemplates'>; name: string }) {
|
|
templateToDelete = template;
|
|
deleteError = null;
|
|
showDeleteModal = true;
|
|
}
|
|
|
|
function closeDeleteModal() {
|
|
showDeleteModal = false;
|
|
templateToDelete = null;
|
|
deleteError = null;
|
|
}
|
|
|
|
async function handleDelete() {
|
|
if (!templateToDelete) return;
|
|
|
|
isDeleting = true;
|
|
deleteError = null;
|
|
|
|
try {
|
|
await client.mutation(api.flows.deleteTemplate, { id: templateToDelete._id });
|
|
closeDeleteModal();
|
|
} catch (e) {
|
|
deleteError = e instanceof Error ? e.message : 'Erro ao excluir template';
|
|
} finally {
|
|
isDeleting = false;
|
|
}
|
|
}
|
|
|
|
async function handleStatusChange(templateId: Id<'flowTemplates'>, newStatus: 'draft' | 'published' | 'archived') {
|
|
try {
|
|
await client.mutation(api.flows.updateTemplate, {
|
|
id: templateId,
|
|
status: newStatus
|
|
});
|
|
} catch (e) {
|
|
console.error('Erro ao atualizar status:', e);
|
|
}
|
|
}
|
|
|
|
function getStatusBadge(status: 'draft' | 'published' | 'archived') {
|
|
switch (status) {
|
|
case 'draft':
|
|
return { class: 'badge-warning', label: 'Rascunho' };
|
|
case 'published':
|
|
return { class: 'badge-success', label: 'Publicado' };
|
|
case 'archived':
|
|
return { class: 'badge-neutral', label: 'Arquivado' };
|
|
}
|
|
}
|
|
|
|
function formatDate(timestamp: number): string {
|
|
return new Date(timestamp).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-10">
|
|
<!-- Header -->
|
|
<section
|
|
class="border-secondary/25 from-secondary/10 via-base-100 to-primary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-8 shadow-2xl"
|
|
>
|
|
<div class="bg-secondary/20 absolute top-10 -left-10 h-40 w-40 rounded-full blur-3xl"></div>
|
|
<div class="bg-primary/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">
|
|
<span
|
|
class="border-secondary/40 bg-secondary/10 text-secondary inline-flex w-fit items-center gap-2 rounded-full border px-4 py-1 text-xs font-semibold tracking-[0.28em] uppercase"
|
|
>
|
|
Gestão de Fluxos
|
|
</span>
|
|
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
|
|
Templates de Fluxo
|
|
</h1>
|
|
<p class="text-base-content/70 text-base leading-relaxed sm:text-lg">
|
|
Crie e gerencie templates de fluxo de trabalho. Templates definem os passos e
|
|
responsabilidades que serão instanciados para projetos ou contratos.
|
|
</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="draft">Rascunho</option>
|
|
<option value="published">Publicado</option>
|
|
<option value="archived">Arquivado</option>
|
|
</select>
|
|
|
|
<ActionGuard recurso="fluxos_templates" acao="criar">
|
|
<button class="btn btn-secondary 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>
|
|
Novo Template
|
|
</button>
|
|
</ActionGuard>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Lista de Templates -->
|
|
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg">
|
|
{#if templatesQuery.isLoading}
|
|
<div class="flex items-center justify-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-secondary"></span>
|
|
</div>
|
|
{:else if !templatesQuery.data || templatesQuery.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-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Nenhum template encontrado</h3>
|
|
<p class="text-base-content/50 mt-2">
|
|
{statusFilter ? 'Não há templates com este status.' : 'Clique em "Novo Template" para criar o primeiro.'}
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{#each templatesQuery.data as template (template._id)}
|
|
{@const statusBadge = getStatusBadge(template.status)}
|
|
<article
|
|
class="card bg-base-200/50 hover:bg-base-200 border transition-all duration-200 hover:shadow-md"
|
|
>
|
|
<div class="card-body">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<h2 class="card-title text-lg">{template.name}</h2>
|
|
<span class="badge {statusBadge.class}">{statusBadge.label}</span>
|
|
</div>
|
|
|
|
{#if template.description}
|
|
<p class="text-base-content/60 text-sm line-clamp-2">
|
|
{template.description}
|
|
</p>
|
|
{/if}
|
|
|
|
<div class="text-base-content/50 mt-2 flex flex-wrap gap-4 text-xs">
|
|
<span class="flex items-center gap-1">
|
|
<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="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 2" />
|
|
</svg>
|
|
{template.stepsCount} passos
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
{formatDate(template.createdAt)}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="card-actions mt-4 justify-between">
|
|
<div class="dropdown">
|
|
<button class="btn btn-ghost btn-sm" aria-label="Alterar status">
|
|
<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 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
|
</svg>
|
|
</button>
|
|
<ul class="dropdown-content menu bg-base-100 rounded-box z-10 w-52 p-2 shadow" role="menu">
|
|
{#if template.status !== 'draft'}
|
|
<li>
|
|
<button onclick={() => handleStatusChange(template._id, 'draft')}>
|
|
Voltar para Rascunho
|
|
</button>
|
|
</li>
|
|
{/if}
|
|
{#if template.status !== 'published'}
|
|
<li>
|
|
<button onclick={() => handleStatusChange(template._id, 'published')}>
|
|
Publicar
|
|
</button>
|
|
</li>
|
|
{/if}
|
|
{#if template.status !== 'archived'}
|
|
<li>
|
|
<button onclick={() => handleStatusChange(template._id, 'archived')}>
|
|
Arquivar
|
|
</button>
|
|
</li>
|
|
{/if}
|
|
<li class="mt-2 border-t pt-2">
|
|
<button class="text-error" onclick={() => openDeleteModal(template)}>
|
|
Excluir
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<a
|
|
href="/fluxos/{template._id}/editor"
|
|
class="btn btn-secondary 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="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>
|
|
Editar
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
|
|
<!-- Link para Instâncias -->
|
|
<section class="flex justify-center">
|
|
<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 Fluxos de Trabalho
|
|
</a>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Modal de Criação -->
|
|
{#if showCreateModal}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold">Novo Template 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-name">
|
|
<span class="label-text">Nome do Template</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="template-name"
|
|
bind:value={newTemplateName}
|
|
class="input input-bordered w-full"
|
|
placeholder="Ex: Fluxo de Aprovação de Contrato"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="template-description">
|
|
<span class="label-text">Descrição (opcional)</span>
|
|
</label>
|
|
<textarea
|
|
id="template-description"
|
|
bind:value={newTemplateDescription}
|
|
class="textarea textarea-bordered w-full"
|
|
placeholder="Descreva o propósito deste fluxo..."
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" onclick={closeCreateModal} disabled={isCreating}>
|
|
Cancelar
|
|
</button>
|
|
<button type="submit" class="btn btn-secondary" disabled={isCreating}>
|
|
{#if isCreating}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Criar e Editar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<button type="button" class="modal-backdrop" onclick={closeCreateModal} aria-label="Fechar modal"></button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Modal de Confirmação de Exclusão -->
|
|
{#if showDeleteModal && templateToDelete}
|
|
<div class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold text-error">Confirmar Exclusão</h3>
|
|
|
|
{#if deleteError}
|
|
<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>{deleteError}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<p class="py-4">
|
|
Tem certeza que deseja excluir o template <strong>{templateToDelete.name}</strong>?
|
|
</p>
|
|
<p class="text-base-content/60 text-sm">
|
|
Esta ação não pode ser desfeita. Templates com instâncias vinculadas não podem ser excluídos.
|
|
</p>
|
|
|
|
<div class="modal-action">
|
|
<button class="btn" onclick={closeDeleteModal} disabled={isDeleting}>
|
|
Cancelar
|
|
</button>
|
|
<button class="btn btn-error" onclick={handleDelete} disabled={isDeleting}>
|
|
{#if isDeleting}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{/if}
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="modal-backdrop" onclick={closeDeleteModal} aria-label="Fechar modal"></button>
|
|
</div>
|
|
{/if}
|
|
|