diff --git a/.agent/rules/svelte-rules.md b/.agent/rules/svelte.md similarity index 92% rename from .agent/rules/svelte-rules.md rename to .agent/rules/svelte.md index 1737d67..0d1ee91 100644 --- a/.agent/rules/svelte-rules.md +++ b/.agent/rules/svelte.md @@ -1,7 +1,5 @@ --- -trigger: model_decision -description: whenever you're working with Svelte files -globs: **/*.svelte.ts,**/*.svelte +trigger: always_on --- You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 046dd22..2bc62d2 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -227,7 +227,8 @@ { label: 'Meus Processos', link: '/fluxos', - permission: { recurso: 'fluxos_instancias', acao: 'listar' } + permission: { recurso: 'fluxos_instancias', acao: 'listar' }, + exact: true }, { label: 'Modelos de Fluxo', diff --git a/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosExcel.ts b/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosExcel.ts new file mode 100644 index 0000000..57310b3 --- /dev/null +++ b/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosExcel.ts @@ -0,0 +1,302 @@ +import { api } from '@sgse-app/backend/convex/_generated/api'; +import type { FunctionReturnType } from 'convex/server'; +import ExcelJS from 'exceljs'; +import logoGovPE from '$lib/assets/logo_governo_PE.png'; + +export type RelatorioPlanejamentosData = FunctionReturnType< + typeof api.planejamentos.gerarRelatorio +>; + +function formatDateTime(ts: number | undefined): string { + if (!ts) return ''; + return new Date(ts).toLocaleString('pt-BR'); +} + +function formatDateYMD(ymd: string): string { + const [y, m, d] = ymd.split('-'); + if (!y || !m || !d) return ymd; + return `${d}/${m}/${y}`; +} + +function argb(hex: string): { argb: string } { + return { argb: hex.replace('#', '').toUpperCase().padStart(8, 'FF') }; +} + +function statusLabel(status: string): string { + switch (status) { + case 'rascunho': + return 'Rascunho'; + case 'gerado': + return 'Gerado'; + case 'cancelado': + return 'Cancelado'; + default: + return status; + } +} + +function applyHeaderRowStyle(row: ExcelJS.Row) { + row.height = 22; + row.eachCell((cell) => { + cell.font = { bold: true, size: 11, color: argb('#FFFFFF') }; + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: argb('#2980B9') }; + cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; + cell.border = { + top: { style: 'thin', color: argb('#000000') }, + bottom: { style: 'thin', color: argb('#000000') }, + left: { style: 'thin', color: argb('#000000') }, + right: { style: 'thin', color: argb('#000000') } + }; + }); +} + +function applyZebraRowStyle(row: ExcelJS.Row, isEven: boolean) { + row.eachCell((cell) => { + cell.font = { size: 10, color: argb('#000000') }; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: argb(isEven ? '#F8F9FA' : '#FFFFFF') + }; + cell.border = { + top: { style: 'thin', color: argb('#E0E0E0') }, + bottom: { style: 'thin', color: argb('#E0E0E0') }, + left: { style: 'thin', color: argb('#E0E0E0') }, + right: { style: 'thin', color: argb('#E0E0E0') } + }; + }); +} + +async function tryLoadLogoBuffer(): Promise { + try { + const response = await fetch(logoGovPE); + if (response.ok) return await response.arrayBuffer(); + } catch { + // ignore + } + + try { + const logoImg = new Image(); + logoImg.crossOrigin = 'anonymous'; + logoImg.src = logoGovPE; + await new Promise((resolve, reject) => { + logoImg.onload = () => resolve(); + logoImg.onerror = () => reject(); + setTimeout(() => reject(), 3000); + }); + + const canvas = document.createElement('canvas'); + canvas.width = logoImg.width; + canvas.height = logoImg.height; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + ctx.drawImage(logoImg, 0, 0); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error('Falha ao converter imagem'))), + 'image/png' + ); + }); + return await blob.arrayBuffer(); + } catch { + return null; + } +} + +function addTitleRow( + worksheet: ExcelJS.Worksheet, + title: string, + columnsCount: number, + workbook: ExcelJS.Workbook, + logoBuffer: ArrayBuffer | null +) { + if (columnsCount <= 0) return; + + worksheet.getRow(1).height = 60; + + if (columnsCount >= 3) { + worksheet.mergeCells(1, 1, 1, 2); + } + + const logoCell = worksheet.getCell(1, 1); + logoCell.alignment = { vertical: 'middle', horizontal: 'left' }; + if (columnsCount >= 3) { + logoCell.border = { right: { style: 'thin', color: argb('#E0E0E0') } }; + } + + if (logoBuffer) { + type WorkbookWithImage = { addImage(image: { buffer: Uint8Array; extension: string }): number }; + const workbookWithImage = workbook as unknown as WorkbookWithImage; + const logoId = workbookWithImage.addImage({ + buffer: new Uint8Array(logoBuffer), + extension: 'png' + }); + worksheet.addImage(logoId, { tl: { col: 0, row: 0 }, ext: { width: 140, height: 55 } }); + } + + if (columnsCount === 1) { + const titleCell = worksheet.getCell(1, 1); + titleCell.value = title; + titleCell.font = { bold: true, size: 18, color: argb('#2980B9') }; + titleCell.alignment = { vertical: 'middle', horizontal: 'center' }; + return; + } + + if (columnsCount === 2) { + const titleCell = worksheet.getCell(1, 2); + titleCell.value = title; + titleCell.font = { bold: true, size: 18, color: argb('#2980B9') }; + titleCell.alignment = { vertical: 'middle', horizontal: 'center' }; + return; + } + + worksheet.mergeCells(1, 3, 1, columnsCount); + const titleCell = worksheet.getCell(1, 3); + titleCell.value = title; + titleCell.font = { bold: true, size: 18, color: argb('#2980B9') }; + titleCell.alignment = { vertical: 'middle', horizontal: 'center' }; +} + +function downloadExcel(buffer: ArrayBuffer, fileName: string) { + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + link.click(); + window.URL.revokeObjectURL(url); +} + +export async function exportarRelatorioPlanejamentosXLSX( + relatorio: RelatorioPlanejamentosData +): Promise { + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'SGSE - Sistema de Gerenciamento'; + workbook.created = new Date(); + + const logoBuffer = await tryLoadLogoBuffer(); + + // ===== Aba: Resumo ===== + { + const ws = workbook.addWorksheet('Resumo'); + ws.columns = [ + { header: 'Campo', key: 'campo', width: 35 }, + { header: 'Valor', key: 'valor', width: 30 } + ]; + + addTitleRow(ws, 'RELATÓRIO DE PLANEJAMENTOS', 2, workbook, logoBuffer); + + const headerRow = ws.getRow(2); + headerRow.values = ['Campo', 'Valor']; + applyHeaderRowStyle(headerRow); + + const rows: Array<{ campo: string; valor: string | number }> = [ + { + campo: 'Período início', + valor: relatorio.filtros.periodoInicio + ? formatDateTime(relatorio.filtros.periodoInicio) + : '' + }, + { + campo: 'Período fim', + valor: relatorio.filtros.periodoFim ? formatDateTime(relatorio.filtros.periodoFim) : '' + }, + { campo: 'Texto (filtro)', valor: relatorio.filtros.texto ?? '' }, + { + campo: 'Status (filtro)', + valor: relatorio.filtros.statuses?.map(statusLabel).join(', ') ?? '' + }, + { campo: 'Total de planejamentos', valor: relatorio.resumo.totalPlanejamentos }, + { campo: 'Total valor estimado', valor: relatorio.resumo.totalValorEstimado } + ]; + + rows.forEach((r, idx) => { + const row = ws.addRow({ campo: r.campo, valor: r.valor }); + applyZebraRowStyle(row, idx % 2 === 1); + row.getCell(2).alignment = { + vertical: 'middle', + horizontal: typeof r.valor === 'number' ? 'right' : 'left' + }; + }); + + ws.addRow({ campo: '', valor: '' }); + const sectionRow = ws.addRow({ campo: 'Totais por status', valor: '' }); + sectionRow.font = { bold: true, size: 11, color: argb('#2980B9') }; + sectionRow.alignment = { vertical: 'middle', horizontal: 'left' }; + + relatorio.resumo.totalPorStatus.forEach((s, idx) => { + const row = ws.addRow({ campo: statusLabel(s.status), valor: s.count }); + applyZebraRowStyle(row, idx % 2 === 1); + row.getCell(2).alignment = { vertical: 'middle', horizontal: 'right' }; + }); + + ws.getColumn(2).numFmt = '#,##0.00'; + ws.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }]; + } + + // ===== Aba: Planejamentos ===== + { + const ws = workbook.addWorksheet('Planejamentos'); + ws.columns = [ + { header: 'Título', key: 'titulo', width: 40 }, + { header: 'Status', key: 'status', width: 15 }, + { header: 'Responsável', key: 'responsavel', width: 25 }, + { header: 'Data', key: 'data', width: 15 }, + { header: 'Ação', key: 'acao', width: 25 }, + { header: 'Itens', key: 'itens', width: 10 }, + { header: 'Estimado (R$)', key: 'estimado', width: 18 }, + { header: 'Criado em', key: 'criadoEm', width: 20 } + ]; + + addTitleRow(ws, 'RELATÓRIO DE PLANEJAMENTOS — LISTA', ws.columns.length, workbook, logoBuffer); + + const headerRow = ws.getRow(2); + headerRow.values = ws.columns.map((c) => c.header as string); + applyHeaderRowStyle(headerRow); + + relatorio.planejamentos.forEach((p, idx) => { + const row = ws.addRow({ + titulo: p.titulo, + status: statusLabel(p.status), + responsavel: p.responsavelNome, + data: formatDateYMD(p.data), + acao: p.acaoNome ?? '', + itens: p.itensCount, + estimado: p.valorEstimadoTotal, + criadoEm: new Date(p.criadoEm) + }); + + applyZebraRowStyle(row, idx % 2 === 1); + + row.getCell(2).alignment = { vertical: 'middle', horizontal: 'center' }; + row.getCell(4).alignment = { vertical: 'middle', horizontal: 'center' }; + row.getCell(6).alignment = { vertical: 'middle', horizontal: 'center' }; + row.getCell(7).alignment = { vertical: 'middle', horizontal: 'right' }; + row.getCell(8).alignment = { vertical: 'middle', horizontal: 'center' }; + + const statusCell = row.getCell(2); + if (p.status === 'gerado') { + statusCell.fill = { type: 'pattern', pattern: 'solid', fgColor: argb('#D4EDDA') }; + statusCell.font = { size: 10, color: argb('#155724') }; + } else if (p.status === 'cancelado') { + statusCell.fill = { type: 'pattern', pattern: 'solid', fgColor: argb('#F8D7DA') }; + statusCell.font = { size: 10, color: argb('#721C24') }; + } + }); + + ws.getColumn(7).numFmt = '"R$" #,##0.00'; + ws.getColumn(8).numFmt = 'dd/mm/yyyy hh:mm'; + + ws.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }]; + ws.autoFilter = { + from: { row: 2, column: 1 }, + to: { row: 2, column: ws.columns.length } + }; + } + + const nomeArquivo = `relatorio-planejamentos-${new Date().toISOString().slice(0, 10)}.xlsx`; + const buffer = await workbook.xlsx.writeBuffer(); + downloadExcel(buffer, nomeArquivo); +} diff --git a/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosPDF.ts b/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosPDF.ts new file mode 100644 index 0000000..b19c31a --- /dev/null +++ b/apps/web/src/lib/utils/planejamentos/relatorioPlanejamentosPDF.ts @@ -0,0 +1,140 @@ +import { api } from '@sgse-app/backend/convex/_generated/api'; +import type { FunctionReturnType } from 'convex/server'; +import jsPDF from 'jspdf'; +import autoTable from 'jspdf-autotable'; + +export type RelatorioPlanejamentosData = FunctionReturnType< + typeof api.planejamentos.gerarRelatorio +>; + +function formatCurrencyBRL(n: number): string { + return n.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); +} + +function formatDateTime(ts: number | undefined): string { + if (!ts) return '-'; + return new Date(ts).toLocaleString('pt-BR'); +} + +function formatDateYMD(ymd: string): string { + const [y, m, d] = ymd.split('-'); + if (!y || !m || !d) return ymd; + return `${d}/${m}/${y}`; +} + +function getPeriodoLabel(filtros: RelatorioPlanejamentosData['filtros']): string { + const inicio = filtros.periodoInicio ? formatDateTime(filtros.periodoInicio) : '—'; + const fim = filtros.periodoFim ? formatDateTime(filtros.periodoFim) : '—'; + return `${inicio} até ${fim}`; +} + +export function gerarRelatorioPlanejamentosPDF(relatorio: RelatorioPlanejamentosData): void { + const doc = new jsPDF({ orientation: 'landscape' }); + + // Título + doc.setFontSize(18); + doc.setTextColor(102, 126, 234); + doc.text('Relatório de Planejamentos', 14, 18); + + // Subtítulo + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + doc.text(`Período: ${getPeriodoLabel(relatorio.filtros)}`, 14, 26); + doc.setFontSize(9); + doc.text(`Gerado em: ${new Date().toLocaleString('pt-BR')}`, 14, 32); + + let yPos = 40; + + // Resumo + doc.setFontSize(12); + doc.setTextColor(102, 126, 234); + doc.text('Resumo', 14, yPos); + yPos += 6; + + autoTable(doc, { + startY: yPos, + head: [['Planejamentos', 'Total Estimado']], + body: [ + [ + String(relatorio.resumo.totalPlanejamentos), + formatCurrencyBRL(relatorio.resumo.totalValorEstimado) + ] + ], + theme: 'striped', + headStyles: { fillColor: [102, 126, 234] }, + styles: { fontSize: 9 } + }); + + type JsPDFWithAutoTable = jsPDF & { lastAutoTable?: { finalY: number } }; + yPos = ((doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPos) + 10; + + // Totais por status + doc.setFontSize(12); + doc.setTextColor(102, 126, 234); + doc.text('Totais por status', 14, yPos); + yPos += 6; + + autoTable(doc, { + startY: yPos, + head: [['Status', 'Quantidade']], + body: relatorio.resumo.totalPorStatus.map((s) => [s.status, String(s.count)]), + theme: 'grid', + headStyles: { fillColor: [102, 126, 234] }, + styles: { fontSize: 9 } + }); + + yPos = ((doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPos) + 12; + + // Planejamentos + doc.setFontSize(12); + doc.setTextColor(102, 126, 234); + doc.text('Planejamentos', 14, yPos); + yPos += 6; + + const planejamentosBody = relatorio.planejamentos.map((p) => [ + p.titulo, + p.status, + p.responsavelNome, + p.acaoNome ?? '—', + formatDateYMD(p.data), + String(p.itensCount), + formatCurrencyBRL(p.valorEstimadoTotal), + formatDateTime(p.criadoEm) + ]); + + autoTable(doc, { + startY: yPos, + head: [['Título', 'Status', 'Responsável', 'Ação', 'Data', 'Itens', 'Estimado', 'Criado em']], + body: planejamentosBody, + theme: 'grid', + headStyles: { fillColor: [102, 126, 234] }, + styles: { fontSize: 8 }, + columnStyles: { + 0: { cellWidth: 60 }, + 1: { cellWidth: 20 }, + 2: { cellWidth: 40 }, + 3: { cellWidth: 40 }, + 4: { cellWidth: 25 }, + 5: { cellWidth: 15 }, + 6: { cellWidth: 30 }, + 7: { cellWidth: 30 } + } + }); + + // Footer com paginação + const pageCount = doc.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.setTextColor(128, 128, 128); + doc.text( + `SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`, + doc.internal.pageSize.getWidth() / 2, + doc.internal.pageSize.getHeight() - 8, + { align: 'center' } + ); + } + + const fileName = `relatorio-planejamentos-${new Date().toISOString().slice(0, 10)}.pdf`; + doc.save(fileName); +} diff --git a/apps/web/src/routes/(dashboard)/fluxos/+page.svelte b/apps/web/src/routes/(dashboard)/fluxos/+page.svelte index 41b48a0..861c710 100644 --- a/apps/web/src/routes/(dashboard)/fluxos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/fluxos/+page.svelte @@ -3,50 +3,52 @@ import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { useConvexClient, useQuery } from 'convex-svelte'; import { goto } from '$app/navigation'; + import { resolve } from '$app/paths'; const client = useConvexClient(); - // Estado do filtro - let statusFilter = $state<'draft' | 'published' | 'archived' | undefined>(undefined); + // Estado dos filtros + let statusFilter = $state<'active' | 'completed' | 'cancelled' | undefined>(undefined); - // Query de templates - const templatesQuery = useQuery(api.flows.listTemplates, () => + // 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 newTemplateName = $state(''); - let newTemplateDescription = $state(''); + let selectedTemplateId = $state | ''>(''); + let contratoId = $state | ''>(''); + let managerId = $state | ''>(''); let isCreating = $state(false); let createError = $state(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(null); + // Query de usuários (para seleção de gerente) + const usuariosQuery = useQuery(api.usuarios.listar, {}); + + // Query de contratos (para seleção) + const contratosQuery = useQuery(api.contratos.listar, {}); function openCreateModal() { - newTemplateName = ''; - newTemplateDescription = ''; + selectedTemplateId = ''; + contratoId = ''; + managerId = ''; createError = null; showCreateModal = true; } function closeCreateModal() { showCreateModal = false; - newTemplateName = ''; - newTemplateDescription = ''; - createError = null; } async function handleCreate() { - if (!newTemplateName.trim()) { - createError = 'O nome é obrigatório'; + if (!selectedTemplateId || !managerId) { + createError = 'Template e gerente são obrigatórios'; return; } @@ -54,72 +56,28 @@ createError = null; try { - const templateId = await client.mutation(api.flows.createTemplate, { - name: newTemplateName.trim(), - description: newTemplateDescription.trim() || undefined + const instanceId = await client.mutation(api.flows.instantiateFlow, { + flowTemplateId: selectedTemplateId as Id<'flowTemplates'>, + contratoId: contratoId ? (contratoId as Id<'contratos'>) : undefined, + managerId: managerId as Id<'usuarios'> }); closeCreateModal(); - // Navegar para o editor - goto(`/fluxos/${templateId}/editor`); + goto(resolve(`/fluxos/instancias/${instanceId}`)); } catch (e) { - createError = e instanceof Error ? e.message : 'Erro ao criar template'; + createError = e instanceof Error ? e.message : 'Erro ao criar instância'; } 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') { + function getStatusBadge(status: 'active' | 'completed' | 'cancelled') { 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' }; + 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' }; } } @@ -127,43 +85,70 @@ return new Date(timestamp).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', - year: 'numeric' + 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); + }
-
-
+
+
- - Gestão de Fluxos - +
+ + + Templates + + + Execução + +

