feat: add document preview overlay

This commit is contained in:
Matthieu
2025-09-17 17:12:41 +02:00
parent 8a32ef4bbc
commit 37c66ac3d6
8 changed files with 617 additions and 1 deletions

View File

@@ -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'

View 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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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'

View File

@@ -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 })

View 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
View 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 "$@"