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>

View File

@@ -17,16 +17,23 @@ export function useDocuments() {
const { get, post, delete: del } = useApi()
const { showError, showSuccess } = useToast()
const loadDocuments = async () => {
const loadFromEndpoint = async (endpoint, { updateStore = false } = {}) => {
loading.value = true
try {
const result = await get('/documents')
const result = await get(endpoint)
if (result.success) {
documents.value = result.data
const data = result.data || []
if (updateStore) {
documents.value = data
}
return { success: true, data }
}
if (result.error) {
showError(result.error)
}
return result
} catch (error) {
console.error('Erreur lors du chargement des documents:', error)
console.error(`Erreur lors du chargement des documents (${endpoint}):`, error)
showError("Impossible de charger les documents")
return { success: false, error: error.message }
} finally {
@@ -34,25 +41,31 @@ export function useDocuments() {
}
}
const loadDocumentsBySite = async (siteId) => {
if (!siteId) return { success: false, error: 'Aucun site sélectionné' }
loading.value = true
try {
const result = await get(`/documents/site/${siteId}`)
if (result.success) {
documents.value = result.data
}
return result
} catch (error) {
console.error('Erreur lors du chargement des documents du site:', error)
showError("Impossible de charger les documents du site")
return { success: false, error: error.message }
} finally {
loading.value = false
}
const loadDocuments = async (options = {}) => {
return loadFromEndpoint('/documents', { updateStore: options.updateStore ?? true })
}
const uploadDocuments = async ({ files = [], context = {} }) => {
const loadDocumentsBySite = async (siteId, options = {}) => {
if (!siteId) return { success: false, error: 'Aucun site sélectionné' }
return loadFromEndpoint(`/documents/site/${siteId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByMachine = async (machineId, options = {}) => {
if (!machineId) return { success: false, error: 'Aucune machine sélectionnée' }
return loadFromEndpoint(`/documents/machine/${machineId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByComponent = async (componentId, options = {}) => {
if (!componentId) return { success: false, error: 'Aucun composant sélectionné' }
return loadFromEndpoint(`/documents/composant/${componentId}`, { updateStore: options.updateStore ?? false })
}
const loadDocumentsByPiece = async (pieceId, options = {}) => {
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 = {} }, { updateStore = false } = {}) => {
if (!files.length) return { success: false, error: 'Aucun fichier sélectionné' }
loading.value = true
@@ -81,7 +94,9 @@ export function useDocuments() {
}
if (created.length) {
documents.value = [...created, ...documents.value]
if (updateStore) {
documents.value = [...created, ...documents.value]
}
return { success: true, data: created }
}
@@ -95,14 +110,16 @@ export function useDocuments() {
}
}
const deleteDocument = async (id) => {
const deleteDocument = async (id, { updateStore = false } = {}) => {
if (!id) return { success: false, error: 'Identifiant manquant' }
loading.value = true
try {
const result = await del(`/documents/${id}`)
if (result.success) {
documents.value = documents.value.filter(doc => doc.id !== id)
if (updateStore) {
documents.value = documents.value.filter(doc => doc.id !== id)
}
showSuccess('Document supprimé')
}
return result
@@ -120,6 +137,9 @@ export function useDocuments() {
loading,
loadDocuments,
loadDocumentsBySite,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece,
uploadDocuments,
deleteDocument,
}

View File

@@ -70,8 +70,15 @@
<tbody>
<tr v-for="document in filteredDocuments" :key="document.id" class="text-sm">
<td>
<div class="font-semibold">{{ document.name }}</div>
<div class="text-xs text-gray-500">{{ document.filename }}</div>
<div class="flex items-center gap-3">
<span class="text-xl" :class="documentIcon(document).colorClass">
{{ documentIcon(document).icon }}
</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>
@@ -104,6 +111,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
const { documents, loading, loadDocuments } = useDocuments()
@@ -155,6 +163,8 @@ const formatSize = (size) => {
return `${formatted.toFixed(1)} ${units[index]}`
}
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const formatDate = (date) => {
if (!date) return '—'
return new Intl.DateTimeFormat('fr-FR', {

View File

@@ -216,6 +216,63 @@
</div>
</div>
<div class="card bg-base-100 shadow-lg mt-6">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<div>
<h2 class="card-title">Documents de la machine</h2>
<p class="text-xs text-gray-500">Ajoutez ou consultez les documents liés à cette machine.</p>
</div>
<span v-if="isEditMode && machineDocumentFiles.length" class="badge badge-outline">
{{ machineDocumentFiles.length }} fichier{{ machineDocumentFiles.length > 1 ? 's' : '' }} sélectionné{{ machineDocumentFiles.length > 1 ? 's' : '' }}
</span>
</div>
<DocumentUpload
v-if="isEditMode"
v-model="machineDocumentFiles"
title="Déposer des fichiers pour la machine"
subtitle="Formats acceptés : PDF, images, documents..."
@files-added="handleMachineFilesAdded"
/>
<div v-if="machineDocumentsList.length" class="space-y-2">
<div
v-for="document in machineDocumentsList"
: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="machineDocumentsUploading"
@click="removeMachineDocument(document.id)"
>
Supprimer
</button>
</div>
</div>
</div>
<p v-else class="text-xs text-gray-500">Aucun document lié à cette machine.</p>
</div>
</div>
<!-- Components Section -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
@@ -301,7 +358,10 @@ import { usePieces } from '~/composables/usePieces'
import { useCustomFields } from '~/composables/useCustomFields'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
import DocumentUpload from '~/components/DocumentUpload.vue'
const route = useRoute()
const machineId = route.params.id
@@ -323,6 +383,13 @@ const {
} = usePieces()
const { upsertCustomFieldValue } = useCustomFields()
const {
uploadDocuments,
deleteDocument,
loadDocumentsByMachine,
loadDocumentsByComponent,
loadDocumentsByPiece
} = useDocuments()
// Data
const loading = ref(true)
@@ -339,6 +406,10 @@ const machineConstructeur = ref('')
// Valeurs des champs personnalisés de la machine
const machineCustomFieldValues = reactive({})
const machineDocumentFiles = ref([])
const machineDocumentsUploading = ref(false)
const machineDocumentsLoaded = ref(false)
// Mode d'édition
const isEditMode = ref(false)
const debug = ref(false) // Ajout de debug pour afficher les infos de debug
@@ -379,6 +450,9 @@ const machinePieces = computed(() => {
return filteredPieces
})
const machineDocumentsList = computed(() => machine.value?.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
const allComponents = computed(() => {
return components.value
})
@@ -391,6 +465,68 @@ const componentPieces = (composantId) => {
return pieces.value.filter(piece => piece.composantId === composantId)
}
const refreshMachineDocuments = async () => {
if (!machine.value?.id) return
const result = await loadDocumentsByMachine(machine.value.id, { updateStore: false })
if (result.success && machine.value) {
machine.value.documents = result.data || []
machineDocumentsLoaded.value = true
}
}
const handleMachineFilesAdded = async (files) => {
if (!files.length || !machine.value?.id) return
machineDocumentsUploading.value = true
try {
const result = await uploadDocuments(
{
files,
context: { machineId: machine.value.id }
},
{ updateStore: false }
)
if (result.success && machine.value) {
const newDocs = result.data || []
machine.value.documents = [...newDocs, ...(machine.value.documents || [])]
machineDocumentFiles.value = []
}
} finally {
machineDocumentsUploading.value = false
}
}
const removeMachineDocument = async (documentId) => {
if (!documentId) return
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success && machine.value) {
machine.value.documents = (machine.value.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]}`
}
// Transform custom field values to custom fields format
const transformCustomFields = (pieces) => {
return pieces.map(piece => {
@@ -406,7 +542,8 @@ const transformCustomFields = (pieces) => {
return {
...piece,
customFields
customFields,
documents: piece.documents || []
}
})
}
@@ -439,7 +576,8 @@ const transformComponentCustomFields = (componentsData) => {
...component,
customFields, // Use customFields for frontend display
pieces,
subComponents // Use the transformed sousComposants as subComponents
subComponents, // Use the transformed sousComposants as subComponents
documents: component.documents || []
};
console.log('Transformed component:', result.name, 'with subComponents:', result.subComponents?.length || 0)
@@ -466,6 +604,8 @@ const loadMachineData = async () => {
if (machineResult.success) {
machine.value = machineResult.data
machine.value.documents = machine.value.documents || []
machineDocumentsLoaded.value = !!(machine.value.documents?.length)
console.log('Machine trouvée et assignée:', machine.value)
} else {
console.error('Machine non trouvée:', machineId)
@@ -515,7 +655,11 @@ const loadMachineData = async () => {
} else {
console.log('Aucune pièce trouvée dans la réponse de la machine')
}
if (!machineDocumentsLoaded.value) {
await refreshMachineDocuments()
}
console.log('Chargement terminé avec succès')
} catch (error) {
console.error('Erreur lors du chargement des données:', error)
@@ -666,6 +810,9 @@ const editPiece = (piece) => {
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
debug.value = !debug.value // Inversez la valeur de debug
if (isEditMode.value && !machineDocumentsLoaded.value) {
refreshMachineDocuments()
}
}
// Lifecycle

View File

@@ -316,10 +316,15 @@
:key="document.id"
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
>
<div class="text-sm">
<div class="font-medium">{{ document.name }}</div>
<div class="text-xs text-gray-500">
{{ document.mimeType || 'Inconnu' }} {{ formatSize(document.size) }}
<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">
@@ -359,10 +364,11 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { useSites } from '~/composables/useSites'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { getFileIcon } from '~/utils/fileIcons'
import DocumentUpload from '~/components/DocumentUpload.vue'
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
const { uploadDocuments, deleteDocument, loadDocumentsBySite, documents: documentStore } = useDocuments()
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
// Data
const showAddSiteModal = ref(false)
@@ -392,6 +398,7 @@ const selectedFiles = ref([])
const uploadingDocuments = ref(false)
const siteDocuments = computed(() => siteBeingEdited.value?.documents || [])
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
// Methods
const handleCreateSite = async () => {
@@ -446,10 +453,13 @@ const handleUpdateSite = async () => {
let uploadedDocuments = []
if (selectedFiles.value.length) {
uploadingDocuments.value = true
const uploadResult = await uploadDocuments({
files: selectedFiles.value,
context: { siteId: siteBeingEdited.value.id }
})
const uploadResult = await uploadDocuments(
{
files: selectedFiles.value,
context: { siteId: siteBeingEdited.value.id }
},
{ updateStore: false }
)
uploadingDocuments.value = false
if (uploadResult.success) {
@@ -502,7 +512,7 @@ const closeEditModal = () => {
const handleRemoveSiteDocument = async (documentId) => {
if (!documentId) return
const result = await deleteDocument(documentId)
const result = await deleteDocument(documentId, { updateStore: false })
if (result.success) {
if (siteBeingEdited.value) {
siteBeingEdited.value.documents = (siteBeingEdited.value.documents || []).filter(doc => doc.id !== documentId)
@@ -535,9 +545,9 @@ const downloadDocument = (doc) => {
const refreshSiteDocuments = async (siteId) => {
if (!siteId) return
const result = await loadDocumentsBySite(siteId)
const result = await loadDocumentsBySite(siteId, { updateStore: false })
if (result.success && siteBeingEdited.value && siteBeingEdited.value.id === siteId) {
const cloned = [...documentStore.value]
const cloned = Array.isArray(result.data) ? [...result.data] : []
siteBeingEdited.value.documents = cloned
const index = sites.value.findIndex(site => site.id === siteId)
if (index !== -1) {

109
app/utils/fileIcons.js Normal file
View File

@@ -0,0 +1,109 @@
const iconMap = [
{
label: 'PDF',
exts: ['pdf'],
icon: '📕',
colorClass: 'text-red-500'
},
{
label: 'Word',
exts: ['doc', 'docx'],
icon: '📝',
colorClass: 'text-blue-500'
},
{
label: 'Excel',
exts: ['xls', 'xlsx', 'csv'],
icon: '📊',
colorClass: 'text-green-500'
},
{
label: 'PowerPoint',
exts: ['ppt', 'pptx'],
icon: '📈',
colorClass: 'text-orange-500'
},
{
label: 'Image',
exts: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'heic'],
icon: '🖼️',
colorClass: 'text-purple-500'
},
{
label: 'Archive',
exts: ['zip', 'rar', '7z', 'tar', 'gz'],
icon: '🗜️',
colorClass: 'text-amber-500'
},
{
label: 'Audio',
exts: ['mp3', 'wav', 'ogg', 'flac', 'aac'],
icon: '🎵',
colorClass: 'text-pink-500'
},
{
label: 'Vidéo',
exts: ['mp4', 'mov', 'avi', 'mkv', 'webm'],
icon: '🎬',
colorClass: 'text-indigo-500'
},
{
label: 'Texte',
exts: ['txt', 'md', 'rtf'],
icon: '📄',
colorClass: 'text-gray-500'
},
{
label: 'Code',
exts: ['json', 'xml', 'yml', 'yaml', 'js', 'ts', 'py', 'java', 'cs'],
icon: '💻',
colorClass: 'text-sky-500'
}
]
const mimeGroups = [
{ prefix: 'image/', icon: '🖼️', colorClass: 'text-purple-500' },
{ prefix: 'video/', icon: '🎬', colorClass: 'text-indigo-500' },
{ prefix: 'audio/', icon: '🎵', colorClass: 'text-pink-500' },
{ prefix: 'text/', icon: '📄', colorClass: 'text-gray-500' },
{ prefix: 'application/pdf', icon: '📕', colorClass: 'text-red-500' },
{ prefix: 'application/zip', icon: '🗜️', colorClass: 'text-amber-500' },
{ prefix: 'application/x-', icon: '🗜️', colorClass: 'text-amber-500' }
]
export const getFileIcon = ({ name = '', mime = '' } = {}) => {
const extension = name.split('.').pop()?.toLowerCase() || ''
if (extension) {
const match = iconMap.find(entry => entry.exts.includes(extension))
if (match) {
return {
icon: match.icon,
colorClass: match.colorClass,
label: match.label
}
}
}
if (mime) {
const match = mimeGroups.find(entry => mime.startsWith(entry.prefix))
if (match) {
return {
icon: match.icon,
colorClass: match.colorClass,
label: mime
}
}
}
return {
icon: '📁',
colorClass: 'text-primary',
label: 'Document'
}
}
export const describeFileType = ({ name = '', mime = '' } = {}) => {
const icon = getFileIcon({ name, mime })
return icon.label
}