- Templates de Fluxo + Instâncias de Fluxo

- Crie e gerencie templates de fluxo de trabalho. Templates definem os passos e - responsabilidades que serão instanciados para projetos ou contratos. + Acompanhe e gerencie as execuções de fluxos de trabalho. Visualize o progresso, documentos + e responsáveis de cada etapa.

-
- +
- {#if templatesQuery.isLoading} + {#if instancesQuery.isLoading}
- +
- {:else if !templatesQuery.data || templatesQuery.data.length === 0} + {:else if !instancesQuery.data || instancesQuery.data.length === 0}
-

Nenhum template encontrado

+

+ Nenhuma instância encontrada +

{statusFilter - ? 'Não há templates com este status.' - : 'Clique em "Novo Template" para criar o primeiro.'} + ? 'Não há instâncias com este status.' + : 'Clique em "Nova Instância" para iniciar um fluxo.'}

{:else} -
- {#each templatesQuery.data as template (template._id)} - {@const statusBadge = getStatusBadge(template.status)} -
-
-
-

{template.name}

- {statusBadge.label} -
- - {#if template.description} -

- {template.description} -

- {/if} - -
- - + + + + + + + + + + + + + {#each instancesQuery.data as instance (instance._id)} + {@const statusBadge = getStatusBadge(instance.status)} + {@const progressPercent = getProgressPercentage( + instance.progress.completed, + instance.progress.total + )} + + + + + + + + + + {/each} + +
TemplateContratoGerenteProgressoStatusIniciado emAções
+
{instance.templateName ?? 'Template desconhecido'}
+
+ {#if instance.contratoId} + {instance.contratoId} + {:else} + - + {/if} + {instance.managerName ?? '-'} +
+ + + {instance.progress.completed}/{instance.progress.total} + +
+
+ {statusBadge.label} + {formatDate(instance.startedAt)} + - - - {template.stepsCount} passos - - - - {formatDate(template.createdAt)} - - - - - - - {/each} + Ver + +
{/if}
- - -
- - - Ver Fluxos de Trabalho - -
{#if showCreateModal}