ajustes
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<!-- front-end/app/pages/gestao/documentos.vue -->
|
||||
<script setup lang="ts">
|
||||
const { apiFetch } = useApi()
|
||||
const { public: { apiBase } } = useRuntimeConfig()
|
||||
const token = useCookie<string | null>('auth_token')
|
||||
|
||||
interface ApiDocument {
|
||||
ID: string
|
||||
@@ -10,6 +12,13 @@ interface ApiDocument {
|
||||
Observacoes: string
|
||||
}
|
||||
|
||||
interface ApiFile {
|
||||
ID: string
|
||||
Nome: string
|
||||
Size: number
|
||||
CreatedAt: string
|
||||
}
|
||||
|
||||
const { data: documentos, refresh } = await useAsyncData('documentos', () =>
|
||||
apiFetch<ApiDocument[]>('/documents')
|
||||
)
|
||||
@@ -56,6 +65,12 @@ function formatDate(iso: string | null): string {
|
||||
return `${d}/${m}/${y}`
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
// ---------- menu ⋯ ----------
|
||||
const menuAberto = ref<string | null>(null)
|
||||
const menuPos = ref({ top: 0, left: 0 })
|
||||
@@ -69,6 +84,65 @@ function abrirMenu(id: string, event: MouseEvent) {
|
||||
|
||||
function fecharMenu() { menuAberto.value = null }
|
||||
|
||||
// ---------- modal visualizar ----------
|
||||
const showVisualizar = ref(false)
|
||||
const viewDoc = ref<ApiDocument | null>(null)
|
||||
const viewFiles = ref<ApiFile[]>([])
|
||||
const viewFilesLoading = ref(false)
|
||||
const uploadLoading = ref(false)
|
||||
|
||||
async function abrirVisualizar(doc: ApiDocument) {
|
||||
fecharMenu()
|
||||
viewDoc.value = doc
|
||||
showVisualizar.value = true
|
||||
viewFilesLoading.value = true
|
||||
try {
|
||||
viewFiles.value = await apiFetch<ApiFile[]>(`/documents/${doc.ID}/files`)
|
||||
} catch { viewFiles.value = [] }
|
||||
viewFilesLoading.value = false
|
||||
}
|
||||
|
||||
async function uploadArquivos(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input.files?.length || !viewDoc.value) return
|
||||
uploadLoading.value = true
|
||||
const formData = new FormData()
|
||||
for (const file of Array.from(input.files)) formData.append('file', file)
|
||||
try {
|
||||
await $fetch(`${apiBase}/documents/${viewDoc.value.ID}/files`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token.value}` },
|
||||
body: formData,
|
||||
})
|
||||
viewFiles.value = await apiFetch<ApiFile[]>(`/documents/${viewDoc.value.ID}/files`)
|
||||
} catch {}
|
||||
uploadLoading.value = false
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
async function excluirArquivo(fileId: string) {
|
||||
if (!viewDoc.value) return
|
||||
try {
|
||||
await apiFetch(`/documents/${viewDoc.value.ID}/files/${fileId}`, { method: 'DELETE' })
|
||||
viewFiles.value = viewFiles.value.filter(f => f.ID !== fileId)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function downloadArquivo(docId: string, fileId: string, nome: string) {
|
||||
try {
|
||||
const blob = await $fetch<Blob>(`${apiBase}/documents/${docId}/files/${fileId}/download`, {
|
||||
headers: { Authorization: `Bearer ${token.value}` },
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = nome
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ---------- modal criar ----------
|
||||
const showCriar = ref(false)
|
||||
const criando = ref(false)
|
||||
@@ -221,8 +295,69 @@ async function confirmarExclusao() {
|
||||
:style="{ top: menuPos.top + 'px', left: menuPos.left + 'px' }"
|
||||
@click.stop
|
||||
>
|
||||
<button @click="abrirVisualizar(documentos!.find(d => d.ID === menuAberto)!); menuAberto = null">Visualizar</button>
|
||||
<button @click="abrirEditar(documentos!.find(d => d.ID === menuAberto)!)">Editar</button>
|
||||
<button @click="abrirExcluir(documentos!.find(d => d.ID === menuAberto)!)">Excluir</button>
|
||||
<button class="drop-danger" @click="abrirExcluir(documentos!.find(d => d.ID === menuAberto)!)">Excluir</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Modal Visualizar -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showVisualizar" class="modal-overlay" @click.self="showVisualizar = false">
|
||||
<div class="modal modal-view">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2>{{ viewDoc?.Nome || tipoLabel(viewDoc?.Tipo ?? '') }}</h2>
|
||||
<span
|
||||
class="doc-status"
|
||||
:style="{ color: STATUS_CFG[calcStatus(viewDoc?.DataVencimento ?? null)].color, background: STATUS_CFG[calcStatus(viewDoc?.DataVencimento ?? null)].bg }"
|
||||
>
|
||||
{{ STATUS_CFG[calcStatus(viewDoc?.DataVencimento ?? null)].label }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="close-btn" @click="showVisualizar = false">✕</button>
|
||||
</div>
|
||||
<div class="view-grid">
|
||||
<div class="view-field">
|
||||
<label>Tipo</label>
|
||||
<span>{{ tipoLabel(viewDoc?.Tipo ?? '') }}</span>
|
||||
</div>
|
||||
<div class="view-field">
|
||||
<label>Vencimento</label>
|
||||
<span>{{ formatDate(viewDoc?.DataVencimento ?? null) }}</span>
|
||||
</div>
|
||||
<div class="view-field view-full">
|
||||
<label>Observações</label>
|
||||
<span>{{ viewDoc?.Observacoes || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="files-section">
|
||||
<div class="files-header">
|
||||
<h3>Arquivos</h3>
|
||||
<label class="upload-btn" :class="{ loading: uploadLoading }">
|
||||
<input type="file" multiple @change="uploadArquivos" style="display:none" :disabled="uploadLoading" />
|
||||
{{ uploadLoading ? 'Enviando...' : '+ Anexar' }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="viewFilesLoading" class="files-empty">Carregando arquivos...</div>
|
||||
<div v-else-if="viewFiles.length === 0" class="files-empty">Nenhum arquivo anexado.</div>
|
||||
<div v-else class="files-list">
|
||||
<div v-for="f in viewFiles" :key="f.ID" class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ f.Nome }}</span>
|
||||
<span class="file-size">{{ formatFileSize(f.Size) }}</span>
|
||||
<button class="file-act" title="Download" @click="downloadArquivo(viewDoc!.ID, f.ID, f.Nome)">⬇</button>
|
||||
<button class="file-act file-del" title="Remover" @click="excluirArquivo(f.ID)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-cancel" @click="showVisualizar = false">Fechar</button>
|
||||
<button class="btn-save" @click="showVisualizar = false; abrirEditar(viewDoc!)">Editar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -348,9 +483,11 @@ async function confirmarExclusao() {
|
||||
.btn-menu:hover { background: #f1f5f9; color: #334155; }
|
||||
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px 12px; border-bottom: 1px solid #f1f5f9; }
|
||||
.modal-header h2 { font-size: 16px; font-weight: 700; color: #0f172a; margin: 0; }
|
||||
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 18px 20px 12px; border-bottom: 1px solid #f1f5f9; }
|
||||
.modal-header h2 { font-size: 16px; font-weight: 700; color: #0f172a; margin: 0 0 6px; }
|
||||
.modal-header button { background: none; border: none; font-size: 18px; cursor: pointer; color: #94a3b8; }
|
||||
.close-btn { background: none; border: none; font-size: 16px; color: #94a3b8; cursor: pointer; padding: 4px 8px; border-radius: 6px; }
|
||||
.close-btn:hover { background: #f1f5f9; color: #475569; }
|
||||
.modal-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 12px 20px 18px; border-top: 1px solid #f1f5f9; }
|
||||
|
||||
@@ -365,6 +502,32 @@ async function confirmarExclusao() {
|
||||
.btn-danger { background: #dc2626; color: white; border: none; border-radius: 8px; padding: 7px 16px; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.erro { color: #dc2626; font-size: 12px; }
|
||||
|
||||
/* Modal visualizar */
|
||||
.modal-view { max-width: 620px !important; }
|
||||
.view-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 16px 20px 4px; }
|
||||
.view-full { grid-column: 1 / -1; }
|
||||
.view-field { display: flex; flex-direction: column; gap: 3px; }
|
||||
.view-field label { font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.view-field span { font-size: 13px; color: #1e293b; }
|
||||
|
||||
/* Arquivos */
|
||||
.files-section { border-top: 1px solid #f1f5f9; padding: 16px 20px 4px; margin-top: 8px; }
|
||||
.files-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.files-header h3 { font-size: 11px; font-weight: 700; color: #94a3b8; margin: 0; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.upload-btn { font-size: 12px; font-weight: 600; padding: 5px 12px; border-radius: 8px; border: 1px dashed #667eea; color: #667eea; cursor: pointer; transition: all 0.15s; }
|
||||
.upload-btn:hover { background: #f0f3ff; }
|
||||
.upload-btn.loading { opacity: 0.6; cursor: not-allowed; }
|
||||
.files-empty { font-size: 13px; color: #94a3b8; text-align: center; padding: 16px 0; }
|
||||
.files-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.file-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; }
|
||||
.file-icon { font-size: 16px; flex-shrink: 0; }
|
||||
.file-name { flex: 1; font-size: 13px; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-size { font-size: 11px; color: #94a3b8; white-space: nowrap; flex-shrink: 0; }
|
||||
.file-act { font-size: 13px; padding: 3px 7px; background: none; border: none; cursor: pointer; color: #667eea; border-radius: 4px; flex-shrink: 0; }
|
||||
.file-act:hover { background: #f0f3ff; }
|
||||
.file-del { color: #dc2626; }
|
||||
.file-del:hover { background: #fff1f1 !important; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@@ -387,4 +550,6 @@ async function confirmarExclusao() {
|
||||
font-size: 13px; background: none; border: none; cursor: pointer; color: #374151;
|
||||
}
|
||||
.dropdown-fixed button:hover { background: #f8fafc; }
|
||||
.dropdown-fixed .drop-danger { color: #dc2626; }
|
||||
.dropdown-fixed .drop-danger:hover { background: #fff1f1; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user