ljoaquim 3 minggu lalu
induk
melakukan
8437af1ceb
2 mengubah file dengan 308 tambahan dan 0 penghapusan
  1. 1 0
      src/lib/layout/SideBar.svelte
  2. 307 0
      src/routes/documents/+page.svelte

+ 1 - 0
src/lib/layout/SideBar.svelte

@@ -28,6 +28,7 @@
     { id: 'wallet', href: '/wallet', label: 'wallet', icon: walletIcon },
     { id: 'trading', href: '/trading', label: 'Trading', icon: operationsIcon },
     { id: 'marketplace', href: '/marketplace', label: 'Marketplace', icon: marketplaceIcon },
+    { id: 'documents', href: '/documents', label: 'Documentos', icon: tokensIcon },
     { id: 'reports', href: '/reports', label: 'Relatórios', icon: reportsIcon },
     { id: 'users', href: '/users', label: 'Usuários', icon: usersIcon },
     { id: 'settings', href: '/settings', label: 'Configurações', icon: settingsIcon },

+ 307 - 0
src/routes/documents/+page.svelte

@@ -0,0 +1,307 @@
+<script>
+  import { onMount } from 'svelte';
+  import { get } from 'svelte/store';
+  import Header from '$lib/layout/Header.svelte';
+  import Tables from '$lib/components/Tables.svelte';
+  import { authToken } from '$lib/utils/stores';
+
+  const apiUrl = import.meta.env.VITE_API_URL;
+  const breadcrumb = [{ label: 'Início' }, { label: 'Documentos', active: true }];
+
+  let columns = [
+    { key: 'document_type', label: 'Tipo' },
+    { key: 'filename', label: 'Arquivo' }
+  ];
+
+  let data = [];
+  let loading = false;
+  let error = '';
+
+  let uploadVisible = false;
+  let uploadDocumentType = '';
+  let uploadFile = null;
+  let uploadLoading = false;
+  let uploadError = '';
+
+  onMount(() => {
+    void fetchDocuments();
+  });
+
+  function resolveToken() {
+    const storeToken = get(authToken);
+    if (storeToken) return storeToken;
+    if (typeof document === 'undefined') return null;
+    const m = document.cookie.match(/(?:^|; )auth_token=([^;]+)/);
+    return m ? decodeURIComponent(m[1]) : null;
+  }
+
+  function filenameFromPath(path) {
+    if (!path) return '-';
+    const parts = String(path).split('/');
+    return parts[parts.length - 1] || '-';
+  }
+
+  async function fetchDocuments() {
+    if (!apiUrl) return;
+    loading = true;
+    error = '';
+    try {
+      const token = resolveToken();
+      if (!token) throw new Error('Sessão expirada. Faça login novamente.');
+
+      const res = await fetch(`${apiUrl}/documents/list`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({})
+      });
+
+      const raw = await res.text();
+      const body = raw ? JSON.parse(raw) : null;
+      if (!res.ok || body?.status !== 'ok') {
+        throw new Error(body?.msg ?? body?.message ?? 'Falha ao carregar documentos.');
+      }
+
+      const docs = body?.data?.documents;
+      data = Array.isArray(docs)
+        ? docs.map((d) => ({
+            __raw: d,
+            document_type: d?.document_type ?? '-',
+            filename: filenameFromPath(d?.document_path)
+          }))
+        : [];
+    } catch (err) {
+      error = err?.message ?? 'Falha ao carregar documentos.';
+      data = [];
+    } finally {
+      loading = false;
+    }
+  }
+
+  function openUpload() {
+    uploadVisible = true;
+    uploadDocumentType = '';
+    uploadFile = null;
+    uploadLoading = false;
+    uploadError = '';
+  }
+
+  function closeUpload() {
+    if (uploadLoading) return;
+    uploadVisible = false;
+    uploadError = '';
+  }
+
+  async function submitUpload() {
+    if (!apiUrl) return;
+    uploadError = '';
+
+    const token = resolveToken();
+    if (!token) {
+      uploadError = 'Sessão expirada. Faça login novamente.';
+      return;
+    }
+
+    if (!uploadDocumentType.trim()) {
+      uploadError = 'Informe o tipo do documento.';
+      return;
+    }
+
+    if (!uploadFile) {
+      uploadError = 'Selecione um arquivo.';
+      return;
+    }
+
+    uploadLoading = true;
+    try {
+      const form = new FormData();
+      form.append('document_type', uploadDocumentType.trim());
+      form.append('file', uploadFile);
+
+      const res = await fetch(`${apiUrl}/documents/upload`, {
+        method: 'POST',
+        headers: {
+          Authorization: `Bearer ${token}`
+        },
+        body: form
+      });
+
+      const raw = await res.text();
+      let body = null;
+      if (raw) {
+        try {
+          body = JSON.parse(raw);
+        } catch (e) {
+          throw new Error('Resposta inválida do servidor ao enviar documento.');
+        }
+      }
+
+      if (!res.ok || body?.status !== 'ok') {
+        throw new Error(body?.msg ?? body?.message ?? 'Falha ao enviar documento.');
+      }
+
+      uploadVisible = false;
+      await fetchDocuments();
+    } catch (err) {
+      uploadError = err?.message ?? 'Falha ao enviar documento.';
+    } finally {
+      uploadLoading = false;
+    }
+  }
+
+  async function downloadDocument(row) {
+    if (!apiUrl) return;
+    error = '';
+    try {
+      const token = resolveToken();
+      if (!token) throw new Error('Sessão expirada. Faça login novamente.');
+
+      const documentType = row?.document_type;
+      if (!documentType || documentType === '-') {
+        throw new Error('Tipo de documento inválido.');
+      }
+
+      const res = await fetch(`${apiUrl}/documents/download`, {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+          Authorization: `Bearer ${token}`
+        },
+        body: JSON.stringify({ document_type: documentType })
+      });
+
+      if (!res.ok) {
+        const raw = await res.text();
+        let body = null;
+        if (raw) {
+          try {
+            body = JSON.parse(raw);
+          } catch {}
+        }
+        throw new Error(body?.msg ?? body?.message ?? `Falha ao baixar documento (HTTP ${res.status}).`);
+      }
+
+      const blob = await res.blob();
+      const cd = res.headers.get('content-disposition') || '';
+      const match = cd.match(/filename="?([^";]+)"?/i);
+      const suggested = match?.[1] || `${documentType}.pdf`;
+
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = suggested;
+      document.body.appendChild(a);
+      a.click();
+      a.remove();
+      URL.revokeObjectURL(url);
+    } catch (err) {
+      error = err?.message ?? 'Não foi possível baixar o documento.';
+    }
+  }
+
+  function handleRowClick(e) {
+    const row = e?.detail?.row;
+    if (!row) return;
+    void downloadDocument(row);
+  }
+</script>
+
+<div>
+  <Header title="Documentos" subtitle="Upload e gestão de documentos" breadcrumb={breadcrumb} />
+
+  <div class="p-4">
+    <div class="max-w-6xl mx-auto mt-4 space-y-4">
+      {#if error}
+        <div class="rounded border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">{error}</div>
+      {/if}
+
+      <Tables
+        title="Documentos"
+        {columns}
+        {data}
+        showEdit={false}
+        showDelete={false}
+        on:addTop={openUpload}
+        on:rowClick={handleRowClick}
+      />
+
+      {#if uploadVisible}
+        <div
+          class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"
+          role="button"
+          tabindex="0"
+          on:click={(e) => {
+            if (e.target === e.currentTarget) closeUpload();
+          }}
+          on:keydown={(e) => {
+            if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') closeUpload();
+          }}
+        >
+          <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg w-full max-w-lg p-6" role="dialog" aria-modal="true">
+            <div class="flex items-center justify-between mb-4">
+              <h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Enviar documento</h4>
+              <button
+                class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
+                on:click={closeUpload}
+                disabled={uploadLoading}
+              >
+                ✕
+              </button>
+            </div>
+
+            <div class="space-y-4">
+              <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tipo do documento</label>
+                <input
+                  class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+                  placeholder="Ex: contrato_social"
+                  bind:value={uploadDocumentType}
+                  disabled={uploadLoading}
+                />
+              </div>
+
+              <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Arquivo</label>
+                <input
+                  type="file"
+                  accept="application/pdf"
+                  class="block w-full text-sm text-gray-700 dark:text-gray-200"
+                  on:change={(e) => {
+                    uploadFile = e.currentTarget.files?.[0] ?? null;
+                  }}
+                  disabled={uploadLoading}
+                />
+                <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Por enquanto aceitamos apenas PDF (mas o backend suporta outros tipos).</p>
+              </div>
+
+              {#if uploadError}
+                <div class="rounded border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-sm">{uploadError}</div>
+              {/if}
+
+              <div class="flex justify-end gap-2">
+                <button
+                  class="px-4 py-2 rounded border border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700"
+                  type="button"
+                  on:click={closeUpload}
+                  disabled={uploadLoading}
+                >
+                  Cancelar
+                </button>
+                <button
+                  class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60"
+                  type="button"
+                  on:click={submitUpload}
+                  disabled={uploadLoading}
+                >
+                  {uploadLoading ? 'Enviando...' : 'Enviar'}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      {/if}
+    </div>
+  </div>
+</div>