feat: enhance document management UI
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
109
app/utils/fileIcons.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user