feat: enhance document management UI

This commit is contained in:
Matthieu
2025-09-17 12:41:51 +02:00
parent 0fbf77ab43
commit 3c0c22ad0f
8 changed files with 660 additions and 47 deletions

View File

@@ -157,6 +157,60 @@
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
<h4 class="font-semibold text-sm text-gray-700">Documents</h4>
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline">
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
</span>
</div>
<p v-if="loadingDocuments" class="text-xs text-gray-500">Chargement des documents...</p>
<DocumentUpload
v-if="isEditMode"
v-model="selectedFiles"
title="Déposer des fichiers pour ce composant"
subtitle="Formats acceptés : PDF, images, documents..."
@files-added="handleFilesAdded"
/>
<div v-if="componentDocuments.length" class="space-y-2">
<div
v-for="document in componentDocuments"
:key="document.id"
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
{{ documentIcon(document).icon }}
</span>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
Télécharger
</button>
<button
v-if="isEditMode"
type="button"
class="btn btn-error btn-xs"
:disabled="uploadingDocuments"
@click="removeDocument(document.id)"
>
Supprimer
</button>
</div>
</div>
</div>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">Aucun document lié à ce composant.</p>
</div>
<!-- Component Pieces -->
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
<h4 class="font-semibold text-gray-700">Pièces du composant</h4>
@@ -194,8 +248,10 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, computed } from 'vue'
import PieceItem from './PieceItem.vue'
import DocumentUpload from './DocumentUpload.vue'
import { useDocuments } from '~/composables/useDocuments'
const props = defineProps({
component: {
@@ -219,17 +275,37 @@ const props = defineProps({
const emit = defineEmits(['update', 'edit-piece'])
const isCollapsed = ref(true)
const selectedFiles = ref([])
const uploadingDocuments = ref(false)
const loadingDocuments = ref(false)
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
const componentDocuments = computed(() => props.component.documents || [])
const { uploadDocuments, deleteDocument, loadDocumentsByComponent } = useDocuments()
watch(
() => props.toggleToken,
() => {
isCollapsed.value = props.collapseAll
if (!isCollapsed.value) {
ensureDocumentsLoaded()
}
},
{ immediate: true }
)
watch(
() => props.component.documents,
(docs) => {
documentsLoaded.value = !!(docs && docs.length)
}
)
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
if (!isCollapsed.value) {
ensureDocumentsLoaded()
}
}
// Methods
@@ -254,4 +330,76 @@ const updatePieceCustomField = (fieldUpdate) => {
// Forward to parent
emit('edit-piece', { ...fieldUpdate, type: 'custom-field-update' })
}
const ensureDocumentsLoaded = async () => {
if (documentsLoaded.value || !props.component?.id) return
await refreshDocuments()
}
const refreshDocuments = async () => {
loadingDocuments.value = true
try {
const result = await loadDocumentsByComponent(props.component.id, { updateStore: false })
if (result.success) {
props.component.documents = result.data || []
documentsLoaded.value = true
}
} finally {
loadingDocuments.value = false
}
}
const handleFilesAdded = async (files) => {
if (!files.length || !props.component?.id) return
uploadingDocuments.value = true
try {
const result = await uploadDocuments(
{
files,
context: { composantId: props.component.id }
},
{ updateStore: false }
)
if (result.success) {
const newDocs = result.data || []
props.component.documents = [...newDocs, ...(props.component.documents || [])]
documentsLoaded.value = true
selectedFiles.value = []
}
} finally {
uploadingDocuments.value = false
}
}
const removeDocument = async (documentId) => {
if (!documentId) return
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success) {
props.component.documents = (props.component.documents || []).filter(doc => doc.id !== documentId)
}
}
const downloadDocument = (doc) => {
if (!doc?.path) return
if (doc.path.startsWith('data:')) {
const link = document.createElement('a')
link.href = doc.path
link.download = doc.filename || doc.name || 'document'
link.click()
return
}
window.open(doc.path, '_blank')
}
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]}`
}
</script>

View File

@@ -34,9 +34,12 @@
<ul v-if="selectedFiles.length" class="mt-4 w-full space-y-2 text-left">
<li v-for="file in selectedFiles" :key="file.name" class="flex items-center justify-between text-sm">
<div class="flex flex-col">
<span class="font-medium">{{ file.name }}</span>
<span class="text-xs text-gray-500">{{ formatSize(file.size) }} {{ file.type || 'Type inconnu' }}</span>
<div class="flex items-center gap-3">
<span class="text-xl" :class="getIcon(file).colorClass">{{ getIcon(file).icon }}</span>
<div class="flex flex-col">
<span class="font-medium">{{ file.name }}</span>
<span class="text-xs text-gray-500">{{ formatSize(file.size) }} {{ file.type || 'Type inconnu' }}</span>
</div>
</div>
<button type="button" class="btn btn-ghost btn-xs" @click="removeFile(file)">
Retirer
@@ -49,6 +52,8 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { useToast } from '~/composables/useToast'
import { getFileIcon } from '~/utils/fileIcons'
const props = defineProps({
title: {
@@ -71,6 +76,10 @@ const props = defineProps({
type: Array,
default: () => [],
},
maxFileSizeMb: {
type: Number,
default: 100,
},
})
const emit = defineEmits(['update:modelValue', 'files-added'])
@@ -78,6 +87,7 @@ const emit = defineEmits(['update:modelValue', 'files-added'])
const dragActive = ref(false)
const fileInput = ref(null)
const internalFiles = ref([])
const { showError } = useToast()
const selectedFiles = computed(() => internalFiles.value)
@@ -103,11 +113,21 @@ const emitFiles = (files) => {
const handleFiles = (fileList) => {
const files = Array.from(fileList)
const maxBytes = props.maxFileSizeMb * 1024 * 1024
if (!props.multiple) {
const validFile = files[0]
if (validFile && validFile.size > maxBytes) {
showError(`Le fichier "${validFile.name}" dépasse la limite de ${props.maxFileSizeMb} Mo`)
return
}
emitFiles(files.slice(0, 1))
} else {
const merged = [...internalFiles.value]
files.forEach((file) => {
if (file.size > maxBytes) {
showError(`Le fichier "${file.name}" dépasse la limite de ${props.maxFileSizeMb} Mo`)
return
}
if (!merged.some(existing => existing.name === file.name && existing.size === file.size)) {
merged.push(file)
}
@@ -148,4 +168,8 @@ const formatSize = (size) => {
const formatted = size / Math.pow(1024, index)
return `${formatted.toFixed(1)} ${units[index]}`
}
const getIcon = (file) => {
return getFileIcon({ name: file.name, mime: file.type })
}
</script>

View File

@@ -165,13 +165,70 @@
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200 space-y-3">
<div class="flex items-center justify-between">
<h5 class="text-sm font-medium text-gray-700">Documents</h5>
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline">
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
</span>
</div>
<p v-if="loadingDocuments" class="text-xs text-gray-500">Chargement des documents...</p>
<DocumentUpload
v-if="isEditMode"
v-model="selectedFiles"
title="Déposer des fichiers pour cette pièce"
subtitle="Formats acceptés : PDF, images, documents..."
@files-added="handleFilesAdded"
/>
<div v-if="pieceDocuments.length" class="space-y-2">
<div
v-for="document in pieceDocuments"
:key="document.id"
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="flex items-center gap-3 text-sm">
<span class="text-xl" :class="documentIcon(document).colorClass">
{{ documentIcon(document).icon }}
</span>
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
Télécharger
</button>
<button
v-if="isEditMode"
type="button"
class="btn btn-error btn-xs"
:disabled="uploadingDocuments"
@click="removeDocument(document.id)"
>
Supprimer
</button>
</div>
</div>
</div>
<p v-else-if="!loadingDocuments" class="text-xs text-gray-500">Aucun document lié à cette pièce.</p>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted, watch } from 'vue'
import { reactive, onMounted, watch, ref, computed } from 'vue'
import { useCustomFields } from '~/composables/useCustomFields'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import DocumentUpload from '~/components/DocumentUpload.vue'
const props = defineProps({
piece: {
@@ -195,6 +252,90 @@ const pieceData = reactive({
prix: props.piece.prix || ''
})
const selectedFiles = ref([])
const uploadingDocuments = ref(false)
const loadingDocuments = ref(false)
const documentsLoaded = ref(!!(props.piece.documents && props.piece.documents.length))
const pieceDocuments = computed(() => props.piece.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const { uploadDocuments, deleteDocument, loadDocumentsByPiece } = useDocuments()
const refreshDocuments = async () => {
if (!props.piece?.id) return
loadingDocuments.value = true
try {
const result = await loadDocumentsByPiece(props.piece.id, { updateStore: false })
if (result.success) {
props.piece.documents = result.data || []
documentsLoaded.value = true
}
} finally {
loadingDocuments.value = false
}
}
const handleFilesAdded = async (files) => {
if (!files.length || !props.piece?.id) return
uploadingDocuments.value = true
try {
const result = await uploadDocuments(
{
files,
context: { pieceId: props.piece.id }
},
{ updateStore: false }
)
if (result.success) {
const newDocs = result.data || []
props.piece.documents = [...newDocs, ...(props.piece.documents || [])]
documentsLoaded.value = true
selectedFiles.value = []
}
} finally {
uploadingDocuments.value = false
}
}
const removeDocument = async (documentId) => {
if (!documentId) return
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success) {
props.piece.documents = (props.piece.documents || []).filter(doc => doc.id !== documentId)
}
}
const downloadDocument = (doc) => {
if (!doc?.path) return
if (doc.path.startsWith('data:')) {
const link = document.createElement('a')
link.href = doc.path
link.download = doc.filename || doc.name || 'document'
link.click()
return
}
window.open(doc.path, '_blank')
}
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]}`
}
watch(
() => props.piece.documents,
(docs) => {
documentsLoaded.value = !!(docs && docs.length)
}
)
// Méthodes pour gérer les champs personnalisés
const setCustomFieldValue = (fieldValueId, value) => {
const fieldValue = props.piece.customFieldValues?.find(fv => fv.id === fieldValueId)
@@ -243,5 +384,9 @@ onMounted(() => {
// Debug: vérifier si les champs personnalisés sont présents
console.log('PieceItem - piece:', props.piece)
console.log('PieceItem - customFieldValues:', props.piece.customFieldValues)
if (!documentsLoaded.value) {
refreshDocuments()
}
})
</script>
</script>