feat: add document preview overlay
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
@close="closePreview"
|
||||
/>
|
||||
|
||||
<!-- Component Header -->
|
||||
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
||||
<div class="flex items-start gap-3 w-full">
|
||||
@@ -199,6 +205,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<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)">
|
||||
Télécharger
|
||||
</button>
|
||||
@@ -260,6 +275,8 @@ import DocumentUpload from './DocumentUpload.vue'
|
||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -289,6 +306,8 @@ const loadingDocuments = ref(false)
|
||||
const documentsLoaded = ref(!!(props.component.documents && props.component.documents.length))
|
||||
const componentDocuments = computed(() => props.component.documents || [])
|
||||
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||
const previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const handleConstructeurChange = async (value) => {
|
||||
props.component.constructeurId = value
|
||||
@@ -409,6 +428,17 @@ const downloadDocument = (doc) => {
|
||||
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) => {
|
||||
if (size === undefined || size === null) return '—'
|
||||
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>
|
||||
<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 gap-2">
|
||||
<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 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)">
|
||||
Télécharger
|
||||
</button>
|
||||
@@ -231,7 +246,9 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import ConstructeurSelect from './ConstructeurSelect.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -261,6 +278,8 @@ 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 previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const handleConstructeurChange = (value) => {
|
||||
props.piece.constructeurId = value
|
||||
@@ -328,6 +347,17 @@ const downloadDocument = (doc) => {
|
||||
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) => {
|
||||
if (size === undefined || size === null) return '—'
|
||||
if (size === 0) return '0 B'
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
@close="closePreview"
|
||||
/>
|
||||
|
||||
<section class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-6">
|
||||
<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 class="text-right">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -112,11 +127,15 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
|
||||
const { documents, loading, loadDocuments } = useDocuments()
|
||||
|
||||
const searchTerm = ref('')
|
||||
const attachmentFilter = ref('all')
|
||||
const previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
loadDocuments()
|
||||
@@ -187,4 +206,15 @@ const downloadDocument = (doc) => {
|
||||
|
||||
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>
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
|
||||
<!-- Machine Details -->
|
||||
<div v-else-if="machine" class="space-y-8">
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
@close="closePreview"
|
||||
/>
|
||||
|
||||
<!-- Header with Edit Button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
||||
@@ -259,6 +265,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<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)">
|
||||
Télécharger
|
||||
</button>
|
||||
@@ -365,10 +380,12 @@ import { useApi } from '~/composables/useApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import ComponentHierarchy from '~/components/ComponentHierarchy.vue'
|
||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const machineId = route.params.id
|
||||
@@ -423,6 +440,8 @@ const machineCustomFieldValues = reactive({})
|
||||
const machineDocumentFiles = ref([])
|
||||
const machineDocumentsUploading = ref(false)
|
||||
const machineDocumentsLoaded = ref(false)
|
||||
const previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const handleMachineConstructeurChange = async (value) => {
|
||||
machineConstructeurId.value = value
|
||||
@@ -537,6 +556,17 @@ const downloadDocument = (doc) => {
|
||||
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) => {
|
||||
if (size === undefined || size === null) return '—'
|
||||
if (size === 0) return '0 B'
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
@close="closePreview"
|
||||
/>
|
||||
<!-- Hero Section -->
|
||||
<div class="hero min-h-[30vh] bg-gradient-to-r from-primary to-secondary">
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
@@ -328,6 +333,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<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)">
|
||||
Télécharger
|
||||
</button>
|
||||
@@ -365,7 +379,9 @@ import { useSites } from '~/composables/useSites'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
|
||||
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
|
||||
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
|
||||
@@ -396,6 +412,8 @@ const editSiteForm = reactive({
|
||||
|
||||
const selectedFiles = ref([])
|
||||
const uploadingDocuments = ref(false)
|
||||
const previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const siteDocuments = computed(() => siteBeingEdited.value?.documents || [])
|
||||
const documentIcon = (doc) => getFileIcon({ name: doc.filename || doc.name, mime: doc.mimeType })
|
||||
@@ -543,6 +561,17 @@ const downloadDocument = (doc) => {
|
||||
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) => {
|
||||
if (!siteId) return
|
||||
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