Files
Inventory_frontend/app/composables/useDocuments.ts
Matthieu e88ed5b8f2 feat(documents): migrate storage to filesystem, add server-side pagination
- Replace Base64 data URIs with file-based storage served via dedicated endpoints
- Add DocumentPreviewModal navigation, DocumentThumbnail fileUrl support
- Refactor documents page with server-side pagination, search, sort and filters
- Update all components to use fileUrl/downloadUrl instead of raw path
- Add pagination composable support (total, page, itemsPerPage, attachmentFilter)

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

298 lines
8.8 KiB
TypeScript

import { ref } from 'vue'
import { useApi } from './useApi'
import { useToast } from './useToast'
import { extractCollection } from '~/shared/utils/apiHelpers'
export interface Document {
id: string
name: string
filename: string
mimeType: string
size: number
fileUrl: string
downloadUrl: string
/** @deprecated Legacy Base64 data URI — use fileUrl instead */
path?: string
createdAt?: string
siteId?: string
machineId?: string
composantId?: string
productId?: string
pieceId?: string
site?: { id: string; name?: string } | null
machine?: { id: string; name?: string } | null
composant?: { id: string; name?: string } | null
piece?: { id: string; name?: string } | null
product?: { id: string; name?: string } | null
}
export interface UploadContext {
siteId?: string
machineId?: string
composantId?: string
productId?: string
pieceId?: string
}
export interface DocumentResult {
success: boolean
data?: Document | Document[]
error?: string
}
interface LoadDocumentsOptions {
search?: string
page?: number
itemsPerPage?: number
orderBy?: string
orderDir?: 'asc' | 'desc'
attachmentFilter?: string
force?: boolean
}
const documents = ref<Document[]>([])
const total = ref(0)
const loading = ref(false)
const loaded = ref(false)
const extractTotal = (payload: unknown, fallbackLength: number): number => {
const p = payload as Record<string, unknown> | null
if (typeof p?.totalItems === 'number') return p.totalItems
if (typeof p?.['hydra:totalItems'] === 'number') return p['hydra:totalItems']
return fallbackLength
}
export function useDocuments() {
const { get, postFormData, delete: del } = useApi()
const { showError, showSuccess } = useToast()
const loadFromEndpoint = async (
endpoint: string,
{ updateStore = false, itemsPerPage }: { updateStore?: boolean; itemsPerPage?: number } = {},
): Promise<DocumentResult> => {
loading.value = true
try {
const url = itemsPerPage ? `${endpoint}${endpoint.includes('?') ? '&' : '?'}itemsPerPage=${itemsPerPage}` : endpoint
const result = await get(url)
if (result.success) {
const data = extractCollection(result.data)
if (updateStore) {
documents.value = data
}
return { success: true, data }
}
if (result.error) {
showError(result.error)
}
return result as DocumentResult
} catch (error) {
const err = error as Error
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
showError('Impossible de charger les documents')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const loadDocuments = async (options: LoadDocumentsOptions = {}): Promise<DocumentResult> => {
const {
search = '',
page = 1,
itemsPerPage = 30,
orderBy = 'createdAt',
orderDir = 'desc',
attachmentFilter = 'all',
force = false,
} = options
if (!force && loaded.value && !search && page === 1 && attachmentFilter === 'all') {
return { success: true, data: documents.value }
}
if (loading.value) {
return { success: true, data: documents.value }
}
loading.value = true
try {
const params = new URLSearchParams()
params.set('itemsPerPage', String(itemsPerPage))
params.set('page', String(page))
if (search && search.trim()) {
params.set('name', search.trim())
}
if (attachmentFilter && attachmentFilter !== 'all') {
params.set(`exists[${attachmentFilter}]`, 'true')
}
params.set(`order[${orderBy}]`, orderDir)
const result = await get(`/documents?${params.toString()}`)
if (result.success) {
const items = extractCollection(result.data)
documents.value = items
total.value = extractTotal(result.data, items.length)
loaded.value = true
return { success: true, data: items }
}
if (result.error) {
showError(result.error)
}
return result as DocumentResult
} catch (error) {
const err = error as Error
console.error('Erreur lors du chargement des documents:', error)
showError('Impossible de charger les documents')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const loadDocumentsBySite = async (
siteId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!siteId) {
return { success: false, error: 'Aucun site sélectionné' }
}
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByMachine = async (
machineId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!machineId) {
return { success: false, error: 'Aucune machine sélectionnée' }
}
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByComponent = async (
componentId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!componentId) {
return { success: false, error: 'Aucun composant sélectionné' }
}
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByProduct = async (
productId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!productId) {
return { success: false, error: 'Aucun produit sélectionné' }
}
return loadFromEndpoint(`/documents/product/${productId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByPiece = async (
pieceId: string,
options: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!pieceId) {
return { success: false, error: 'Aucune pièce sélectionnée' }
}
return loadFromEndpoint(`/documents/piece/${pieceId}`, { updateStore: options.updateStore ?? false })
}
const uploadDocuments = async (
{ files, context = {} }: { files: File[]; context?: UploadContext },
{ updateStore = false }: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!files.length) {
return { success: false, error: 'Aucun fichier sélectionné' }
}
loading.value = true
const created: Document[] = []
try {
for (const file of files) {
const formData = new FormData()
formData.append('file', file)
formData.append('name', file.name)
if (context.siteId) formData.append('siteId', context.siteId)
if (context.machineId) formData.append('machineId', context.machineId)
if (context.composantId) formData.append('composantId', context.composantId)
if (context.productId) formData.append('productId', context.productId)
if (context.pieceId) formData.append('pieceId', context.pieceId)
const result = await postFormData('/documents', formData)
if (result.success) {
created.push(result.data as Document)
showSuccess(`Document "${file.name}" ajouté`)
} else if (result.error) {
showError(`Erreur sur ${file.name} : ${result.error}`)
}
}
if (created.length) {
if (updateStore) {
documents.value = [...created, ...documents.value]
}
return { success: true, data: created }
}
return { success: false, error: 'Aucun document ajouté' }
} catch (error) {
const err = error as Error
console.error("Erreur lors de l'upload des documents:", error)
showError("Échec de l'ajout des documents")
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
const deleteDocument = async (
id: string | number,
{ updateStore = false }: { updateStore?: boolean } = {},
): Promise<DocumentResult> => {
if (!id) {
return { success: false, error: 'Identifiant manquant' }
}
loading.value = true
try {
const result = await del(`/documents/${id}`)
if (result.success) {
if (updateStore) {
documents.value = documents.value.filter((doc) => doc.id !== id)
}
showSuccess('Document supprimé')
}
return result as DocumentResult
} catch (error) {
const err = error as Error
console.error('Erreur lors de la suppression du document:', error)
showError('Impossible de supprimer le document')
return { success: false, error: err.message }
} finally {
loading.value = false
}
}
return {
documents,
total,
loading,
loaded,
loadDocuments,
loadDocumentsBySite,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece,
loadDocumentsByProduct,
uploadDocuments,
deleteDocument,
}
}