948 lines
27 KiB
Svelte
948 lines
27 KiB
Svelte
<script lang="ts">
|
||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||
import { resolve } from '$app/paths';
|
||
import { page } from '$app/stores';
|
||
|
||
const client = useConvexClient();
|
||
let instanceId = $derived($page.params.id as Id<'flowInstances'>);
|
||
|
||
// Query da instância com passos
|
||
const instanceQuery = useQuery(api.flows.getInstanceWithSteps, () => ({
|
||
id: instanceId
|
||
}));
|
||
|
||
// 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);
|
||
let processingError = $state<string | null>(null);
|
||
|
||
// Modal de reatribuição
|
||
let showReassignModal = $state(false);
|
||
let stepToReassign = $state<{
|
||
_id: Id<'flowInstanceSteps'>;
|
||
stepName: string;
|
||
} | null>(null);
|
||
let newAssigneeId = $state<Id<'usuarios'> | ''>('');
|
||
|
||
// Modal de notas
|
||
let showNotesModal = $state(false);
|
||
let stepForNotes = $state<{
|
||
_id: Id<'flowInstanceSteps'>;
|
||
stepName: string;
|
||
notes: string;
|
||
} | null>(null);
|
||
let editedNotes = $state('');
|
||
|
||
// Modal de upload
|
||
let showUploadModal = $state(false);
|
||
let stepForUpload = $state<{
|
||
_id: Id<'flowInstanceSteps'>;
|
||
stepName: string;
|
||
} | null>(null);
|
||
let uploadFile = $state<File | null>(null);
|
||
let isUploading = $state(false);
|
||
|
||
// Modal de confirmação de cancelamento
|
||
let showCancelModal = $state(false);
|
||
|
||
async function handleStartStep(stepId: Id<'flowInstanceSteps'>) {
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.updateStepStatus, {
|
||
instanceStepId: stepId,
|
||
status: 'in_progress'
|
||
});
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao iniciar passo';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
async function handleCompleteStep(stepId: Id<'flowInstanceSteps'>) {
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.completeStep, {
|
||
instanceStepId: stepId
|
||
});
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao completar passo';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
async function handleBlockStep(stepId: Id<'flowInstanceSteps'>) {
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.updateStepStatus, {
|
||
instanceStepId: stepId,
|
||
status: 'blocked'
|
||
});
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao bloquear passo';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
function openReassignModal(step: {
|
||
_id: Id<'flowInstanceSteps'>;
|
||
stepName: string;
|
||
assignedToId?: Id<'usuarios'>;
|
||
}) {
|
||
stepToReassign = step;
|
||
newAssigneeId = step.assignedToId ?? '';
|
||
showReassignModal = true;
|
||
}
|
||
|
||
function closeReassignModal() {
|
||
showReassignModal = false;
|
||
stepToReassign = null;
|
||
newAssigneeId = '';
|
||
}
|
||
|
||
async function handleReassign() {
|
||
if (!stepToReassign || !newAssigneeId) return;
|
||
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.reassignStep, {
|
||
instanceStepId: stepToReassign._id,
|
||
assignedToId: newAssigneeId as Id<'usuarios'>
|
||
});
|
||
closeReassignModal();
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao reatribuir passo';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
function openNotesModal(step: {
|
||
_id: Id<'flowInstanceSteps'>;
|
||
stepName: string;
|
||
notes?: string;
|
||
}) {
|
||
stepForNotes = { ...step, notes: step.notes ?? '' };
|
||
editedNotes = step.notes ?? '';
|
||
showNotesModal = true;
|
||
}
|
||
|
||
function closeNotesModal() {
|
||
showNotesModal = false;
|
||
stepForNotes = null;
|
||
editedNotes = '';
|
||
}
|
||
|
||
async function handleSaveNotes() {
|
||
if (!stepForNotes) return;
|
||
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.updateStepNotes, {
|
||
instanceStepId: stepForNotes._id,
|
||
notes: editedNotes
|
||
});
|
||
closeNotesModal();
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao salvar notas';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
function openUploadModal(step: { _id: Id<'flowInstanceSteps'>; stepName: string }) {
|
||
stepForUpload = step;
|
||
uploadFile = null;
|
||
showUploadModal = true;
|
||
}
|
||
|
||
function closeUploadModal() {
|
||
showUploadModal = false;
|
||
stepForUpload = null;
|
||
uploadFile = null;
|
||
}
|
||
|
||
function handleFileSelect(event: Event) {
|
||
const input = event.target as HTMLInputElement;
|
||
if (input.files && input.files.length > 0) {
|
||
uploadFile = input.files[0];
|
||
}
|
||
}
|
||
|
||
async function handleUpload() {
|
||
if (!stepForUpload || !uploadFile) return;
|
||
|
||
isUploading = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
// Gerar URL de upload
|
||
const uploadUrl = await client.mutation(api.flows.generateUploadUrl, {});
|
||
|
||
// Fazer upload do arquivo
|
||
const response = await fetch(uploadUrl, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': uploadFile.type },
|
||
body: uploadFile
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Falha no upload do arquivo');
|
||
}
|
||
|
||
const { storageId } = await response.json();
|
||
|
||
// Registrar o documento
|
||
await client.mutation(api.flows.registerDocument, {
|
||
flowInstanceStepId: stepForUpload._id,
|
||
storageId,
|
||
name: uploadFile.name
|
||
});
|
||
|
||
closeUploadModal();
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao fazer upload';
|
||
} finally {
|
||
isUploading = false;
|
||
}
|
||
}
|
||
|
||
async function handleDeleteDocument(documentId: Id<'flowInstanceDocuments'>) {
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.deleteDocument, { id: documentId });
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao excluir documento';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
async function handleCancelInstance() {
|
||
isProcessing = true;
|
||
processingError = null;
|
||
|
||
try {
|
||
await client.mutation(api.flows.cancelInstance, { id: instanceId });
|
||
showCancelModal = false;
|
||
} catch (e) {
|
||
processingError = e instanceof Error ? e.message : 'Erro ao cancelar instância';
|
||
} finally {
|
||
isProcessing = false;
|
||
}
|
||
}
|
||
|
||
function getStatusBadge(status: 'pending' | 'in_progress' | 'completed' | 'blocked') {
|
||
switch (status) {
|
||
case 'pending':
|
||
return { class: 'badge-ghost', label: 'Pendente', icon: 'clock' };
|
||
case 'in_progress':
|
||
return { class: 'badge-info', label: 'Em Progresso', icon: 'play' };
|
||
case 'completed':
|
||
return { class: 'badge-success', label: 'Concluído', icon: 'check' };
|
||
case 'blocked':
|
||
return { class: 'badge-error', label: 'Bloqueado', icon: 'x' };
|
||
}
|
||
}
|
||
|
||
function getInstanceStatusBadge(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 | undefined): string {
|
||
if (!timestamp) return '-';
|
||
return new Date(timestamp).toLocaleDateString('pt-BR', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
function isStepCurrent(stepId: Id<'flowInstanceSteps'>): boolean {
|
||
return instanceQuery.data?.instance.currentStepId === stepId;
|
||
}
|
||
|
||
function isOverdue(dueDate: number | undefined): boolean {
|
||
if (!dueDate) return false;
|
||
return Date.now() > dueDate;
|
||
}
|
||
</script>
|
||
|
||
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-10">
|
||
{#if instanceQuery.isLoading}
|
||
<div class="flex items-center justify-center py-24">
|
||
<span class="loading loading-spinner loading-lg text-info"></span>
|
||
</div>
|
||
{:else if !instanceQuery.data}
|
||
<div class="flex flex-col items-center justify-center py-24 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.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">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}
|
||
{@const steps = instanceQuery.data.steps}
|
||
{@const statusBadge = getInstanceStatusBadge(instance.status)}
|
||
|
||
<!-- 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">
|
||
<div class="mb-6 flex items-center gap-4">
|
||
<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>
|
||
Voltar
|
||
</a>
|
||
<span class="badge {statusBadge.class} badge-lg">{statusBadge.label}</span>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||
<div class="max-w-3xl space-y-4">
|
||
<h1 class="text-base-content text-3xl leading-tight font-black sm:text-4xl">
|
||
{instance.templateName ?? 'Fluxo'}
|
||
</h1>
|
||
<div class="flex flex-wrap gap-4">
|
||
{#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="text-base-content/60 flex items-center gap-2">
|
||
<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"
|
||
/>
|
||
</svg>
|
||
Gerente: {instance.managerName ?? '-'}
|
||
</div>
|
||
<div class="text-base-content/60 flex items-center gap-2">
|
||
<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>
|
||
Iniciado: {formatDate(instance.startedAt)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{#if instance.status === 'active'}
|
||
<button class="btn btn-error btn-outline" onclick={() => (showCancelModal = true)}>
|
||
<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="M6 18L18 6M6 6l12 12"
|
||
/>
|
||
</svg>
|
||
Cancelar Fluxo
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Erro global -->
|
||
{#if processingError}
|
||
<div class="alert alert-error">
|
||
<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>{processingError}</span>
|
||
<button class="btn btn-ghost btn-sm" onclick={() => (processingError = null)}>Fechar</button
|
||
>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Timeline de Passos -->
|
||
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg">
|
||
<h2 class="mb-6 text-xl font-bold">Timeline do Fluxo</h2>
|
||
|
||
<div class="space-y-6">
|
||
{#each steps as step, index (step._id)}
|
||
{@const stepStatus = getStatusBadge(step.status)}
|
||
{@const isCurrent = isStepCurrent(step._id)}
|
||
{@const overdue = step.status !== 'completed' && isOverdue(step.dueDate)}
|
||
|
||
<div class="relative flex gap-6 {index < steps.length - 1 ? 'pb-6' : ''}">
|
||
<!-- Linha conectora -->
|
||
{#if index < steps.length - 1}
|
||
<div
|
||
class="absolute top-10 bottom-0 left-5 w-0.5 {step.status === 'completed'
|
||
? 'bg-success'
|
||
: 'bg-base-300'}"
|
||
></div>
|
||
{/if}
|
||
|
||
<!-- Indicador de status -->
|
||
<div
|
||
class="z-10 flex h-10 w-10 shrink-0 items-center justify-center rounded-full {step.status ===
|
||
'completed'
|
||
? 'bg-success text-success-content'
|
||
: isCurrent
|
||
? 'bg-info text-info-content'
|
||
: step.status === 'blocked'
|
||
? 'bg-error text-error-content'
|
||
: 'bg-base-300 text-base-content'}"
|
||
>
|
||
{#if step.status === 'completed'}
|
||
<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="M5 13l4 4L19 7"
|
||
/>
|
||
</svg>
|
||
{:else if step.status === 'blocked'}
|
||
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||
/>
|
||
</svg>
|
||
{:else}
|
||
<span class="text-sm font-bold">{index + 1}</span>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Conteúdo do passo -->
|
||
<div
|
||
class="flex-1 rounded-xl border {isCurrent
|
||
? 'border-info bg-info/5'
|
||
: 'bg-base-200/50'} p-4"
|
||
>
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<h3 class="font-semibold">{step.stepName}</h3>
|
||
<span class="badge {stepStatus.class} badge-sm">{stepStatus.label}</span>
|
||
{#if overdue}
|
||
<span class="badge badge-warning badge-sm">Atrasado</span>
|
||
{/if}
|
||
</div>
|
||
{#if step.stepDescription}
|
||
<p class="text-base-content/60 mt-1 text-sm">{step.stepDescription}</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-3 w-3"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5"
|
||
/>
|
||
</svg>
|
||
{step.setorNome ?? 'Setor não definido'}
|
||
</span>
|
||
{#if step.assignedToName}
|
||
<span class="flex items-center gap-1">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
class="h-3 w-3"
|
||
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"
|
||
/>
|
||
</svg>
|
||
{step.assignedToName}
|
||
</span>
|
||
{/if}
|
||
{#if step.dueDate}
|
||
<span class="flex items-center gap-1 {overdue ? 'text-warning' : ''}">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
class="h-3 w-3"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||
/>
|
||
</svg>
|
||
Prazo: {formatDate(step.dueDate)}
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ações do passo -->
|
||
{#if instance.status === 'active'}
|
||
<div class="flex flex-wrap gap-2">
|
||
{#if step.status === 'pending'}
|
||
<button
|
||
class="btn btn-info btn-sm"
|
||
onclick={() => handleStartStep(step._id)}
|
||
disabled={isProcessing}
|
||
>
|
||
Iniciar
|
||
</button>
|
||
{:else if step.status === 'in_progress'}
|
||
<button
|
||
class="btn btn-success btn-sm"
|
||
onclick={() => handleCompleteStep(step._id)}
|
||
disabled={isProcessing}
|
||
>
|
||
Concluir
|
||
</button>
|
||
<button
|
||
class="btn btn-warning btn-sm"
|
||
onclick={() => handleBlockStep(step._id)}
|
||
disabled={isProcessing}
|
||
>
|
||
Bloquear
|
||
</button>
|
||
{:else if step.status === 'blocked'}
|
||
<button
|
||
class="btn btn-info btn-sm"
|
||
onclick={() => handleStartStep(step._id)}
|
||
disabled={isProcessing}
|
||
>
|
||
Desbloquear
|
||
</button>
|
||
{/if}
|
||
|
||
<button
|
||
class="btn btn-ghost btn-sm"
|
||
onclick={() => openReassignModal(step)}
|
||
aria-label="Reatribuir responsável"
|
||
>
|
||
<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"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-ghost btn-sm"
|
||
onclick={() => openNotesModal(step)}
|
||
aria-label="Editar notas"
|
||
>
|
||
<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>
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-ghost btn-sm"
|
||
onclick={() => openUploadModal(step)}
|
||
aria-label="Upload de documento"
|
||
>
|
||
<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="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Notas -->
|
||
{#if step.notes}
|
||
<div class="bg-base-300/50 mt-4 rounded-lg p-3">
|
||
<p class="text-base-content/70 text-sm whitespace-pre-wrap">{step.notes}</p>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Documentos -->
|
||
{#if step.documents && step.documents.length > 0}
|
||
<div class="mt-4">
|
||
<h4 class="text-base-content/70 mb-2 text-xs font-semibold uppercase">
|
||
Documentos
|
||
</h4>
|
||
<div class="flex flex-wrap gap-2">
|
||
{#each step.documents as doc (doc._id)}
|
||
<div class="badge badge-outline gap-2 py-3">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
class="h-3 w-3"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||
/>
|
||
</svg>
|
||
{doc.name}
|
||
<button
|
||
class="btn btn-ghost btn-xs text-error"
|
||
onclick={() => handleDeleteDocument(doc._id)}
|
||
aria-label="Excluir documento {doc.name}"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Datas de início/fim -->
|
||
{#if step.startedAt || step.finishedAt}
|
||
<div class="text-base-content/40 mt-4 flex gap-4 text-xs">
|
||
{#if step.startedAt}
|
||
<span>Iniciado: {formatDate(step.startedAt)}</span>
|
||
{/if}
|
||
{#if step.finishedAt}
|
||
<span>Concluído: {formatDate(step.finishedAt)}</span>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</section>
|
||
{/if}
|
||
</main>
|
||
|
||
<!-- Modal de Reatribuição -->
|
||
{#if showReassignModal && stepToReassign}
|
||
<div class="modal modal-open">
|
||
<div class="modal-box">
|
||
<h3 class="text-lg font-bold">Reatribuir Responsável</h3>
|
||
<p class="text-base-content/60 mt-2">
|
||
Selecione o novo responsável pelo passo <strong>{stepToReassign.stepName}</strong>
|
||
</p>
|
||
|
||
<div class="form-control mt-4">
|
||
<label class="label" for="assignee-select">
|
||
<span class="label-text">Responsável</span>
|
||
</label>
|
||
<select
|
||
id="assignee-select"
|
||
bind:value={newAssigneeId}
|
||
class="select select-bordered w-full"
|
||
>
|
||
<option value="">Selecione um usuário</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 class="btn" onclick={closeReassignModal} disabled={isProcessing}> Cancelar </button>
|
||
<button
|
||
class="btn btn-primary"
|
||
onclick={handleReassign}
|
||
disabled={isProcessing || !newAssigneeId}
|
||
>
|
||
{#if isProcessing}
|
||
<span class="loading loading-spinner loading-sm"></span>
|
||
{/if}
|
||
Reatribuir
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="modal-backdrop"
|
||
onclick={closeReassignModal}
|
||
aria-label="Fechar modal"
|
||
></button>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Modal de Notas -->
|
||
{#if showNotesModal && stepForNotes}
|
||
<div class="modal modal-open">
|
||
<div class="modal-box">
|
||
<h3 class="text-lg font-bold">Notas do Passo</h3>
|
||
<p class="text-base-content/60 mt-2">
|
||
Adicione ou edite notas para o passo <strong>{stepForNotes.stepName}</strong>
|
||
</p>
|
||
|
||
<div class="form-control mt-4">
|
||
<label class="label" for="notes-textarea">
|
||
<span class="label-text">Notas</span>
|
||
</label>
|
||
<textarea
|
||
id="notes-textarea"
|
||
bind:value={editedNotes}
|
||
class="textarea textarea-bordered w-full"
|
||
rows="5"
|
||
placeholder="Adicione observações, comentários ou informações relevantes..."
|
||
></textarea>
|
||
</div>
|
||
|
||
<div class="modal-action">
|
||
<button class="btn" onclick={closeNotesModal} disabled={isProcessing}> Cancelar </button>
|
||
<button class="btn btn-primary" onclick={handleSaveNotes} disabled={isProcessing}>
|
||
{#if isProcessing}
|
||
<span class="loading loading-spinner loading-sm"></span>
|
||
{/if}
|
||
Salvar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="modal-backdrop" onclick={closeNotesModal} aria-label="Fechar modal"
|
||
></button>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Modal de Upload -->
|
||
{#if showUploadModal && stepForUpload}
|
||
<div class="modal modal-open">
|
||
<div class="modal-box">
|
||
<h3 class="text-lg font-bold">Upload de Documento</h3>
|
||
<p class="text-base-content/60 mt-2">
|
||
Anexe um documento ao passo <strong>{stepForUpload.stepName}</strong>
|
||
</p>
|
||
|
||
<div class="form-control mt-4">
|
||
<label class="label" for="file-input">
|
||
<span class="label-text">Arquivo</span>
|
||
</label>
|
||
<input
|
||
type="file"
|
||
id="file-input"
|
||
class="file-input file-input-bordered w-full"
|
||
onchange={handleFileSelect}
|
||
/>
|
||
</div>
|
||
|
||
{#if uploadFile}
|
||
<p class="text-base-content/60 mt-2 text-sm">
|
||
Arquivo selecionado: <strong>{uploadFile.name}</strong>
|
||
</p>
|
||
{/if}
|
||
|
||
<div class="modal-action">
|
||
<button class="btn" onclick={closeUploadModal} disabled={isUploading}> Cancelar </button>
|
||
<button
|
||
class="btn btn-primary"
|
||
onclick={handleUpload}
|
||
disabled={isUploading || !uploadFile}
|
||
>
|
||
{#if isUploading}
|
||
<span class="loading loading-spinner loading-sm"></span>
|
||
{/if}
|
||
Enviar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="modal-backdrop"
|
||
onclick={closeUploadModal}
|
||
aria-label="Fechar modal"
|
||
></button>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Modal de Cancelamento -->
|
||
{#if showCancelModal}
|
||
<div class="modal modal-open">
|
||
<div class="modal-box">
|
||
<h3 class="text-error text-lg font-bold">Cancelar Fluxo</h3>
|
||
<p class="py-4">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.
|
||
</p>
|
||
|
||
<div class="modal-action">
|
||
<button class="btn" onclick={() => (showCancelModal = false)} disabled={isProcessing}>
|
||
Voltar
|
||
</button>
|
||
<button class="btn btn-error" onclick={handleCancelInstance} disabled={isProcessing}>
|
||
{#if isProcessing}
|
||
<span class="loading loading-spinner loading-sm"></span>
|
||
{/if}
|
||
Cancelar Fluxo
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="modal-backdrop"
|
||
onclick={() => (showCancelModal = false)}
|
||
aria-label="Fechar modal"
|
||
></button>
|
||
</div>
|
||
{/if}
|