619 lines
21 KiB
Vue
619 lines
21 KiB
Vue
<template>
|
|
<main class="container mx-auto px-6 py-8">
|
|
<DocumentPreviewModal
|
|
:document="previewDocument"
|
|
:visible="previewVisible"
|
|
@close="closePreview"
|
|
/>
|
|
|
|
<!-- Sites Management -->
|
|
<div class="my-8">
|
|
<!-- Header with Add Button -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold">Sites</h2>
|
|
<button @click="showAddSiteModal = true" class="btn btn-primary">
|
|
<IconLucidePlus class="w-5 h-5 mr-2" aria-hidden="true" />
|
|
Ajouter un site
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Sites Grid -->
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
|
|
<div v-else-if="sites.length === 0" class="text-center py-12">
|
|
<div class="max-w-md mx-auto">
|
|
<IconLucideMapPin class="w-16 h-16 mx-auto text-gray-400 mb-4" aria-hidden="true" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun site trouvé</h3>
|
|
<p class="text-gray-500 mb-4">Commencez par ajouter votre premier site.</p>
|
|
<button @click="showAddSiteModal = true" class="btn btn-primary">
|
|
Ajouter un site
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<div
|
|
v-for="site in sites"
|
|
:key="site.id"
|
|
class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow"
|
|
>
|
|
<div class="card-body">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="card-title text-lg">{{ site.name }}</h3>
|
|
<div class="badge badge-primary badge-sm">{{ site.machines?.length || 0 }} machines</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex items-center gap-2 text-gray-700">
|
|
<IconLucideUser class="w-4 h-4 text-primary" aria-hidden="true" />
|
|
<span class="font-medium">{{ site.contactName }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600">
|
|
<IconLucidePhone class="w-4 h-4 text-secondary" aria-hidden="true" />
|
|
<span>{{ site.contactPhone }}</span>
|
|
</div>
|
|
|
|
<div class="flex items-start gap-2 text-gray-600">
|
|
<IconLucideMapPin class="w-4 h-4 text-accent mt-1" aria-hidden="true" />
|
|
<span>
|
|
{{ site.contactAddress }}<br />
|
|
{{ site.contactPostalCode }} {{ site.contactCity }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 text-gray-600">
|
|
<IconLucideFactory class="w-4 h-4 text-blue-500" aria-hidden="true" />
|
|
<span>{{ site.machines?.length || 0 }} machine(s)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions justify-end mt-4">
|
|
<button class="btn btn-sm btn-outline" @click="editSite(site)">
|
|
Modifier
|
|
</button>
|
|
<button class="btn btn-sm btn-error" @click="confirmDeleteSite(site)">
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Site Modal -->
|
|
<div v-if="showAddSiteModal" class="modal modal-open">
|
|
<div class="modal-box max-w-md">
|
|
<h3 class="font-bold text-lg mb-4">Ajouter un nouveau site</h3>
|
|
<form @submit.prevent="handleCreateSite" class="space-y-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Nom du site</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.name"
|
|
type="text"
|
|
placeholder="Ex: Usine principale"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Nom du contact</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.contactName"
|
|
type="text"
|
|
placeholder="Nom et prénom"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Téléphone</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.contactPhone"
|
|
type="tel"
|
|
placeholder="Ex: 06 00 00 00 00"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Adresse</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.contactAddress"
|
|
type="text"
|
|
placeholder="Adresse complète"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Code postal</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.contactPostalCode"
|
|
type="text"
|
|
placeholder="Code postal"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Ville</span>
|
|
</label>
|
|
<input
|
|
v-model="newSite.contactCity"
|
|
type="text"
|
|
placeholder="Ville"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" @click="showAddSiteModal = false">
|
|
Annuler
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
Créer le site
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Site Modal -->
|
|
<div v-if="showEditSiteModal" class="modal modal-open">
|
|
<div class="modal-box max-w-md">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
Modifier le site
|
|
<span v-if="siteBeingEdited" class="block text-sm font-normal text-gray-500">{{ siteBeingEdited.name }}</span>
|
|
</h3>
|
|
<form @submit.prevent="handleUpdateSite" class="space-y-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Nom du site</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.name"
|
|
type="text"
|
|
placeholder="Nom du site"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Nom du contact</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.contactName"
|
|
type="text"
|
|
placeholder="Nom et prénom"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Téléphone</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.contactPhone"
|
|
type="tel"
|
|
placeholder="Ex: 06 00 00 00 00"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Adresse</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.contactAddress"
|
|
type="text"
|
|
placeholder="Adresse complète"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Code postal</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.contactPostalCode"
|
|
type="text"
|
|
placeholder="Code postal"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Ville</span>
|
|
</label>
|
|
<input
|
|
v-model="editSiteForm.contactCity"
|
|
type="text"
|
|
placeholder="Ville"
|
|
class="input input-bordered"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-t border-base-200 pt-4 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h4 class="font-semibold text-sm">Documents liés</h4>
|
|
<p class="text-xs text-gray-500">Ajoutez des documents (PDF, images...) relatifs à ce site.</p>
|
|
</div>
|
|
<span v-if="selectedFiles.length" class="badge badge-outline">
|
|
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} prêt{{ selectedFiles.length > 1 ? 's' : '' }} à être ajouté
|
|
</span>
|
|
</div>
|
|
|
|
<DocumentUpload
|
|
v-model="selectedFiles"
|
|
title="Déposer vos fichiers"
|
|
subtitle="Formats courants acceptés : PDF, JPG, PNG, DOCX..."
|
|
/>
|
|
|
|
<div v-if="siteDocuments.length" class="space-y-3">
|
|
<h5 class="text-sm font-medium">Documents existants</h5>
|
|
<div class="space-y-2 max-h-48 overflow-y-auto pr-1">
|
|
<div
|
|
v-for="document in siteDocuments"
|
|
:key="document.id"
|
|
class="flex items-center justify-between rounded border border-base-200 bg-base-100 px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-3 text-sm">
|
|
<span class="text-xl" :class="documentIcon(document).colorClass">
|
|
<component
|
|
:is="documentIcon(document).component"
|
|
class="h-6 w-6"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
<div>
|
|
<div class="font-medium">{{ document.name }}</div>
|
|
<div class="text-xs text-gray-500">
|
|
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-xs"
|
|
: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>
|
|
<button
|
|
type="button"
|
|
class="btn btn-error btn-xs"
|
|
@click="handleRemoveSiteDocument(document.id)"
|
|
>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" @click="closeEditModal">
|
|
Annuler
|
|
</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="uploadingDocuments">
|
|
<span v-if="uploadingDocuments" class="loading loading-spinner loading-xs mr-2"></span>
|
|
Enregistrer
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|
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'
|
|
import IconLucidePlus from '~icons/lucide/plus'
|
|
import IconLucideMapPin from '~icons/lucide/map-pin'
|
|
import IconLucideUser from '~icons/lucide/user'
|
|
import IconLucidePhone from '~icons/lucide/phone'
|
|
import IconLucideFactory from '~icons/lucide/factory'
|
|
|
|
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
|
|
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
|
|
const route = useRoute()
|
|
|
|
// Data
|
|
const showAddSiteModal = ref(false)
|
|
const showEditSiteModal = ref(false)
|
|
|
|
const siteBeingEdited = ref(null)
|
|
|
|
const newSite = reactive({
|
|
name: '',
|
|
contactName: '',
|
|
contactPhone: '',
|
|
contactAddress: '',
|
|
contactPostalCode: '',
|
|
contactCity: ''
|
|
})
|
|
|
|
const editSiteForm = reactive({
|
|
name: '',
|
|
contactName: '',
|
|
contactPhone: '',
|
|
contactAddress: '',
|
|
contactPostalCode: '',
|
|
contactCity: ''
|
|
})
|
|
|
|
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 })
|
|
|
|
// Methods
|
|
const handleCreateSite = async () => {
|
|
const result = await createSite({
|
|
name: newSite.name,
|
|
contactName: newSite.contactName,
|
|
contactPhone: newSite.contactPhone,
|
|
contactAddress: newSite.contactAddress,
|
|
contactPostalCode: newSite.contactPostalCode,
|
|
contactCity: newSite.contactCity
|
|
})
|
|
|
|
if (result.success) {
|
|
// Reset form
|
|
newSite.name = ''
|
|
newSite.contactName = ''
|
|
newSite.contactPhone = ''
|
|
newSite.contactAddress = ''
|
|
newSite.contactPostalCode = ''
|
|
newSite.contactCity = ''
|
|
showAddSiteModal.value = false
|
|
}
|
|
}
|
|
|
|
const editSite = (site) => {
|
|
siteBeingEdited.value = site
|
|
editSiteForm.name = site.name || ''
|
|
editSiteForm.contactName = site.contactName || ''
|
|
editSiteForm.contactPhone = site.contactPhone || ''
|
|
editSiteForm.contactAddress = site.contactAddress || ''
|
|
editSiteForm.contactPostalCode = site.contactPostalCode || ''
|
|
editSiteForm.contactCity = site.contactCity || ''
|
|
selectedFiles.value = []
|
|
refreshSiteDocuments(site.id)
|
|
showEditSiteModal.value = true
|
|
}
|
|
|
|
const handleUpdateSite = async () => {
|
|
if (!siteBeingEdited.value) return
|
|
|
|
const result = await updateSite(siteBeingEdited.value.id, {
|
|
name: editSiteForm.name,
|
|
contactName: editSiteForm.contactName,
|
|
contactPhone: editSiteForm.contactPhone,
|
|
contactAddress: editSiteForm.contactAddress,
|
|
contactPostalCode: editSiteForm.contactPostalCode,
|
|
contactCity: editSiteForm.contactCity
|
|
})
|
|
|
|
if (!result.success) return
|
|
|
|
let uploadedDocuments = []
|
|
if (selectedFiles.value.length) {
|
|
uploadingDocuments.value = true
|
|
const uploadResult = await uploadDocuments(
|
|
{
|
|
files: selectedFiles.value,
|
|
context: { siteId: siteBeingEdited.value.id }
|
|
},
|
|
{ updateStore: false }
|
|
)
|
|
uploadingDocuments.value = false
|
|
|
|
if (uploadResult.success) {
|
|
uploadedDocuments = uploadResult.data || []
|
|
selectedFiles.value = []
|
|
}
|
|
}
|
|
|
|
if (uploadedDocuments.length) {
|
|
if (siteBeingEdited.value) {
|
|
siteBeingEdited.value.documents = [...uploadedDocuments, ...(siteBeingEdited.value.documents || [])]
|
|
}
|
|
const index = sites.value.findIndex(site => site.id === siteBeingEdited.value?.id)
|
|
if (index !== -1) {
|
|
sites.value[index] = {
|
|
...sites.value[index],
|
|
name: editSiteForm.name,
|
|
contactName: editSiteForm.contactName,
|
|
contactPhone: editSiteForm.contactPhone,
|
|
contactAddress: editSiteForm.contactAddress,
|
|
contactPostalCode: editSiteForm.contactPostalCode,
|
|
contactCity: editSiteForm.contactCity,
|
|
documents: [...uploadedDocuments, ...(sites.value[index].documents || [])]
|
|
}
|
|
}
|
|
} else {
|
|
const index = sites.value.findIndex(site => site.id === siteBeingEdited.value?.id)
|
|
if (index !== -1) {
|
|
sites.value[index] = {
|
|
...sites.value[index],
|
|
name: editSiteForm.name,
|
|
contactName: editSiteForm.contactName,
|
|
contactPhone: editSiteForm.contactPhone,
|
|
contactAddress: editSiteForm.contactAddress,
|
|
contactPostalCode: editSiteForm.contactPostalCode,
|
|
contactCity: editSiteForm.contactCity,
|
|
}
|
|
}
|
|
}
|
|
|
|
closeEditModal()
|
|
}
|
|
|
|
const closeEditModal = () => {
|
|
showEditSiteModal.value = false
|
|
siteBeingEdited.value = null
|
|
selectedFiles.value = []
|
|
}
|
|
|
|
const handleRemoveSiteDocument = async (documentId) => {
|
|
if (!documentId) return
|
|
|
|
const result = await deleteDocument(documentId, { updateStore: false })
|
|
if (result.success) {
|
|
if (siteBeingEdited.value) {
|
|
siteBeingEdited.value.documents = (siteBeingEdited.value.documents || []).filter(doc => doc.id !== documentId)
|
|
}
|
|
|
|
const index = sites.value.findIndex(site => site.id === siteBeingEdited.value?.id)
|
|
if (index !== -1) {
|
|
const updatedDocs = (sites.value[index].documents || []).filter(doc => doc.id !== documentId)
|
|
sites.value[index] = {
|
|
...sites.value[index],
|
|
documents: updatedDocs,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const downloadDocument = (doc) => {
|
|
if (!doc?.path) return
|
|
|
|
if (doc.path.startsWith('data:')) {
|
|
const link = document.createElement('a')
|
|
link.href = doc.path
|
|
link.download = doc.filename || doc.name || 'document'
|
|
link.click()
|
|
return
|
|
}
|
|
|
|
window.open(doc.path, '_blank')
|
|
}
|
|
|
|
const 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 })
|
|
if (result.success && siteBeingEdited.value && siteBeingEdited.value.id === siteId) {
|
|
const cloned = Array.isArray(result.data) ? [...result.data] : []
|
|
siteBeingEdited.value.documents = cloned
|
|
const index = sites.value.findIndex(site => site.id === siteId)
|
|
if (index !== -1) {
|
|
sites.value[index] = {
|
|
...sites.value[index],
|
|
documents: cloned,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const formatSize = (size) => {
|
|
if (size === undefined || size === null) return '—'
|
|
if (size === 0) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB']
|
|
const index = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024)))
|
|
const formatted = size / Math.pow(1024, index)
|
|
return `${formatted.toFixed(1)} ${units[index]}`
|
|
}
|
|
|
|
const confirmDeleteSite = async (site) => {
|
|
const { showError, showSuccess } = useToast()
|
|
|
|
if (confirm(`Êtes-vous sûr de vouloir supprimer le site "${site.name}" ? Cette action est irréversible.`)) {
|
|
try {
|
|
const result = await deleteSite(site.id)
|
|
if (result.success) {
|
|
showSuccess(`Site "${site.name}" supprimé avec succès`)
|
|
} else {
|
|
showError(`Erreur lors de la suppression: ${result.error}`)
|
|
}
|
|
} catch (error) {
|
|
showError(`Erreur lors de la suppression: ${error.message}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
await loadSites()
|
|
})
|
|
|
|
watch(
|
|
() => route.query.add,
|
|
async (shouldOpen) => {
|
|
if (shouldOpen === 'true') {
|
|
showAddSiteModal.value = true
|
|
await navigateTo('/sites', { replace: true })
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
</script>
|