feat: add document preview overlay
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<DocumentPreviewModal
|
||||||
|
:document="previewDocument"
|
||||||
|
:visible="previewVisible"
|
||||||
|
@close="closePreview"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Component Header -->
|
<!-- Component Header -->
|
||||||
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
||||||
<div class="flex items-start gap-3 w-full">
|
<div class="flex items-start gap-3 w-full">
|
||||||
@@ -199,6 +205,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="!canPreviewDocument(document)"
|
||||||
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||||
|
@click="openPreview(document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
||||||
Télécharger
|
Télécharger
|
||||||
</button>
|
</button>
|
||||||
@@ -260,6 +275,8 @@ import DocumentUpload from './DocumentUpload.vue'
|
|||||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
@@ -289,6 +306,8 @@ const loadingDocuments = ref(false)
|
|||||||
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
|
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
|
||||||
const componentDocuments = computed(() => props.component.documents || [])
|
const componentDocuments = computed(() => props.component.documents || [])
|
||||||
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||||
|
const previewDocument = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
const handleConstructeurChange = async (value) => {
|
const handleConstructeurChange = async (value) => {
|
||||||
props.component.constructeurId = value
|
props.component.constructeurId = value
|
||||||
@@ -409,6 +428,17 @@ const downloadDocument = (doc) => {
|
|||||||
window.open(doc.path, '_blank')
|
window.open(doc.path, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (doc) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const formatSize = (size) => {
|
const formatSize = (size) => {
|
||||||
if (size === undefined || size === null) return '—'
|
if (size === undefined || size === null) return '—'
|
||||||
if (size === 0) return '0 B'
|
if (size === 0) return '0 B'
|
||||||
|
|||||||
152
app/components/DocumentPreviewModal.vue
Normal file
152
app/components/DocumentPreviewModal.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="fixed inset-0 z-[1200] flex items-center justify-center bg-black/60 backdrop-blur-sm px-4 py-6"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-[1600px] h-full max-h-[94vh] bg-base-100 rounded-2xl shadow-2xl flex flex-col overflow-hidden">
|
||||||
|
<header class="flex items-start justify-between gap-4 p-6 border-b border-base-200">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="font-bold text-xl truncate">Prévisualisation</h3>
|
||||||
|
<p class="text-sm text-gray-500 truncate">
|
||||||
|
{{ document?.name || document?.filename }}<span v-if="documentDescription"> • {{ documentDescription }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm shrink-0" @click="close">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="flex-1 bg-base-200/40 px-6 py-5 overflow-hidden">
|
||||||
|
<div class="h-full w-full rounded-xl border border-base-300 bg-base-100 flex items-center justify-center overflow-hidden">
|
||||||
|
<template v-if="previewType === 'image'">
|
||||||
|
<img :src="document?.path" alt="preview" class="max-h-full max-w-full object-contain" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="previewType === 'pdf'">
|
||||||
|
<iframe
|
||||||
|
:src="document?.path"
|
||||||
|
class="w-full h-full bg-white"
|
||||||
|
frameborder="0"
|
||||||
|
title="Aperçu PDF"
|
||||||
|
></iframe>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="previewType === 'audio'">
|
||||||
|
<audio :src="document?.path" controls class="w-full"></audio>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="previewType === 'video'">
|
||||||
|
<video :src="document?.path" controls class="w-full h-full bg-black"></video>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="previewType === 'text'">
|
||||||
|
<div class="w-full h-full overflow-auto">
|
||||||
|
<div v-if="textLoading" class="flex items-center justify-center py-10 text-sm text-gray-500">
|
||||||
|
<span class="loading loading-spinner loading-md mr-2"></span>
|
||||||
|
Chargement du document...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="textError" class="alert alert-error text-sm">
|
||||||
|
{{ textError }}
|
||||||
|
</div>
|
||||||
|
<pre v-else class="bg-base-100 border border-base-300 rounded-lg p-4 whitespace-pre-wrap">
|
||||||
|
{{ textContent }}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="text-sm text-gray-500 text-center px-6">
|
||||||
|
Prévisualisation non disponible pour ce type de document.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="border-t border-base-200 px-6 py-4 flex flex-wrap gap-2 justify-end bg-base-100">
|
||||||
|
<button type="button" class="btn" @click="close">Fermer</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="download">
|
||||||
|
Télécharger
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { getPreviewType, describeDocument } from '~/utils/documentPreview'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
document: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const previewType = computed(() => getPreviewType(props.document))
|
||||||
|
const documentDescription = computed(() => describeDocument(props.document))
|
||||||
|
|
||||||
|
const textContent = ref('')
|
||||||
|
const textLoading = ref(false)
|
||||||
|
const textError = ref('')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.document,
|
||||||
|
async (doc) => {
|
||||||
|
textContent.value = ''
|
||||||
|
textError.value = ''
|
||||||
|
textLoading.value = false
|
||||||
|
|
||||||
|
if (!doc) return
|
||||||
|
if (getPreviewType(doc) !== 'text') return
|
||||||
|
|
||||||
|
try {
|
||||||
|
textLoading.value = true
|
||||||
|
const path = doc.path || ''
|
||||||
|
if (path.startsWith('data:')) {
|
||||||
|
const base64Part = path.split(',')[1] || ''
|
||||||
|
if (!base64Part) {
|
||||||
|
textError.value = 'Impossible de lire ce document texte.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const decoded = atob(base64Part)
|
||||||
|
textContent.value = decodeURIComponent(escape(decoded))
|
||||||
|
} else {
|
||||||
|
const response = await fetch(path)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Téléchargement du document impossible')
|
||||||
|
}
|
||||||
|
textContent.value = await response.text()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement du texte:', error)
|
||||||
|
textError.value = error.message || 'Impossible de lire ce document.'
|
||||||
|
} finally {
|
||||||
|
textLoading.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = () => {
|
||||||
|
if (!props.document?.path) return
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = props.document.path
|
||||||
|
link.download = props.document.filename || props.document.name || 'document'
|
||||||
|
link.target = '_blank'
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border border-gray-200 rounded-lg p-4">
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<DocumentPreviewModal
|
||||||
|
:document="previewDocument"
|
||||||
|
:visible="previewVisible"
|
||||||
|
@close="closePreview"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -205,6 +211,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="!canPreviewDocument(document)"
|
||||||
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||||
|
@click="openPreview(document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
||||||
Télécharger
|
Télécharger
|
||||||
</button>
|
</button>
|
||||||
@@ -231,7 +246,9 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||||
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -261,6 +278,8 @@ const loadingDocuments = ref(false)
|
|||||||
const documentsLoaded = ref(!!(props.piece.documents && props.piece.documents.length))
|
const documentsLoaded = ref(!!(props.piece.documents && props.piece.documents.length))
|
||||||
const pieceDocuments = computed(() => props.piece.documents || [])
|
const pieceDocuments = computed(() => props.piece.documents || [])
|
||||||
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||||
|
const previewDocument = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
const handleConstructeurChange = (value) => {
|
const handleConstructeurChange = (value) => {
|
||||||
props.piece.constructeurId = value
|
props.piece.constructeurId = value
|
||||||
@@ -328,6 +347,17 @@ const downloadDocument = (doc) => {
|
|||||||
window.open(doc.path, '_blank')
|
window.open(doc.path, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (doc) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const formatSize = (size) => {
|
const formatSize = (size) => {
|
||||||
if (size === undefined || size === null) return '—'
|
if (size === undefined || size === null) return '—'
|
||||||
if (size === 0) return '0 B'
|
if (size === 0) return '0 B'
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DocumentPreviewModal
|
||||||
|
:document="previewDocument"
|
||||||
|
:visible="previewVisible"
|
||||||
|
@close="closePreview"
|
||||||
|
/>
|
||||||
|
|
||||||
<section class="card bg-base-100 shadow-lg">
|
<section class="card bg-base-100 shadow-lg">
|
||||||
<div class="card-body space-y-6">
|
<div class="card-body space-y-6">
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
<div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
@@ -94,7 +100,16 @@
|
|||||||
<td>{{ formatDate(document.createdAt) }}</td>
|
<td>{{ formatDate(document.createdAt) }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
<button
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canPreviewDocument(document)"
|
||||||
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||||
|
@click="openPreview(document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-xs" type="button" @click="downloadDocument(document)">
|
||||||
Télécharger
|
Télécharger
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,11 +127,15 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
|
|
||||||
const { documents, loading, loadDocuments } = useDocuments()
|
const { documents, loading, loadDocuments } = useDocuments()
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const attachmentFilter = ref('all')
|
const attachmentFilter = ref('all')
|
||||||
|
const previewDocument = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDocuments()
|
loadDocuments()
|
||||||
@@ -187,4 +206,15 @@ const downloadDocument = (doc) => {
|
|||||||
|
|
||||||
window.open(doc.path, '_blank')
|
window.open(doc.path, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (doc) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
|
|
||||||
<!-- Machine Details -->
|
<!-- Machine Details -->
|
||||||
<div v-else-if="machine" class="space-y-8">
|
<div v-else-if="machine" class="space-y-8">
|
||||||
|
<DocumentPreviewModal
|
||||||
|
:document="previewDocument"
|
||||||
|
:visible="previewVisible"
|
||||||
|
@close="closePreview"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Header with Edit Button -->
|
<!-- Header with Edit Button -->
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
||||||
@@ -259,6 +265,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="!canPreviewDocument(document)"
|
||||||
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||||
|
@click="openPreview(document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
||||||
Télécharger
|
Télécharger
|
||||||
</button>
|
</button>
|
||||||
@@ -365,10 +380,12 @@ import { useApi } from '~/composables/useApi'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
||||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const machineId = route.params.id
|
const machineId = route.params.id
|
||||||
@@ -423,6 +440,8 @@ const machineCustomFieldValues = reactive({})
|
|||||||
const machineDocumentFiles = ref([])
|
const machineDocumentFiles = ref([])
|
||||||
const machineDocumentsUploading = ref(false)
|
const machineDocumentsUploading = ref(false)
|
||||||
const machineDocumentsLoaded = ref(false)
|
const machineDocumentsLoaded = ref(false)
|
||||||
|
const previewDocument = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
const handleMachineConstructeurChange = async (value) => {
|
const handleMachineConstructeurChange = async (value) => {
|
||||||
machineConstructeurId.value = value
|
machineConstructeurId.value = value
|
||||||
@@ -537,6 +556,17 @@ const downloadDocument = (doc) => {
|
|||||||
window.open(doc.path, '_blank')
|
window.open(doc.path, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (doc) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const formatSize = (size) => {
|
const formatSize = (size) => {
|
||||||
if (size === undefined || size === null) return '—'
|
if (size === undefined || size === null) return '—'
|
||||||
if (size === 0) return '0 B'
|
if (size === 0) return '0 B'
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="container mx-auto px-6 py-8">
|
<main class="container mx-auto px-6 py-8">
|
||||||
|
<DocumentPreviewModal
|
||||||
|
:document="previewDocument"
|
||||||
|
:visible="previewVisible"
|
||||||
|
@close="closePreview"
|
||||||
|
/>
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero min-h-[30vh] bg-gradient-to-r from-primary to-secondary">
|
<div class="hero min-h-[30vh] bg-gradient-to-r from-primary to-secondary">
|
||||||
<div class="hero-content text-center text-neutral-content">
|
<div class="hero-content text-center text-neutral-content">
|
||||||
@@ -328,6 +333,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="!canPreviewDocument(document)"
|
||||||
|
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
||||||
|
@click="openPreview(document)"
|
||||||
|
>
|
||||||
|
Consulter
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
<button type="button" class="btn btn-ghost btn-xs" @click="downloadDocument(document)">
|
||||||
Télécharger
|
Télécharger
|
||||||
</button>
|
</button>
|
||||||
@@ -365,7 +379,9 @@ import { useSites } from '~/composables/useSites'
|
|||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
|
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||||
|
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||||
|
|
||||||
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
|
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
|
||||||
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
|
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
|
||||||
@@ -396,6 +412,8 @@ const editSiteForm = reactive({
|
|||||||
|
|
||||||
const selectedFiles = ref([])
|
const selectedFiles = ref([])
|
||||||
const uploadingDocuments = ref(false)
|
const uploadingDocuments = ref(false)
|
||||||
|
const previewDocument = ref(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
|
||||||
const siteDocuments = computed(() => siteBeingEdited.value?.documents || [])
|
const siteDocuments = computed(() => siteBeingEdited.value?.documents || [])
|
||||||
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||||
@@ -543,6 +561,17 @@ const downloadDocument = (doc) => {
|
|||||||
window.open(doc.path, '_blank')
|
window.open(doc.path, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openPreview = (doc) => {
|
||||||
|
if (!canPreviewDocument(doc)) return
|
||||||
|
previewDocument.value = doc
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePreview = () => {
|
||||||
|
previewVisible.value = false
|
||||||
|
previewDocument.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const refreshSiteDocuments = async (siteId) => {
|
const refreshSiteDocuments = async (siteId) => {
|
||||||
if (!siteId) return
|
if (!siteId) return
|
||||||
const result = await loadDocumentsBySite(siteId, { updateStore: false })
|
const result = await loadDocumentsBySite(siteId, { updateStore: false })
|
||||||
|
|||||||
25
app/utils/documentPreview.js
Normal file
25
app/utils/documentPreview.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { getFileIcon } from './fileIcons'
|
||||||
|
|
||||||
|
export const getPreviewType = (document) => {
|
||||||
|
if (!document) return null
|
||||||
|
const mime = (document.mimeType || '').toLowerCase()
|
||||||
|
const path = document.path || ''
|
||||||
|
|
||||||
|
const check = (prefix) => mime.startsWith(prefix) || path.startsWith(`data:${prefix}`)
|
||||||
|
|
||||||
|
if (check('image/')) return 'image'
|
||||||
|
if (mime === 'application/pdf' || path.startsWith('data:application/pdf')) return 'pdf'
|
||||||
|
if (check('audio/')) return 'audio'
|
||||||
|
if (check('video/')) return 'video'
|
||||||
|
if (check('text/') || mime.includes('json') || mime.includes('xml') || path.startsWith('data:application/json')) return 'text'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canPreviewDocument = (document = {}) => !!getPreviewType(document)
|
||||||
|
|
||||||
|
export const describeDocument = (document) => {
|
||||||
|
if (!document) return ''
|
||||||
|
const name = document.filename || document.name || ''
|
||||||
|
const icon = getFileIcon({ name, mime: document.mimeType })
|
||||||
|
return icon.label
|
||||||
|
}
|
||||||
290
deploy.sh
Normal file
290
deploy.sh
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script de déploiement automatique pour Inventory V2
|
||||||
|
# Usage: ./deploy.sh [backend|frontend|all]
|
||||||
|
|
||||||
|
set -e # Arrêter en cas d'erreur
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROJECT_DIR="/home/matt/inventory_v2"
|
||||||
|
BACKEND_DIR="$PROJECT_DIR/inventory_backend"
|
||||||
|
FRONTEND_DIR="$PROJECT_DIR/inventory_frontend"
|
||||||
|
LOG_FILE="/tmp/deploy.log"
|
||||||
|
|
||||||
|
# Couleurs pour les logs
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Fonction de logging
|
||||||
|
log() {
|
||||||
|
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour déployer le backend
|
||||||
|
deploy_backend() {
|
||||||
|
log "Déploiement du backend..."
|
||||||
|
|
||||||
|
# Sauvegarde automatique de la base de données avant modification
|
||||||
|
log "Sauvegarde automatique de la base de données..."
|
||||||
|
if command -v pg_dump &> /dev/null; then
|
||||||
|
BACKUP_DIR="/home/matt/backups"
|
||||||
|
DATE=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
BACKUP_FILE="auto_backup_$DATE.sql"
|
||||||
|
|
||||||
|
# Créer le répertoire de sauvegarde
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Sauvegarde de la base de données
|
||||||
|
if pg_dump -h localhost -U inventory_user -d inventory_db > "$BACKUP_DIR/$BACKUP_FILE" 2>/dev/null; then
|
||||||
|
# Compresser la sauvegarde
|
||||||
|
gzip "$BACKUP_DIR/$BACKUP_FILE"
|
||||||
|
log_success "Sauvegarde automatique créée: $BACKUP_DIR/$BACKUP_FILE.gz"
|
||||||
|
|
||||||
|
# Garder seulement les 10 dernières sauvegardes automatiques
|
||||||
|
cd "$BACKUP_DIR"
|
||||||
|
ls -t auto_backup_*.sql.gz | tail -n +11 | xargs -r rm
|
||||||
|
else
|
||||||
|
log_warning "Impossible de créer la sauvegarde automatique (base de données non accessible)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "pg_dump non trouvé, sauvegarde automatique impossible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$BACKEND_DIR"
|
||||||
|
|
||||||
|
# Sauvegarder les modifications locales si nécessaire
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
log_warning "Modifications locales détectées, sauvegarde..."
|
||||||
|
git stash push -m "Auto-sauvegarde avant déploiement $(date)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull les dernières modifications
|
||||||
|
log "Récupération des dernières modifications..."
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Installer les dépendances si nécessaire
|
||||||
|
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
|
||||||
|
log "Installation des dépendances..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build du projet
|
||||||
|
log "Compilation du projet..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Appliquer les migrations Prisma si nécessaire
|
||||||
|
log "Vérification et application des migrations Prisma..."
|
||||||
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
# Générer le client Prisma
|
||||||
|
log "Génération du client Prisma..."
|
||||||
|
npx prisma generate
|
||||||
|
|
||||||
|
# Redémarrer le service backend
|
||||||
|
log "Redémarrage du service backend..."
|
||||||
|
if systemctl is-active --quiet inventory-backend; then
|
||||||
|
sudo systemctl restart inventory-backend
|
||||||
|
log_success "Service backend redémarré"
|
||||||
|
else
|
||||||
|
log_warning "Service backend non trouvé, démarrage manuel requis"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Backend déployé avec succès"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour déployer le frontend
|
||||||
|
deploy_frontend() {
|
||||||
|
log "Déploiement du frontend..."
|
||||||
|
|
||||||
|
cd "$FRONTEND_DIR"
|
||||||
|
|
||||||
|
# Sauvegarder les modifications locales si nécessaire
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
log_warning "Modifications locales détectées, sauvegarde..."
|
||||||
|
git stash push -m "Auto-sauvegarde avant déploiement $(date)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull les dernières modifications
|
||||||
|
log "Récupération des dernières modifications..."
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Installer les dépendances si nécessaire
|
||||||
|
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
|
||||||
|
log "Installation des dépendances..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build du projet
|
||||||
|
log "Compilation du projet..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Redémarrer le service frontend
|
||||||
|
log "Redémarrage du service frontend..."
|
||||||
|
if systemctl is-active --quiet inventory-frontend; then
|
||||||
|
sudo systemctl restart inventory-frontend
|
||||||
|
log_success "Service frontend redémarré"
|
||||||
|
else
|
||||||
|
log_warning "Service frontend non trouvé, démarrage manuel requis"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Frontend déployé avec succès"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour déployer tout
|
||||||
|
deploy_all() {
|
||||||
|
log "Déploiement complet..."
|
||||||
|
deploy_backend
|
||||||
|
deploy_frontend
|
||||||
|
log_success "Déploiement complet terminé"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour restaurer la base de données
|
||||||
|
restore_database() {
|
||||||
|
log "Restauration de la base de données..."
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
log_error "Veuillez spécifier le fichier de sauvegarde à restaurer"
|
||||||
|
echo "Usage: $0 restore <fichier_sauvegarde>"
|
||||||
|
echo "Exemple: $0 restore /home/matt/backups/auto_backup_20241230_143022.sql.gz"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKUP_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
log_error "Fichier de sauvegarde non trouvé: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier si c'est un fichier compressé
|
||||||
|
if [[ "$BACKUP_FILE" == *.gz ]]; then
|
||||||
|
log "Décompression de la sauvegarde..."
|
||||||
|
gunzip -c "$BACKUP_FILE" | psql -h localhost -U inventory_user -d inventory_db
|
||||||
|
else
|
||||||
|
psql -h localhost -U inventory_user -d inventory_db < "$BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log_success "Base de données restaurée avec succès"
|
||||||
|
else
|
||||||
|
log_error "Erreur lors de la restauration"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour lister les sauvegardes disponibles
|
||||||
|
list_backups() {
|
||||||
|
log "Sauvegardes disponibles:"
|
||||||
|
BACKUP_DIR="/home/matt/backups"
|
||||||
|
|
||||||
|
if [ -d "$BACKUP_DIR" ]; then
|
||||||
|
ls -la "$BACKUP_DIR"/*.sql.gz 2>/dev/null | while read -r line; do
|
||||||
|
echo " $line"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
log_warning "Aucune sauvegarde trouvée"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher l'aide
|
||||||
|
show_help() {
|
||||||
|
echo "Script de déploiement automatique pour Inventory V2"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 [OPTION]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " backend Déployer uniquement le backend"
|
||||||
|
echo " frontend Déployer uniquement le frontend"
|
||||||
|
echo " all Déployer backend et frontend (défaut)"
|
||||||
|
echo " restore Restaurer la base de données"
|
||||||
|
echo " backups Lister les sauvegardes disponibles"
|
||||||
|
echo " help Afficher cette aide"
|
||||||
|
echo ""
|
||||||
|
echo "Exemples:"
|
||||||
|
echo " $0 backend"
|
||||||
|
echo " $0 frontend"
|
||||||
|
echo " $0 all"
|
||||||
|
echo " $0 restore /home/matt/backups/auto_backup_20241230_143022.sql.gz"
|
||||||
|
echo " $0 backups"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérification des prérequis
|
||||||
|
check_prerequisites() {
|
||||||
|
log "Vérification des prérequis..."
|
||||||
|
|
||||||
|
# Vérifier que git est installé
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
log_error "Git n'est pas installé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier que npm est installé
|
||||||
|
if ! command -v npm &> /dev/null; then
|
||||||
|
log_error "NPM n'est pas installé"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier que le répertoire du projet existe
|
||||||
|
if [ ! -d "$PROJECT_DIR" ]; then
|
||||||
|
log_error "Le répertoire du projet n'existe pas: $PROJECT_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Prérequis vérifiés"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction principale
|
||||||
|
main() {
|
||||||
|
log "=== Début du déploiement ==="
|
||||||
|
|
||||||
|
# Vérifier les prérequis
|
||||||
|
check_prerequisites
|
||||||
|
|
||||||
|
# Traitement des arguments
|
||||||
|
case "${1:-all}" in
|
||||||
|
"backend")
|
||||||
|
deploy_backend
|
||||||
|
;;
|
||||||
|
"frontend")
|
||||||
|
deploy_frontend
|
||||||
|
;;
|
||||||
|
"all")
|
||||||
|
deploy_all
|
||||||
|
;;
|
||||||
|
"restore")
|
||||||
|
restore_database "$2"
|
||||||
|
;;
|
||||||
|
"backups")
|
||||||
|
list_backups
|
||||||
|
;;
|
||||||
|
"help"|"-h"|"--help")
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Option invalide: $1"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "=== Déploiement terminé ==="
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exécution du script
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user