feat: enhance DocumentacaoCard with visualizer button and improve PDF generation with structured content and metadata handling
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { FileText, Calendar, Tag, CheckCircle2, Circle } from 'lucide-svelte';
|
||||
import { FileText, Calendar, Tag, CheckCircle2, Circle, Eye } from 'lucide-svelte';
|
||||
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
type TipoDocumento =
|
||||
@@ -35,9 +35,10 @@
|
||||
documento: Documento;
|
||||
selecionado?: boolean;
|
||||
onToggleSelecao?: () => void;
|
||||
onVisualizar?: () => void;
|
||||
}
|
||||
|
||||
let { documento, selecionado = false, onToggleSelecao }: Props = $props();
|
||||
let { documento, selecionado = false, onToggleSelecao, onVisualizar }: Props = $props();
|
||||
|
||||
const tipoLabels: Record<string, string> = {
|
||||
query: 'Query',
|
||||
@@ -63,23 +64,25 @@
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="group relative flex cursor-pointer flex-col gap-4 overflow-hidden rounded-2xl border border-base-300 bg-base-100 p-6 shadow-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-xl"
|
||||
class="group relative flex flex-col gap-4 overflow-hidden rounded-2xl border border-base-300 bg-base-100 p-6 shadow-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-xl {onToggleSelecao ? '' : 'cursor-pointer'}"
|
||||
onclick={() => {
|
||||
if (onToggleSelecao) {
|
||||
onToggleSelecao();
|
||||
} else {
|
||||
// Se não houver seleção habilitada, redireciona para a página do documento
|
||||
if (!onToggleSelecao) {
|
||||
window.location.href = resolve(`/ti/documentacao/${documento._id}`);
|
||||
}
|
||||
// Se houver seleção, não faz nada no clique do card (apenas o checkbox seleciona)
|
||||
}}
|
||||
>
|
||||
<!-- Checkbox de seleção -->
|
||||
{#if onToggleSelecao}
|
||||
<button
|
||||
class="absolute right-4 top-4 z-10"
|
||||
class="absolute right-4 top-4 z-20 cursor-pointer"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onToggleSelecao();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if selecionado}
|
||||
<CheckCircle2 class="text-primary h-6 w-6" />
|
||||
@@ -134,12 +137,31 @@
|
||||
<div class="flex items-center gap-2 text-xs text-base-content/50">
|
||||
<Calendar class="h-4 w-4" />
|
||||
<span>
|
||||
{format(new Date(documento.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
|
||||
{documento.atualizadoEm && !isNaN(new Date(documento.atualizadoEm).getTime())
|
||||
? format(new Date(documento.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })
|
||||
: 'Data não disponível'}
|
||||
</span>
|
||||
</div>
|
||||
{#if documento.visualizacoes !== undefined}
|
||||
<span class="text-base-content/50 text-xs">{documento.visualizacoes} visualizações</span>
|
||||
{/if}
|
||||
<div class="flex items-center gap-3">
|
||||
{#if documento.visualizacoes !== undefined}
|
||||
<span class="text-base-content/50 text-xs">{documento.visualizacoes} visualizações</span>
|
||||
{/if}
|
||||
{#if onVisualizar}
|
||||
<button
|
||||
class="btn btn-ghost btn-xs gap-1"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onVisualizar();
|
||||
}}
|
||||
title="Visualizar documento"
|
||||
type="button"
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
Visualizar
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<script lang="ts">
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { marked } from 'marked';
|
||||
import { X, BookOpen, FileText, Calendar, User, Tag, Eye } from 'lucide-svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
interface Props {
|
||||
documentoId: Id<'documentacao'> | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { documentoId, onClose }: Props = $props();
|
||||
|
||||
const documentoQuery = useQuery(
|
||||
api.documentacao.obterDocumento,
|
||||
documentoId ? { documentoId } : 'skip'
|
||||
);
|
||||
|
||||
// Gerar índice a partir do conteúdo Markdown
|
||||
const indice = $derived(() => {
|
||||
if (!documentoQuery?.conteudo) return [];
|
||||
|
||||
const linhas = documentoQuery.conteudo.split('\n');
|
||||
const indices: Array<{ nivel: number; titulo: string; id: string }> = [];
|
||||
|
||||
linhas.forEach((linha) => {
|
||||
const match = linha.match(/^(#{1,3})\s+(.+)$/);
|
||||
if (match) {
|
||||
const nivel = match[1].length;
|
||||
const titulo = match[2].trim();
|
||||
const id = titulo
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
indices.push({ nivel, titulo, id });
|
||||
}
|
||||
});
|
||||
|
||||
return indices;
|
||||
});
|
||||
|
||||
function scrollParaSecao(id: string) {
|
||||
const elemento = document.getElementById(id);
|
||||
if (elemento) {
|
||||
elemento.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Processar HTML para adicionar IDs aos títulos
|
||||
const conteudoHtml = $derived(() => {
|
||||
if (!documentoQuery) return '';
|
||||
|
||||
// Se já existe HTML renderizado, usar ele
|
||||
if (documentoQuery.conteudoHtml && documentoQuery.conteudoHtml.trim().length > 0) {
|
||||
let html = documentoQuery.conteudoHtml;
|
||||
|
||||
// Adicionar IDs aos títulos
|
||||
html = html.replace(/<h([1-3])>(.+?)<\/h[1-3]>/g, (match, nivel, titulo) => {
|
||||
const id = titulo
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
return `<h${nivel} id="${id}">${titulo}</h${nivel}>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Se não, converter Markdown para HTML
|
||||
if (documentoQuery.conteudo && documentoQuery.conteudo.trim().length > 0) {
|
||||
let html = marked.parse(documentoQuery.conteudo);
|
||||
|
||||
// Adicionar IDs aos títulos
|
||||
html = html.replace(/<h([1-3])>(.+?)<\/h[1-3]>/g, (match, nivel, titulo) => {
|
||||
const id = titulo
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
return `<h${nivel} id="${id}">${titulo}</h${nivel}>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if documentoId}
|
||||
{#if documentoQuery === undefined}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-6xl">
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={onClose}></div>
|
||||
</div>
|
||||
{:else if documentoQuery}
|
||||
{@const doc = documentoQuery}
|
||||
{#if doc}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-6xl max-h-[90vh] flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between border-b border-base-300 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<BookOpen class="text-primary h-6 w-6" />
|
||||
<h2 class="text-2xl font-bold">{doc.titulo || 'Documento'}</h2>
|
||||
</div>
|
||||
<button class="btn btn-circle btn-ghost btn-sm" onclick={onClose}>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo com scroll -->
|
||||
<div class="flex flex-1 gap-6 overflow-hidden">
|
||||
<!-- Índice lateral -->
|
||||
{#if indice().length > 0}
|
||||
<aside class="w-64 shrink-0 overflow-y-auto border-r border-base-300 pr-4">
|
||||
<div class="sticky top-0">
|
||||
<h3 class="text-base-content mb-4 text-sm font-semibold uppercase">Índice</h3>
|
||||
<nav class="space-y-1">
|
||||
{#each indice() as item}
|
||||
<button
|
||||
class="text-base-content/70 hover:text-primary block w-full text-left text-sm transition-colors {item.nivel === 1
|
||||
? 'font-semibold'
|
||||
: item.nivel === 2
|
||||
? 'ml-4 font-medium'
|
||||
: 'ml-8'}"
|
||||
onclick={() => scrollParaSecao(item.id)}
|
||||
>
|
||||
{item.titulo}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- Conteúdo principal -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- Metadados do Documento (carregados do card) -->
|
||||
<div class="bg-base-200 mb-6 rounded-lg p-4">
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<FileText class="text-base-content/50 h-4 w-4" />
|
||||
<span class="badge badge-primary">{doc.tipo}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="text-base-content/50 h-4 w-4" />
|
||||
<span class="text-base-content/70 font-medium">
|
||||
{doc.atualizadoEm && !isNaN(new Date(doc.atualizadoEm).getTime())
|
||||
? format(new Date(doc.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})
|
||||
: 'Data não disponível'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Eye class="text-base-content/50 h-4 w-4" />
|
||||
<span class="text-base-content/70 font-medium">
|
||||
{doc.visualizacoes || 0} {doc.visualizacoes === 1 ? 'visualização' : 'visualizações'}
|
||||
</span>
|
||||
</div>
|
||||
{#if doc.criadoPorUsuario}
|
||||
<div class="flex items-center gap-2">
|
||||
<User class="text-base-content/50 h-4 w-4" />
|
||||
<span class="text-base-content/70">{doc.criadoPorUsuario.nome}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if doc.tags && doc.tags.length > 0}
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2">
|
||||
<Tag class="text-base-content/50 h-5 w-5" />
|
||||
{#each doc.tags as tag}
|
||||
<span class="badge badge-outline badge-sm">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Conteúdo do Documento (carregado do card selecionado) -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-base-content mb-4 text-xl font-bold border-b border-base-300 pb-2">
|
||||
Conteúdo do Documento
|
||||
</h3>
|
||||
|
||||
<!-- Renderizar conteúdo quando disponível -->
|
||||
{#if doc.conteudo && doc.conteudo.trim().length > 0}
|
||||
<!-- Exibir conteúdo Markdown convertido para HTML -->
|
||||
<div class="prose prose-slate prose-lg max-w-none">
|
||||
{@html marked.parse(doc.conteudo)}
|
||||
</div>
|
||||
{:else if doc.conteudoHtml && doc.conteudoHtml.trim().length > 0}
|
||||
<!-- Exibir conteúdo HTML renderizado -->
|
||||
<div class="prose prose-slate prose-lg max-w-none">
|
||||
{@html doc.conteudoHtml}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Mensagem quando não há conteúdo -->
|
||||
<div class="bg-base-200 rounded-lg p-6 text-center">
|
||||
<p class="text-base-content/70 mb-2">Conteúdo não disponível para este documento.</p>
|
||||
{#if doc.arquivoOrigem}
|
||||
<p class="text-base-content/50 text-sm">
|
||||
Arquivo: <span class="font-mono">{doc.arquivoOrigem}</span>
|
||||
</p>
|
||||
{/if}
|
||||
{#if doc.funcaoOrigem}
|
||||
<p class="text-base-content/50 text-sm mt-1">
|
||||
Função: <span class="font-mono">{doc.funcaoOrigem}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Informações adicionais -->
|
||||
{#if doc.arquivoOrigem}
|
||||
<div class="bg-base-200 mt-8 rounded-lg p-6">
|
||||
<h3 class="text-base-content mb-4 flex items-center gap-2 text-lg font-semibold">
|
||||
<FileText class="h-5 w-5" />
|
||||
Informações do Arquivo
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Arquivo Origem:</span>
|
||||
<span class="text-base-content ml-2 font-mono text-sm">{doc.arquivoOrigem}</span>
|
||||
</div>
|
||||
{#if doc.funcaoOrigem}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Função Origem:</span>
|
||||
<span class="text-base-content ml-2 font-mono text-sm">{doc.funcaoOrigem}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-4 flex justify-end border-t border-base-300 pt-4">
|
||||
<button class="btn btn-ghost" onclick={onClose}>Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={onClose}></div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-6xl">
|
||||
<div class="bg-base-200 rounded-2xl border border-base-300 p-12 text-center">
|
||||
<h3 class="text-base-content mb-2 text-xl font-semibold">Documento não encontrado</h3>
|
||||
<p class="text-base-content/70">O documento solicitado não existe ou foi removido.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onclick={onClose}></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.prose) {
|
||||
color: hsl(var(--bc));
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:global(.prose h1) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
border-bottom: 2px solid hsl(var(--b3));
|
||||
padding-bottom: 0.5em;
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
:global(.prose h2) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 2em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.75em;
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
:global(.prose h3) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
:global(.prose code) {
|
||||
background-color: hsl(var(--b2));
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9em;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: hsl(var(--p));
|
||||
}
|
||||
|
||||
:global(.prose pre) {
|
||||
background-color: hsl(var(--b2));
|
||||
padding: 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid hsl(var(--p));
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { marked } from 'marked';
|
||||
import { X, Download, Loader2 } from 'lucide-svelte';
|
||||
|
||||
@@ -31,101 +32,224 @@
|
||||
}
|
||||
|
||||
const doc = new jsPDF();
|
||||
let yPos = 20;
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 20;
|
||||
const primaryColor = [102, 126, 234];
|
||||
const secondaryColor = [128, 128, 128];
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text('Biblioteca de Documentação SGSE', margin, yPos);
|
||||
yPos += 15;
|
||||
// Cabeçalho
|
||||
doc.setFillColor(...primaryColor);
|
||||
doc.rect(0, 0, pageWidth, 40, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(22);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Biblioteca de Documentação SGSE', pageWidth / 2, 25, { align: 'center' });
|
||||
|
||||
doc.setFontSize(12);
|
||||
let yPos = 50;
|
||||
|
||||
// Informações de geração
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`Gerado em: ${new Date().toLocaleString('pt-BR')}`, margin, yPos);
|
||||
yPos += 10;
|
||||
yPos += 6;
|
||||
doc.text(`Total de documentos: ${documentos.length}`, margin, yPos);
|
||||
yPos += 15;
|
||||
|
||||
// Índice
|
||||
// Índice com tabela
|
||||
doc.setFontSize(16);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Índice', margin, yPos);
|
||||
yPos += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
documentos.forEach((documento, index) => {
|
||||
if (yPos > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
const indiceData = documentos.map((doc, index) => [
|
||||
(index + 1).toString(),
|
||||
doc.titulo,
|
||||
doc.tipo,
|
||||
doc.versao
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['#', 'Título', 'Tipo', 'Versão']],
|
||||
body: indiceData,
|
||||
theme: 'striped',
|
||||
headStyles: {
|
||||
fillColor: primaryColor,
|
||||
textColor: [255, 255, 255],
|
||||
fontStyle: 'bold'
|
||||
},
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3
|
||||
},
|
||||
columnStyles: {
|
||||
0: { cellWidth: 15 },
|
||||
1: { cellWidth: 'auto' },
|
||||
2: { cellWidth: 40 },
|
||||
3: { cellWidth: 30 }
|
||||
}
|
||||
doc.text(`${index + 1}. ${documento.titulo}`, margin + 5, yPos);
|
||||
yPos += 7;
|
||||
});
|
||||
|
||||
yPos += 10;
|
||||
yPos = (doc as any).lastAutoTable.finalY + 15;
|
||||
|
||||
// Documentos
|
||||
for (let i = 0; i < documentos.length; i++) {
|
||||
const documento = documentos[i];
|
||||
|
||||
// Nova página para cada documento
|
||||
if (i > 0 || yPos > pageHeight - 50) {
|
||||
// Nova página para cada documento (exceto o primeiro)
|
||||
if (i > 0) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
} else if (yPos > pageHeight - 50) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
|
||||
// Título do documento
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text(documento.titulo, margin, yPos, { maxWidth: 170 });
|
||||
yPos += 10;
|
||||
// Cabeçalho do documento
|
||||
doc.setFillColor(...primaryColor);
|
||||
doc.rect(0, yPos - 10, pageWidth, 15, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(16);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${i + 1}. ${documento.titulo}`, margin, yPos + 3, { maxWidth: pageWidth - 2 * margin });
|
||||
yPos += 20;
|
||||
|
||||
// Metadados
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.text(`Tipo: ${documento.tipo} | Versão: ${documento.versao}`, margin, yPos);
|
||||
yPos += 8;
|
||||
// Metadados em tabela
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
body: [
|
||||
['Tipo', documento.tipo],
|
||||
['Versão', documento.versao],
|
||||
['Arquivo Origem', documento.arquivoOrigem || 'N/A']
|
||||
],
|
||||
theme: 'plain',
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 4
|
||||
},
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 50, fillColor: [240, 240, 240] },
|
||||
1: { cellWidth: 'auto' }
|
||||
}
|
||||
});
|
||||
|
||||
yPos = (doc as any).lastAutoTable.finalY + 10;
|
||||
|
||||
// Conteúdo
|
||||
doc.setFontSize(11);
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
// Converter Markdown para texto simples (remover formatação)
|
||||
// Processar Markdown de forma mais inteligente
|
||||
let conteudoTexto = documento.conteudo;
|
||||
// Remover markdown básico
|
||||
conteudoTexto = conteudoTexto.replace(/#{1,6}\s+/g, ''); // Headers
|
||||
conteudoTexto = conteudoTexto.replace(/\*\*(.*?)\*\*/g, '$1'); // Bold
|
||||
conteudoTexto = conteudoTexto.replace(/\*(.*?)\*/g, '$1'); // Italic
|
||||
conteudoTexto = conteudoTexto.replace(/`(.*?)`/g, '$1'); // Code
|
||||
conteudoTexto = conteudoTexto.replace(/\[(.*?)\]\(.*?\)/g, '$1'); // Links
|
||||
|
||||
// Dividir em linhas e adicionar ao PDF
|
||||
const linhas = doc.splitTextToSize(conteudoTexto, 170);
|
||||
for (const linha of linhas) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
|
||||
// Processar seções
|
||||
const secoes = conteudoTexto.split(/\n##\s+/);
|
||||
|
||||
for (let j = 0; j < secoes.length; j++) {
|
||||
let secao = secoes[j];
|
||||
if (j === 0) {
|
||||
// Primeira seção (antes do primeiro ##)
|
||||
secao = secao.replace(/^#\s+.*?\n\n/, ''); // Remove título principal
|
||||
}
|
||||
doc.text(linha, margin, yPos);
|
||||
yPos += 6;
|
||||
|
||||
// Detectar título da seção
|
||||
const tituloMatch = secao.match(/^(.+?)\n/);
|
||||
if (tituloMatch && j > 0) {
|
||||
// Adicionar título da seção
|
||||
if (yPos > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(tituloMatch[1], margin, yPos);
|
||||
yPos += 8;
|
||||
secao = secao.substring(tituloMatch[0].length);
|
||||
}
|
||||
|
||||
// Processar conteúdo da seção
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
// Remover markdown básico
|
||||
secao = secao.replace(/\*\*(.*?)\*\*/g, '$1'); // Bold
|
||||
secao = secao.replace(/\*(.*?)\*/g, '$1'); // Italic
|
||||
secao = secao.replace(/`(.*?)`/g, '$1'); // Code inline
|
||||
secao = secao.replace(/\[(.*?)\]\(.*?\)/g, '$1'); // Links
|
||||
|
||||
// Processar listas
|
||||
const linhas = secao.split('\n');
|
||||
for (const linha of linhas) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
|
||||
if (linha.trim().startsWith('-') || linha.trim().startsWith('*')) {
|
||||
// Item de lista
|
||||
const texto = linha.replace(/^[\s\-*]+/, '• ');
|
||||
const linhasTexto = doc.splitTextToSize(texto, pageWidth - 2 * margin - 10);
|
||||
doc.text(linhasTexto[0], margin + 5, yPos);
|
||||
yPos += 5;
|
||||
for (let k = 1; k < linhasTexto.length; k++) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.text(linhasTexto[k], margin + 10, yPos);
|
||||
yPos += 5;
|
||||
}
|
||||
} else if (linha.trim().startsWith('###')) {
|
||||
// Subtítulo
|
||||
const subtitulo = linha.replace(/^###\s+/, '');
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(subtitulo, margin + 5, yPos);
|
||||
yPos += 7;
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
} else if (linha.trim()) {
|
||||
// Texto normal
|
||||
const linhasTexto = doc.splitTextToSize(linha.trim(), pageWidth - 2 * margin);
|
||||
for (const linhaTexto of linhasTexto) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.text(linhaTexto, margin, yPos);
|
||||
yPos += 5;
|
||||
}
|
||||
} else {
|
||||
// Linha vazia
|
||||
yPos += 3;
|
||||
}
|
||||
}
|
||||
|
||||
yPos += 5;
|
||||
}
|
||||
|
||||
yPos += 10;
|
||||
}
|
||||
|
||||
// Footer em todas as páginas
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFillColor(240, 240, 240);
|
||||
doc.rect(0, pageHeight - 15, pageWidth, 15, 'F');
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.setTextColor(...secondaryColor);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
pageWidth / 2,
|
||||
pageHeight - 7,
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import {
|
||||
FileText,
|
||||
Search,
|
||||
Filter,
|
||||
Settings,
|
||||
Download,
|
||||
BookOpen,
|
||||
Tag,
|
||||
Folder,
|
||||
ChevronRight
|
||||
} from 'lucide-svelte';
|
||||
import { Filter, Settings, Download, BookOpen } from 'lucide-svelte';
|
||||
import DocumentacaoCard from '$lib/components/documentacao/DocumentacaoCard.svelte';
|
||||
import DocumentacaoSearch from '$lib/components/documentacao/DocumentacaoSearch.svelte';
|
||||
import DocumentacaoSidebar from '$lib/components/documentacao/DocumentacaoSidebar.svelte';
|
||||
import PdfGenerator from '$lib/components/documentacao/PdfGenerator.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
import DocumentacaoModal from '$lib/components/documentacao/DocumentacaoModal.svelte';
|
||||
|
||||
// Estados
|
||||
let busca = $state('');
|
||||
@@ -33,41 +20,49 @@
|
||||
let tagsSelecionadas = $state<string[]>([]);
|
||||
let mostrarFiltros = $state(false);
|
||||
let mostrarPdfGenerator = $state(false);
|
||||
let documentoModalId = $state<Id<'documentacao'> | null>(null);
|
||||
let documentosSelecionados = $state<Id<'documentacao'>[]>([]);
|
||||
|
||||
// Queries
|
||||
const documentosQuery = useQuery(api.documentacao.listarDocumentos, {
|
||||
// Queries - usar funções para garantir reatividade
|
||||
const documentosQuery = useQuery(api.documentacao.listarDocumentos, () => ({
|
||||
categoriaId: categoriaSelecionada || undefined,
|
||||
tipo: tipoSelecionado || undefined,
|
||||
tags: tagsSelecionadas.length > 0 ? tagsSelecionadas : undefined,
|
||||
busca: busca.trim() || undefined,
|
||||
ativo: true,
|
||||
limite: 50
|
||||
});
|
||||
}));
|
||||
|
||||
const categoriasQuery = useQuery(api.documentacao.listarCategorias, {
|
||||
const categoriasQuery = useQuery(api.documentacao.listarCategorias, () => ({
|
||||
ativo: true
|
||||
});
|
||||
}));
|
||||
|
||||
const tagsQuery = useQuery(api.documentacao.listarTags, {
|
||||
const tagsQuery = useQuery(api.documentacao.listarTags, () => ({
|
||||
ativo: true,
|
||||
limite: 50
|
||||
});
|
||||
}));
|
||||
|
||||
// Dados derivados
|
||||
const documentos = $derived.by(() => {
|
||||
if (!documentosQuery) return [];
|
||||
return documentosQuery.documentos || [];
|
||||
if (!documentosQuery || !documentosQuery.data) return [];
|
||||
const data = documentosQuery.data;
|
||||
return Array.isArray(data.documentos) ? data.documentos : [];
|
||||
});
|
||||
|
||||
const totalDocumentos = $derived.by(() => {
|
||||
if (!documentosQuery || !documentosQuery.data) return 0;
|
||||
const data = documentosQuery.data;
|
||||
return typeof data.total === 'number' ? data.total : 0;
|
||||
});
|
||||
|
||||
const categorias = $derived.by(() => {
|
||||
if (!categoriasQuery) return [];
|
||||
return categoriasQuery || [];
|
||||
if (!categoriasQuery || !categoriasQuery.data) return [];
|
||||
return Array.isArray(categoriasQuery.data) ? categoriasQuery.data : [];
|
||||
});
|
||||
|
||||
const tags = $derived.by(() => {
|
||||
if (!tagsQuery) return [];
|
||||
return tagsQuery || [];
|
||||
if (!tagsQuery || !tagsQuery.data) return [];
|
||||
return Array.isArray(tagsQuery.data) ? tagsQuery.data : [];
|
||||
});
|
||||
|
||||
// Funções
|
||||
@@ -93,6 +88,14 @@
|
||||
}
|
||||
mostrarPdfGenerator = true;
|
||||
}
|
||||
|
||||
function abrirModal(documentoId: Id<'documentacao'>) {
|
||||
documentoModalId = documentoId;
|
||||
}
|
||||
|
||||
function fecharModal() {
|
||||
documentoModalId = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={['ti_master', 'ti_usuario', 'admin']} maxLevel={1}>
|
||||
@@ -102,7 +105,9 @@
|
||||
class="border-primary/25 from-primary/10 via-base-100 to-secondary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-8 shadow-2xl"
|
||||
>
|
||||
<div class="bg-primary/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="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">
|
||||
<span
|
||||
@@ -119,10 +124,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href={resolve('/ti/documentacao/configuracao')}
|
||||
class="btn btn-primary gap-2"
|
||||
>
|
||||
<a href={resolve('/ti/documentacao/configuracao')} class="btn btn-primary gap-2">
|
||||
<Settings class="h-5 w-5" />
|
||||
Configuração
|
||||
</a>
|
||||
@@ -137,7 +139,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Busca e Filtros -->
|
||||
<section class="bg-base-100 rounded-2xl border border-base-300 p-6 shadow-lg">
|
||||
<section class="bg-base-100 border-base-300 rounded-2xl border p-6 shadow-lg">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<DocumentacaoSearch bind:busca bind:mostrarFiltros />
|
||||
|
||||
@@ -150,9 +152,7 @@
|
||||
Filtros
|
||||
</button>
|
||||
{#if busca || categoriaSelecionada || tipoSelecionado || tagsSelecionadas.length > 0}
|
||||
<button class="btn btn-ghost btn-sm gap-2" onclick={limparFiltros}>
|
||||
Limpar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm gap-2" onclick={limparFiltros}> Limpar </button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,7 +178,7 @@
|
||||
Documentos
|
||||
{#if documentosQuery}
|
||||
<span class="text-base-content/50 text-lg font-normal">
|
||||
({documentosQuery.total})
|
||||
({totalDocumentos})
|
||||
</span>
|
||||
{/if}
|
||||
</h2>
|
||||
@@ -189,7 +189,7 @@
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if documentos.length === 0}
|
||||
<div class="bg-base-200 rounded-2xl border border-base-300 p-12 text-center">
|
||||
<div class="bg-base-200 border-base-300 rounded-2xl border p-12 text-center">
|
||||
<BookOpen class="text-base-content/30 mx-auto mb-4 h-16 w-16" />
|
||||
<h3 class="text-base-content mb-2 text-xl font-semibold">Nenhum documento encontrado</h3>
|
||||
<p class="text-base-content/70">
|
||||
@@ -208,6 +208,7 @@
|
||||
{documento}
|
||||
selecionado={documentosSelecionados.includes(documento._id)}
|
||||
onToggleSelecao={() => toggleSelecaoDocumento(documento._id)}
|
||||
onVisualizar={() => abrirModal(documento._id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -225,5 +226,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
<!-- Modal de Visualização -->
|
||||
<DocumentacaoModal documentoId={documentoModalId} onClose={fecharModal} />
|
||||
</ProtectedRoute>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { marked } from 'marked';
|
||||
import { ArrowLeft, Download, Calendar, User, Tag, FileText } from 'lucide-svelte';
|
||||
import { ArrowLeft, Download, Calendar, User, Tag, FileText, BookOpen } from 'lucide-svelte';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
const documentoId = $derived(page.params.id);
|
||||
const documentoQuery = useQuery(
|
||||
@@ -18,58 +19,196 @@
|
||||
|
||||
let gerandoPdf = $state(false);
|
||||
|
||||
// Gerar índice a partir do conteúdo Markdown
|
||||
const indice = $derived(() => {
|
||||
if (!documentoQuery?.conteudo) return [];
|
||||
|
||||
const linhas = documentoQuery.conteudo.split('\n');
|
||||
const indices: Array<{ nivel: number; titulo: string; id: string }> = [];
|
||||
|
||||
linhas.forEach((linha) => {
|
||||
const match = linha.match(/^(#{1,3})\s+(.+)$/);
|
||||
if (match) {
|
||||
const nivel = match[1].length;
|
||||
const titulo = match[2].trim();
|
||||
const id = titulo
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
indices.push({ nivel, titulo, id });
|
||||
}
|
||||
});
|
||||
|
||||
return indices;
|
||||
});
|
||||
|
||||
function scrollParaSecao(id: string) {
|
||||
const elemento = document.getElementById(id);
|
||||
if (elemento) {
|
||||
elemento.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Processar HTML para adicionar IDs aos títulos
|
||||
const conteudoHtml = $derived(() => {
|
||||
if (!documentoQuery?.conteudo) return '';
|
||||
|
||||
let html = marked.parse(documentoQuery.conteudo);
|
||||
|
||||
// Adicionar IDs aos títulos
|
||||
html = html.replace(/<h([1-3])>(.+?)<\/h[1-3]>/g, (match, nivel, titulo) => {
|
||||
const id = titulo
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
return `<h${nivel} id="${id}">${titulo}</h${nivel}>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
});
|
||||
|
||||
async function gerarPdfIndividual() {
|
||||
if (!documentoQuery) return;
|
||||
|
||||
try {
|
||||
gerandoPdf = true;
|
||||
const doc = new jsPDF();
|
||||
let yPos = 20;
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 20;
|
||||
const primaryColor = [102, 126, 234];
|
||||
const secondaryColor = [128, 128, 128];
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(102, 126, 234);
|
||||
doc.text(documentoQuery.titulo, margin, yPos, { maxWidth: 170 });
|
||||
yPos += 15;
|
||||
// Cabeçalho
|
||||
doc.setFillColor(...primaryColor);
|
||||
doc.rect(0, 0, pageWidth, 40, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(documentoQuery.titulo, pageWidth / 2, 25, { align: 'center', maxWidth: pageWidth - 2 * margin });
|
||||
|
||||
// Metadados
|
||||
let yPos = 50;
|
||||
|
||||
// Metadados em tabela
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
body: [
|
||||
['Tipo', documentoQuery.tipo],
|
||||
['Versão', documentoQuery.versao],
|
||||
['Arquivo Origem', documentoQuery.arquivoOrigem || 'N/A']
|
||||
],
|
||||
theme: 'plain',
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 4
|
||||
},
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 50, fillColor: [240, 240, 240] },
|
||||
1: { cellWidth: 'auto' }
|
||||
}
|
||||
});
|
||||
|
||||
yPos = (doc as any).lastAutoTable.finalY + 10;
|
||||
|
||||
// Conteúdo processado
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.text(`Tipo: ${documentoQuery.tipo} | Versão: ${documentoQuery.versao}`, margin, yPos);
|
||||
yPos += 10;
|
||||
|
||||
// Conteúdo
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
let conteudoTexto = documentoQuery.conteudo;
|
||||
conteudoTexto = conteudoTexto.replace(/#{1,6}\s+/g, '');
|
||||
conteudoTexto = conteudoTexto.replace(/\*\*(.*?)\*\*/g, '$1');
|
||||
conteudoTexto = conteudoTexto.replace(/\*(.*?)\*/g, '$1');
|
||||
conteudoTexto = conteudoTexto.replace(/`(.*?)`/g, '$1');
|
||||
conteudoTexto = conteudoTexto.replace(/\[(.*?)\]\(.*?\)/g, '$1');
|
||||
const secoes = conteudoTexto.split(/\n##\s+/);
|
||||
|
||||
const linhas = doc.splitTextToSize(conteudoTexto, 170);
|
||||
for (const linha of linhas) {
|
||||
if (yPos > doc.internal.pageSize.getHeight() - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
for (let j = 0; j < secoes.length; j++) {
|
||||
let secao = secoes[j];
|
||||
if (j === 0) {
|
||||
secao = secao.replace(/^#\s+.*?\n\n/, '');
|
||||
}
|
||||
doc.text(linha, margin, yPos);
|
||||
yPos += 6;
|
||||
|
||||
const tituloMatch = secao.match(/^(.+?)\n/);
|
||||
if (tituloMatch && j > 0) {
|
||||
if (yPos > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(tituloMatch[1], margin, yPos);
|
||||
yPos += 8;
|
||||
secao = secao.substring(tituloMatch[0].length);
|
||||
}
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
secao = secao.replace(/\*\*(.*?)\*\*/g, '$1');
|
||||
secao = secao.replace(/\*(.*?)\*/g, '$1');
|
||||
secao = secao.replace(/`(.*?)`/g, '$1');
|
||||
secao = secao.replace(/\[(.*?)\]\(.*?\)/g, '$1');
|
||||
|
||||
const linhas = secao.split('\n');
|
||||
for (const linha of linhas) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
|
||||
if (linha.trim().startsWith('-') || linha.trim().startsWith('*')) {
|
||||
const texto = linha.replace(/^[\s\-*]+/, '• ');
|
||||
const linhasTexto = doc.splitTextToSize(texto, pageWidth - 2 * margin - 10);
|
||||
doc.text(linhasTexto[0], margin + 5, yPos);
|
||||
yPos += 5;
|
||||
for (let k = 1; k < linhasTexto.length; k++) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.text(linhasTexto[k], margin + 10, yPos);
|
||||
yPos += 5;
|
||||
}
|
||||
} else if (linha.trim().startsWith('###')) {
|
||||
const subtitulo = linha.replace(/^###\s+/, '');
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(subtitulo, margin + 5, yPos);
|
||||
yPos += 7;
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
} else if (linha.trim()) {
|
||||
const linhasTexto = doc.splitTextToSize(linha.trim(), pageWidth - 2 * margin);
|
||||
for (const linhaTexto of linhasTexto) {
|
||||
if (yPos > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
yPos = margin;
|
||||
}
|
||||
doc.text(linhaTexto, margin, yPos);
|
||||
yPos += 5;
|
||||
}
|
||||
} else {
|
||||
yPos += 3;
|
||||
}
|
||||
}
|
||||
|
||||
yPos += 5;
|
||||
}
|
||||
|
||||
// Footer
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFillColor(240, 240, 240);
|
||||
doc.rect(0, pageHeight - 15, pageWidth, 15, 'F');
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.setTextColor(...secondaryColor);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
pageWidth / 2,
|
||||
pageHeight - 7,
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
@@ -112,80 +251,159 @@
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Documento -->
|
||||
<article class="bg-base-100 rounded-2xl border border-base-300 p-8 shadow-lg">
|
||||
<!-- Título -->
|
||||
<header class="mb-6">
|
||||
<h1 class="text-base-content mb-4 text-4xl font-bold">{documentoQuery.titulo}</h1>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-base-content/70">
|
||||
<div class="flex items-center gap-2">
|
||||
<FileText class="h-4 w-4" />
|
||||
<span class="badge badge-primary">{documentoQuery.tipo}</span>
|
||||
<div class="flex gap-6">
|
||||
<!-- Índice lateral -->
|
||||
{#if indice().length > 0}
|
||||
<aside class="hidden w-64 shrink-0 lg:block">
|
||||
<div class="bg-base-100 border-base-300 sticky top-8 rounded-2xl border p-6 shadow-lg">
|
||||
<h3 class="text-base-content mb-4 text-sm font-semibold uppercase">Índice</h3>
|
||||
<nav class="space-y-1">
|
||||
{#each indice() as item}
|
||||
<button
|
||||
class="text-base-content/70 hover:text-primary block w-full text-left text-sm transition-colors {item.nivel === 1
|
||||
? 'font-semibold'
|
||||
: item.nivel === 2
|
||||
? 'ml-4 font-medium'
|
||||
: 'ml-8'}"
|
||||
onclick={() => scrollParaSecao(item.id)}
|
||||
>
|
||||
{item.titulo}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="h-4 w-4" />
|
||||
<span>
|
||||
{format(new Date(documentoQuery.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{#if documentoQuery.criadoPorUsuario}
|
||||
<div class="flex items-center gap-2">
|
||||
<User class="h-4 w-4" />
|
||||
<span>{documentoQuery.criadoPorUsuario.nome}</span>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- Conteúdo principal -->
|
||||
<article class="bg-base-100 flex-1 rounded-2xl border border-base-300 shadow-lg">
|
||||
<!-- Cabeçalho com gradiente -->
|
||||
<header class="bg-gradient-to-r from-primary to-primary-focus rounded-t-2xl p-8 text-white">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<BookOpen class="h-6 w-6" />
|
||||
<span class="badge badge-secondary badge-lg">{documentoQuery.tipo}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<h1 class="mb-4 text-4xl font-bold">{documentoQuery.titulo}</h1>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-white/90">
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="h-4 w-4" />
|
||||
<span>
|
||||
Atualizado em {documentoQuery.atualizadoEm && !isNaN(new Date(documentoQuery.atualizadoEm).getTime())
|
||||
? format(new Date(documentoQuery.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})
|
||||
: 'Data não disponível'}
|
||||
</span>
|
||||
</div>
|
||||
{#if documentoQuery.criadoPorUsuario}
|
||||
<div class="flex items-center gap-2">
|
||||
<User class="h-4 w-4" />
|
||||
<span>Por {documentoQuery.criadoPorUsuario.nome}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<FileText class="h-4 w-4" />
|
||||
<span>Versão {documentoQuery.versao}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if documentoQuery.tags && documentoQuery.tags.length > 0}
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2">
|
||||
<Tag class="text-base-content/50 h-5 w-5" />
|
||||
{#each documentoQuery.tags as tag}
|
||||
<span class="badge badge-outline">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Conteúdo principal -->
|
||||
<div class="p-8">
|
||||
<!-- Tags -->
|
||||
{#if documentoQuery.tags && documentoQuery.tags.length > 0}
|
||||
<div class="mb-6 flex flex-wrap items-center gap-2">
|
||||
<Tag class="text-base-content/50 h-5 w-5" />
|
||||
{#each documentoQuery.tags as tag}
|
||||
<span class="badge badge-outline badge-sm">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Conteúdo -->
|
||||
<div class="prose prose-slate max-w-none">
|
||||
{@html marked.parse(documentoQuery.conteudo)}
|
||||
<!-- Conteúdo Markdown -->
|
||||
<div class="prose prose-slate prose-lg max-w-none">
|
||||
{@html conteudoHtml()}
|
||||
</div>
|
||||
|
||||
<!-- Informações adicionais -->
|
||||
{#if documentoQuery.arquivoOrigem}
|
||||
<div class="bg-base-200 mt-8 rounded-lg p-6">
|
||||
<h3 class="text-base-content mb-4 flex items-center gap-2 text-lg font-semibold">
|
||||
<FileText class="h-5 w-5" />
|
||||
Informações do Arquivo
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Arquivo Origem:</span>
|
||||
<span class="text-base-content ml-2 font-mono text-sm">{documentoQuery.arquivoOrigem}</span>
|
||||
</div>
|
||||
{#if documentoQuery.funcaoOrigem}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Função Origem:</span>
|
||||
<span class="text-base-content ml-2 font-mono text-sm">{documentoQuery.funcaoOrigem}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Hash:</span>
|
||||
<span class="text-base-content ml-2 font-mono text-xs">{documentoQuery.hash}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Metadados técnicos -->
|
||||
{#if documentoQuery.metadados}
|
||||
<div class="bg-base-200 mt-6 rounded-lg p-6">
|
||||
<h3 class="text-base-content mb-4 text-lg font-semibold">Detalhes Técnicos</h3>
|
||||
<div class="space-y-4">
|
||||
{#if documentoQuery.metadados.parametros && documentoQuery.metadados.parametros.length > 0}
|
||||
<div>
|
||||
<h4 class="text-base-content mb-3 font-medium">Parâmetros de Entrada:</h4>
|
||||
<div class="bg-base-100 rounded-lg p-4">
|
||||
<ul class="space-y-2">
|
||||
{#each documentoQuery.metadados.parametros as param}
|
||||
<li class="text-base-content/80 flex items-start gap-2">
|
||||
<span class="text-primary mt-1">•</span>
|
||||
<span class="font-mono text-sm">{param}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if documentoQuery.metadados.retorno}
|
||||
<div>
|
||||
<h4 class="text-base-content mb-2 font-medium">Valor de Retorno:</h4>
|
||||
<div class="bg-base-100 rounded-lg p-4">
|
||||
<p class="text-base-content/80 font-mono text-sm">{documentoQuery.metadados.retorno}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if documentoQuery.metadados.dependencias && documentoQuery.metadados.dependencias.length > 0}
|
||||
<div>
|
||||
<h4 class="text-base-content mb-2 font-medium">Dependências:</h4>
|
||||
<div class="bg-base-100 rounded-lg p-4">
|
||||
<ul class="space-y-2">
|
||||
{#each documentoQuery.metadados.dependencias as dep}
|
||||
<li class="text-base-content/80 flex items-start gap-2">
|
||||
<span class="text-primary mt-1">•</span>
|
||||
<span>{dep}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Metadados -->
|
||||
{#if documentoQuery.metadados}
|
||||
<div class="bg-base-200 mt-8 rounded-lg p-6">
|
||||
<h3 class="text-base-content mb-4 text-lg font-semibold">Informações Técnicas</h3>
|
||||
{#if documentoQuery.metadados.parametros}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-base-content mb-2 font-medium">Parâmetros:</h4>
|
||||
<ul class="list-inside list-disc">
|
||||
{#each documentoQuery.metadados.parametros as param}
|
||||
<li class="text-base-content/70">{param}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if documentoQuery.metadados.retorno}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-base-content mb-2 font-medium">Retorno:</h4>
|
||||
<p class="text-base-content/70">{documentoQuery.metadados.retorno}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if documentoQuery.metadados.dependencias}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-base-content mb-2 font-medium">Dependências:</h4>
|
||||
<ul class="list-inside list-disc">
|
||||
{#each documentoQuery.metadados.dependencias as dep}
|
||||
<li class="text-base-content/70">{dep}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
@@ -193,25 +411,121 @@
|
||||
<style>
|
||||
:global(.prose) {
|
||||
color: hsl(var(--bc));
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:global(.prose h1) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
border-bottom: 2px solid hsl(var(--b3));
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
:global(.prose h2) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 2em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
:global(.prose h1),
|
||||
:global(.prose h2),
|
||||
:global(.prose h3) {
|
||||
color: hsl(var(--p));
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
:global(.prose h4) {
|
||||
color: hsl(var(--bc));
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
:global(.prose p) {
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:global(.prose ul),
|
||||
:global(.prose ol) {
|
||||
margin: 1em 0;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
:global(.prose li) {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:global(.prose code) {
|
||||
background-color: hsl(var(--b2));
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9em;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: hsl(var(--p));
|
||||
}
|
||||
|
||||
:global(.prose pre) {
|
||||
background-color: hsl(var(--b2));
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid hsl(var(--p));
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
:global(.prose pre code) {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: hsl(var(--bc));
|
||||
}
|
||||
|
||||
:global(.prose strong) {
|
||||
color: hsl(var(--bc));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.prose a) {
|
||||
color: hsl(var(--p));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
:global(.prose a:hover) {
|
||||
color: hsl(var(--pf));
|
||||
}
|
||||
|
||||
:global(.prose blockquote) {
|
||||
border-left: 4px solid hsl(var(--p));
|
||||
padding-left: 1.5em;
|
||||
margin: 1.5em 0;
|
||||
color: hsl(var(--bc) / 0.8);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.prose table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
:global(.prose th),
|
||||
:global(.prose td) {
|
||||
border: 1px solid hsl(var(--b3));
|
||||
padding: 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:global(.prose th) {
|
||||
background-color: hsl(var(--b2));
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
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 ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
@@ -13,7 +14,9 @@
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2
|
||||
Loader2,
|
||||
X,
|
||||
CheckCircle
|
||||
} from 'lucide-svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
@@ -22,17 +25,17 @@
|
||||
type DiaSemana = 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado';
|
||||
|
||||
type ConfigVarredura = {
|
||||
_id: Id<'documentacaoConfig'>;
|
||||
_id?: Id<'documentacaoConfig'>;
|
||||
ativo: boolean;
|
||||
diasSemana: DiaSemana[];
|
||||
horario: string;
|
||||
fusoHorario?: string;
|
||||
ultimaExecucao?: number;
|
||||
proximaExecucao?: number;
|
||||
configuradoPor: Id<'usuarios'>;
|
||||
configuradoEm: number;
|
||||
configuradoPor?: Id<'usuarios'>;
|
||||
configuradoEm?: number;
|
||||
atualizadoPor?: Id<'usuarios'>;
|
||||
atualizadoEm: number;
|
||||
atualizadoEm?: number;
|
||||
};
|
||||
|
||||
type Varredura = {
|
||||
@@ -53,12 +56,29 @@
|
||||
nome: string;
|
||||
email: string;
|
||||
} | null;
|
||||
arquivosModificados?: Array<{
|
||||
arquivo: string;
|
||||
versao: string;
|
||||
funcoes: string[];
|
||||
}>;
|
||||
totalArquivosModificados?: number;
|
||||
};
|
||||
|
||||
// Estados
|
||||
let config = $state<ConfigVarredura | null>(null);
|
||||
// Estados - inicializar com valores padrão
|
||||
let config = $state<ConfigVarredura>({
|
||||
ativo: false,
|
||||
diasSemana: [],
|
||||
horario: '08:00',
|
||||
fusoHorario: 'America/Recife'
|
||||
});
|
||||
let executandoVarredura = $state(false);
|
||||
let historicoVarreduras = $state<Varredura[]>([]);
|
||||
|
||||
// Estados para modais
|
||||
let showSuccessModal = $state(false);
|
||||
let showErrorModal = $state(false);
|
||||
let modalMessage = $state('');
|
||||
let modalTitle = $state('');
|
||||
|
||||
// Queries
|
||||
const configQuery = useQuery(api.documentacao.obterConfigVarredura, {});
|
||||
@@ -67,18 +87,33 @@
|
||||
$effect(() => {
|
||||
if (configQuery) {
|
||||
config = {
|
||||
_id: configQuery._id,
|
||||
ativo: configQuery.ativo ?? false,
|
||||
diasSemana: configQuery.diasSemana ?? [],
|
||||
horario: configQuery.horario ?? '08:00',
|
||||
fusoHorario: configQuery.fusoHorario ?? 'America/Recife'
|
||||
fusoHorario: configQuery.fusoHorario ?? 'America/Recife',
|
||||
ultimaExecucao: configQuery.ultimaExecucao,
|
||||
proximaExecucao: configQuery.proximaExecucao,
|
||||
configuradoPor: configQuery.configuradoPor,
|
||||
configuradoEm: configQuery.configuradoEm,
|
||||
atualizadoPor: configQuery.atualizadoPor,
|
||||
atualizadoEm: configQuery.atualizadoEm
|
||||
};
|
||||
} else if (configQuery === null) {
|
||||
// Se não há configuração, criar uma padrão
|
||||
if (!config || !config._id) {
|
||||
config = {
|
||||
ativo: false,
|
||||
diasSemana: [],
|
||||
horario: '08:00',
|
||||
fusoHorario: 'America/Recife'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Funções
|
||||
async function salvarConfig() {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
await client.mutation(api.documentacao.salvarConfigVarredura, {
|
||||
ativo: config.ativo,
|
||||
@@ -86,10 +121,14 @@
|
||||
horario: config.horario,
|
||||
fusoHorario: config.fusoHorario
|
||||
});
|
||||
alert('Configuração salva com sucesso!');
|
||||
modalTitle = 'Sucesso';
|
||||
modalMessage = 'Configuração salva com sucesso!';
|
||||
showSuccessModal = true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configuração:', error);
|
||||
alert('Erro ao salvar configuração');
|
||||
modalTitle = 'Erro';
|
||||
modalMessage = 'Erro ao salvar configuração. Tente novamente.';
|
||||
showErrorModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,32 +136,57 @@
|
||||
try {
|
||||
executandoVarredura = true;
|
||||
await client.mutation(api.documentacaoVarredura.executarVarreduraManual, {});
|
||||
alert('Varredura iniciada! Você será notificado quando concluir.');
|
||||
// Recarregar histórico após alguns segundos
|
||||
setTimeout(() => {
|
||||
carregarHistorico();
|
||||
}, 3000);
|
||||
modalTitle = 'Varredura Iniciada';
|
||||
modalMessage = 'Varredura iniciada! Você será notificado quando concluir.';
|
||||
showSuccessModal = true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao executar varredura:', error);
|
||||
alert('Erro ao executar varredura');
|
||||
modalTitle = 'Erro';
|
||||
modalMessage = 'Erro ao executar varredura. Tente novamente.';
|
||||
showErrorModal = true;
|
||||
} finally {
|
||||
executandoVarredura = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function carregarHistorico() {
|
||||
try {
|
||||
const historico = await client.mutation(api.documentacaoVarredura.obterHistoricoVarreduras, {
|
||||
limite: 20
|
||||
});
|
||||
historicoVarreduras = historico || [];
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar histórico:', error);
|
||||
}
|
||||
|
||||
function fecharModal() {
|
||||
showSuccessModal = false;
|
||||
showErrorModal = false;
|
||||
modalMessage = '';
|
||||
modalTitle = '';
|
||||
}
|
||||
|
||||
const historicoQuery = useQuery(api.documentacaoVarredura.obterHistoricoVarreduras, () => ({
|
||||
limite: 20
|
||||
}));
|
||||
|
||||
// Extrair dados do histórico
|
||||
$effect(() => {
|
||||
carregarHistorico();
|
||||
if (historicoQuery === undefined) {
|
||||
historicoVarreduras = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (historicoQuery === null) {
|
||||
historicoVarreduras = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Se é diretamente um array, usar ele (caso mais comum)
|
||||
if (Array.isArray(historicoQuery)) {
|
||||
historicoVarreduras = historicoQuery;
|
||||
return;
|
||||
}
|
||||
|
||||
// Se tem propriedade data, usar os dados
|
||||
if (typeof historicoQuery === 'object' && historicoQuery !== null && 'data' in historicoQuery) {
|
||||
const data = (historicoQuery as any).data;
|
||||
historicoVarreduras = Array.isArray(data) ? data : [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Caso padrão
|
||||
historicoVarreduras = [];
|
||||
});
|
||||
|
||||
const diasSemana = [
|
||||
@@ -136,7 +200,6 @@
|
||||
];
|
||||
|
||||
function toggleDiaSemana(dia: DiaSemana) {
|
||||
if (!config) return;
|
||||
if (config.diasSemana.includes(dia)) {
|
||||
config.diasSemana = config.diasSemana.filter((d) => d !== dia);
|
||||
} else {
|
||||
@@ -186,10 +249,6 @@
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if !config}
|
||||
<div class="bg-base-200 rounded-2xl border border-base-300 p-12 text-center">
|
||||
<p class="text-base-content/70">Carregando configuração...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Configuração -->
|
||||
<section class="bg-base-100 rounded-2xl border border-base-300 p-6 shadow-lg">
|
||||
@@ -199,6 +258,51 @@
|
||||
</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Informações da Configuração Atual -->
|
||||
{#if config._id}
|
||||
<div class="bg-base-200 rounded-lg p-4">
|
||||
<h3 class="text-base-content mb-3 text-lg font-semibold">Configuração Atual</h3>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-2">
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Status:</span>
|
||||
<span class="text-base-content ml-2">
|
||||
{config.ativo ? 'Ativa' : 'Inativa'}
|
||||
</span>
|
||||
</div>
|
||||
{#if config.ultimaExecucao && !isNaN(new Date(config.ultimaExecucao).getTime())}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Última execução:</span>
|
||||
<span class="text-base-content ml-2">
|
||||
{format(new Date(config.ultimaExecucao), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if config.proximaExecucao && !isNaN(new Date(config.proximaExecucao).getTime())}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Próxima execução:</span>
|
||||
<span class="text-base-content ml-2">
|
||||
{format(new Date(config.proximaExecucao), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if config.atualizadoEm && !isNaN(new Date(config.atualizadoEm).getTime())}
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Atualizado em:</span>
|
||||
<span class="text-base-content ml-2">
|
||||
{format(new Date(config.atualizadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Ativar/Desativar -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
@@ -223,7 +327,7 @@
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={config.diasSemana.includes(dia.value)}
|
||||
onchange={() => toggleDiaSemana(dia.value)}
|
||||
onclick={() => toggleDiaSemana(dia.value)}
|
||||
/>
|
||||
<span>{dia.label}</span>
|
||||
</label>
|
||||
@@ -272,57 +376,90 @@
|
||||
Histórico de Varreduras
|
||||
</h2>
|
||||
|
||||
{#if historicoVarreduras.length === 0}
|
||||
{#if historicoQuery === undefined}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if historicoVarreduras.length === 0}
|
||||
<div class="bg-base-200 rounded-lg p-8 text-center">
|
||||
<p class="text-base-content/70">Nenhuma varredura executada ainda</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tipo</th>
|
||||
<th>Status</th>
|
||||
<th>Documentos</th>
|
||||
<th>Executado por</th>
|
||||
<th>Iniciado em</th>
|
||||
<th>Duração</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each historicoVarreduras as varredura}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge badge-outline">
|
||||
{varredura.tipo === 'automatica' ? 'Automática' : 'Manual'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {statusColors[varredura.status] || 'badge-ghost'}">
|
||||
{statusLabels[varredura.status] || varredura.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
Novos: {varredura.documentosNovos} | Atualizados:{' '}
|
||||
{varredura.documentosAtualizados}
|
||||
</td>
|
||||
<td>
|
||||
<div class="space-y-4">
|
||||
{#each historicoVarreduras as varredura}
|
||||
<div class="bg-base-200 rounded-lg border border-base-300 p-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge badge-outline">
|
||||
{varredura.tipo === 'automatica' ? 'Automática' : 'Manual'}
|
||||
</span>
|
||||
<span class="badge {statusColors[varredura.status] || 'badge-ghost'}">
|
||||
{statusLabels[varredura.status] || varredura.status}
|
||||
</span>
|
||||
<span class="text-base-content/70 text-sm">
|
||||
{varredura.iniciadoEm && !isNaN(new Date(varredura.iniciadoEm).getTime())
|
||||
? format(new Date(varredura.iniciadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})
|
||||
: 'Data não disponível'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-base-content/70 text-sm">
|
||||
{varredura.duracaoMs
|
||||
? `${(varredura.duracaoMs / 1000).toFixed(1)}s`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 grid grid-cols-2 gap-4 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Arquivos analisados:</span>
|
||||
<span class="text-base-content ml-2">{varredura.arquivosAnalisados}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Documentos novos:</span>
|
||||
<span class="text-base-content ml-2">{varredura.documentosNovos}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Documentos atualizados:</span>
|
||||
<span class="text-base-content ml-2">{varredura.documentosAtualizados}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/70 font-medium">Executado por:</span>
|
||||
<span class="text-base-content ml-2">
|
||||
{varredura.executadoPorUsuario?.nome || 'N/A'}
|
||||
</td>
|
||||
<td>
|
||||
{format(new Date(varredura.iniciadoEm), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{varredura.duracaoMs
|
||||
? `${(varredura.duracaoMs / 1000).toFixed(1)}s`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if varredura.totalArquivosModificados !== undefined && varredura.totalArquivosModificados > 0}
|
||||
<div class="border-base-300 mt-3 border-t pt-3">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="text-base-content font-semibold">
|
||||
Arquivos modificados: {varredura.totalArquivosModificados}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#each varredura.arquivosModificados || [] as arquivo}
|
||||
<div class="bg-base-100 rounded-lg p-3">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-base-content font-mono text-sm font-medium">
|
||||
{arquivo.arquivo}
|
||||
</span>
|
||||
<span class="badge badge-primary badge-sm">v{arquivo.versao}</span>
|
||||
</div>
|
||||
{#if arquivo.funcoes.length > 0}
|
||||
<div class="text-base-content/70 mt-1 text-xs">
|
||||
Funções: {arquivo.funcoes.join(', ')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -330,3 +467,55 @@
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
|
||||
<!-- Modal de Sucesso -->
|
||||
{#if showSuccessModal}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box from-base-100 to-base-200 bg-linear-to-br">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
onclick={fecharModal}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<CheckCircle class="text-success h-8 w-8" />
|
||||
<h3 class="text-primary text-2xl font-bold">{modalTitle}</h3>
|
||||
</div>
|
||||
<p class="text-base-content mb-6">{modalMessage}</p>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-primary" onclick={fecharModal}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModal}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Erro -->
|
||||
{#if showErrorModal}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box from-base-100 to-base-200 bg-linear-to-br">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
onclick={fecharModal}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<XCircle class="text-error h-8 w-8" />
|
||||
<h3 class="text-error text-2xl font-bold">{modalTitle}</h3>
|
||||
</div>
|
||||
<p class="text-base-content mb-6">{modalMessage}</p>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-error" onclick={fecharModal}>Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop" onclick={fecharModal}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user