Files
Inventory_frontend/app/pages/documents.vue
Matthieu 5cab15422d fix(documents) : exclude path from collection to prevent OOM, lazy-load on demand
The path field contains base64 data URIs that can be several MB each.
Loading 200 documents at once exceeded the 128MB PHP memory limit.
Now the collection endpoint uses document:list group (without path)
and the frontend fetches the full document on demand when the user
clicks download or preview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 17:16:15 +01:00

239 lines
8.0 KiB
Vue

<template>
<main class="container mx-auto px-6 py-8 space-y-8">
<DocumentPreviewModal
:document="previewDocument"
:visible="previewVisible"
@close="closePreview"
/>
<section class="card bg-base-100 shadow-lg">
<div class="card-body space-y-6">
<div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div class="w-full md:w-2/3">
<label class="label">
<span class="label-text">Recherche</span>
</label>
<input
v-model="searchTerm"
type="search"
placeholder="Nom du document, type, site, machine..."
class="input input-bordered w-full"
>
</div>
<div class="w-full md:w-1/3">
<label class="label">
<span class="label-text">Filtrer par rattachement</span>
</label>
<select v-model="attachmentFilter" class="select select-bordered w-full">
<option value="all">
Tous
</option>
<option value="site">
Sites
</option>
<option value="machine">
Machines
</option>
<option value="composant">
Composants
</option>
<option value="piece">
Pièces
</option>
</select>
</div>
</div>
<div class="divider my-0" />
<div v-if="loading" class="flex flex-col items-center justify-center py-16 text-sm text-gray-500">
<span class="loading loading-spinner loading-lg mb-3" />
Chargement des documents...
</div>
<div v-else-if="filteredDocuments.length === 0" class="text-center py-16 text-sm text-gray-500">
<IconLucideFileSearch class="mx-auto mb-4 h-14 w-14 text-gray-400" aria-hidden="true" />
Aucun document ne correspond à votre recherche pour l'instant.
</div>
<div v-else class="overflow-x-auto">
<table class="table">
<thead>
<tr class="text-xs uppercase">
<th>Nom</th>
<th>Type</th>
<th>Taille</th>
<th>Rattaché à</th>
<th>Date</th>
<th class="text-right">
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="document in filteredDocuments" :key="document.id" class="text-sm">
<td>
<div class="flex items-center gap-3">
<span class="text-xl" :class="documentIcon(document).colorClass">
<component
:is="documentIcon(document).component"
class="h-6 w-6"
aria-hidden="true"
/>
</span>
<div>
<div class="font-semibold">
{{ document.name }}
</div>
<div class="text-xs text-gray-500">
{{ document.filename }}
</div>
</div>
</div>
</td>
<td>{{ document.mimeType || 'Inconnu' }}</td>
<td>{{ formatSize(document.size) }}</td>
<td>
<div class="flex flex-col text-xs">
<span v-if="document.site">Site · {{ document.site.name }}</span>
<span v-else-if="document.machine">Machine · {{ document.machine.name }}</span>
<span v-else-if="document.composant">Composant · {{ document.composant.name }}</span>
<span v-else-if="document.piece">Pièce · {{ document.piece.name }}</span>
<span v-else class="text-gray-400">Non défini</span>
</div>
</td>
<td>{{ formatFrenchDate(document.createdAt) }}</td>
<td class="text-right">
<div class="flex justify-end gap-2">
<button
class="btn btn-ghost btn-xs"
type="button"
:disabled="!canPreviewDocument(document)"
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
@click="openPreview(document)"
>
Consulter
</button>
<button class="btn btn-ghost btn-xs" type="button" @click="downloadDocument(document)">
Télécharger
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</main>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useDocuments } from '~/composables/useDocuments'
import { useApi } from '~/composables/useApi'
import { useUrlState } from '~/composables/useUrlState'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument } from '~/utils/documentPreview'
import { formatFrenchDate } from '~/utils/date'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import IconLucideFileSearch from '~icons/lucide/file-search'
const { documents, loading, loadDocuments } = useDocuments()
const { get } = useApi()
const { q: searchTerm, filter: attachmentFilter } = useUrlState({
q: { default: '', debounce: 300 },
filter: { default: 'all' },
})
const previewDocument = ref(null)
const previewVisible = ref(false)
onMounted(() => {
loadDocuments({ itemsPerPage: 200 })
})
const filteredDocuments = computed(() => {
const term = searchTerm.value.trim().toLowerCase()
const filter = attachmentFilter.value
return documents.value.filter((document) => {
const matchesFilter =
filter === 'all' ||
(filter === 'site' && document.site) ||
(filter === 'machine' && document.machine) ||
(filter === 'composant' && document.composant) ||
(filter === 'piece' && document.piece)
if (!matchesFilter) { return false }
if (!term) { return true }
const searchable = [
document.name,
document.filename,
document.mimeType,
document.site?.name,
document.machine?.name,
document.composant?.name,
document.piece?.name
]
.filter(Boolean)
.map(value => value.toLowerCase())
return searchable.some(value => value.includes(term))
})
})
const formatSize = (size) => {
if (size === undefined || size === null) { return '—' }
if (size === 0) { return '0 B' }
const units = ['B', 'KB', 'MB', 'GB']
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
const formatted = size / Math.pow(1024, index)
return `${formatted.toFixed(1)} ${units[index]}`
}
const documentIcon = doc => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
/** Fetch the full document (with path) from the API on demand. */
const fetchDocumentPath = async (doc) => {
if (doc?.path) { return doc.path }
if (!doc?.id) { return null }
const result = await get(`/documents/${doc.id}`)
if (result.success && result.data?.path) {
doc.path = result.data.path
return result.data.path
}
return null
}
const downloadDocument = async (doc) => {
const path = await fetchDocumentPath(doc)
if (!path) { return }
if (path.startsWith('data:')) {
const link = document.createElement('a')
link.href = path
link.download = doc.filename || doc.name || 'document'
link.click()
return
}
window.open(path, '_blank')
}
const openPreview = async (doc) => {
if (!canPreviewDocument(doc)) { return }
await fetchDocumentPath(doc)
previewDocument.value = doc
previewVisible.value = true
}
const closePreview = () => {
previewVisible.value = false
previewDocument.value = null
}
</script>