|
@@ -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>
|