- 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>
298 lines
8.8 KiB
TypeScript
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,
|
|
}
|
|
}
|