feat: add profile management flow
This commit is contained in:
@@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8 space-y-8">
|
||||
<div class="hero min-h-[25vh] bg-gradient-to-r from-primary to-secondary">
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
<div class="max-w-xl">
|
||||
<h1 class="mb-3 text-4xl font-bold">Gestion documentaire</h1>
|
||||
<p class="text-sm opacity-90">
|
||||
Consultez tous les documents liés à vos sites, machines et composants. Recherchez et filtrez pour retrouver rapidement l'information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Gestion documentaire"
|
||||
subtitle="Consultez tous les documents liés à vos sites, machines et composants. Recherchez et filtrez pour retrouver rapidement l'information."
|
||||
/>
|
||||
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
@@ -129,6 +123,7 @@ import { useDocuments } from '~/composables/useDocuments'
|
||||
import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { documents, loading, loadDocuments } = useDocuments()
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8 space-y-8">
|
||||
<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="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Générateur de Types</h1>
|
||||
<p class="mb-5">
|
||||
Créez rapidement un nouveau type de machine avec sa structure complète.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Générateur de Types"
|
||||
subtitle="Créez rapidement un nouveau type de machine avec sa structure complète."
|
||||
min-height="min-h-[18vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
@@ -89,6 +85,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { machineTypes, loadMachineTypes, createMachineType } = useMachineTypesApi()
|
||||
const { showError } = useToast()
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- 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">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Dashboard Inventaire</h1>
|
||||
<p class="mb-5">
|
||||
Vue hiérarchique de vos sites et machines industrielles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Dashboard Inventaire"
|
||||
subtitle="Vue hiérarchique de vos sites et machines industrielles."
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Hierarchical View -->
|
||||
<div class="my-8">
|
||||
@@ -434,6 +430,7 @@ import { useSites } from '~/composables/useSites'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { sites, loading, loadSites, createSite } = useSites()
|
||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Machine Details -->
|
||||
<div v-else-if="machine" class="space-y-8">
|
||||
<div v-else-if="machine" ref="printAreaRef" class="space-y-8">
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
@@ -14,22 +14,37 @@
|
||||
/>
|
||||
|
||||
<!-- Header with Edit Button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<h1 class="text-3xl font-bold">Détails de la machine</h1>
|
||||
<button
|
||||
@click="toggleEditMode"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-outline': isEditMode }"
|
||||
>
|
||||
<svg v-if="!isEditMode" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
||||
</button>
|
||||
<div class="flex items-center gap-2 print:hidden" data-print-hide>
|
||||
<button
|
||||
@click="toggleEditMode"
|
||||
class="btn btn-primary"
|
||||
:class="{ 'btn-outline': isEditMode }"
|
||||
>
|
||||
<svg v-if="!isEditMode" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!isEditMode"
|
||||
@click="openPrintModal"
|
||||
type="button"
|
||||
class="btn btn-outline btn-secondary"
|
||||
>
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2V9a2 2 0 00-2-2h-2V4a1 1 0 00-1-1H8a1 1 0 00-1 1v3H5a2 2 0 00-2 2v6a2 2 0 002 2h2">
|
||||
</path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17H7v5h10v-5z"></path>
|
||||
</svg>
|
||||
Imprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug info -->
|
||||
@@ -40,19 +55,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="hero bg-gradient-to-r from-primary to-secondary min-h-[20vh] rounded-lg">
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">{{ machine.name }}</h1>
|
||||
<p class="mb-5">{{ machine.description || machine.typeMachine?.description }}</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div class="badge badge-outline">{{ machine.typeMachine?.category || 'N/A' }}</div>
|
||||
<div class="badge badge-outline">{{ machine.site?.name }}</div>
|
||||
<div v-if="machine.reference" class="badge badge-outline">{{ machine.reference }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
:title="machine.name"
|
||||
:subtitle="machine.description || machine.typeMachine?.description"
|
||||
min-height="min-h-[20vh]"
|
||||
max-width="max-w-md"
|
||||
rounded
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div class="badge badge-outline">{{ machine.typeMachine?.category || 'N/A' }}</div>
|
||||
<div class="badge badge-outline">{{ machine.site?.name }}</div>
|
||||
<div v-if="machine.reference" class="badge badge-outline">{{ machine.reference }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageHero>
|
||||
|
||||
<!-- Machine Info Card -->
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
@@ -367,10 +382,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<MachinePrintSelectionModal
|
||||
:open="printModalOpen"
|
||||
:selection="printSelection"
|
||||
:components="components"
|
||||
:pieces="machinePieces"
|
||||
@close="closePrintModal"
|
||||
@confirm="handlePrintConfirm"
|
||||
@select-all="setAllPrintSelection(true)"
|
||||
@deselect-all="setAllPrintSelection(false)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useComposants } from '~/composables/useComposants'
|
||||
@@ -386,6 +412,9 @@ import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
import MachinePrintSelectionModal from '~/components/MachinePrintSelectionModal.vue'
|
||||
import { buildMachinePrintContext, buildMachinePrintHtml } from '~/utils/printTemplates/machineReport'
|
||||
|
||||
const route = useRoute()
|
||||
const machineId = route.params.id
|
||||
@@ -420,6 +449,7 @@ const loading = ref(true)
|
||||
const machine = ref(null)
|
||||
const components = ref([])
|
||||
const pieces = ref([])
|
||||
const printAreaRef = ref(null)
|
||||
|
||||
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
||||
|
||||
@@ -442,6 +472,16 @@ const machineDocumentsUploading = ref(false)
|
||||
const machineDocumentsLoaded = ref(false)
|
||||
const previewDocument = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
const printModalOpen = ref(false)
|
||||
const printSelection = reactive({
|
||||
machine: {
|
||||
info: true,
|
||||
customFields: true,
|
||||
documents: true,
|
||||
},
|
||||
components: {},
|
||||
pieces: {},
|
||||
})
|
||||
|
||||
const handleMachineConstructeurChange = async (value) => {
|
||||
machineConstructeurId.value = value
|
||||
@@ -567,6 +607,114 @@ const closePreview = () => {
|
||||
previewDocument.value = null
|
||||
}
|
||||
|
||||
const ensurePrintSelectionEntries = () => {
|
||||
printSelection.machine.info ??= true
|
||||
printSelection.machine.customFields ??= true
|
||||
printSelection.machine.documents ??= true
|
||||
|
||||
const ensureComponent = (component) => {
|
||||
if (component?.id !== undefined && printSelection.components[component.id] === undefined) {
|
||||
printSelection.components[component.id] = true
|
||||
}
|
||||
;(component.pieces || []).forEach((piece) => {
|
||||
if (piece?.id !== undefined && printSelection.pieces[piece.id] === undefined) {
|
||||
printSelection.pieces[piece.id] = true
|
||||
}
|
||||
})
|
||||
;(component.subComponents || []).forEach(ensureComponent)
|
||||
}
|
||||
|
||||
components.value.forEach(ensureComponent)
|
||||
machinePieces.value.forEach((piece) => {
|
||||
if (piece?.id !== undefined && printSelection.pieces[piece.id] === undefined) {
|
||||
printSelection.pieces[piece.id] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setAllPrintSelection = (value) => {
|
||||
ensurePrintSelectionEntries()
|
||||
printSelection.machine.info = value
|
||||
printSelection.machine.customFields = value
|
||||
printSelection.machine.documents = value
|
||||
Object.keys(printSelection.components).forEach((key) => {
|
||||
printSelection.components[key] = value
|
||||
})
|
||||
Object.keys(printSelection.pieces).forEach((key) => {
|
||||
printSelection.pieces[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
const openPrintModal = () => {
|
||||
ensurePrintSelectionEntries()
|
||||
printModalOpen.value = true
|
||||
}
|
||||
|
||||
const closePrintModal = () => {
|
||||
printModalOpen.value = false
|
||||
}
|
||||
|
||||
const handlePrintConfirm = async () => {
|
||||
closePrintModal()
|
||||
await nextTick()
|
||||
printMachine(printSelection)
|
||||
}
|
||||
|
||||
|
||||
const printMachine = (currentSelection = printSelection) => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const context = buildMachinePrintContext({
|
||||
machine: machine.value,
|
||||
machineName: machineName.value,
|
||||
machineReference: machineReference.value,
|
||||
machineEmplacement: machineEmplacement.value,
|
||||
machinePieces: machinePieces.value,
|
||||
components: components.value,
|
||||
selection: currentSelection,
|
||||
})
|
||||
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style'))
|
||||
.map(node => node.outerHTML)
|
||||
.join('')
|
||||
|
||||
const htmlContent = buildMachinePrintHtml(context, styles)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.position = 'fixed'
|
||||
iframe.style.right = '0'
|
||||
iframe.style.bottom = '0'
|
||||
iframe.style.width = '0'
|
||||
iframe.style.height = '0'
|
||||
iframe.style.border = '0'
|
||||
iframe.setAttribute('title', 'print-frame')
|
||||
document.body.appendChild(iframe)
|
||||
|
||||
const iframeWindow = iframe.contentWindow
|
||||
const iframeDocument = iframe.contentDocument || iframeWindow?.document
|
||||
if (!iframeDocument || !iframeWindow) {
|
||||
iframe.remove()
|
||||
return
|
||||
}
|
||||
|
||||
iframeDocument.open()
|
||||
iframeDocument.write(htmlContent)
|
||||
iframeDocument.close()
|
||||
|
||||
const triggerPrint = () => {
|
||||
iframeWindow.focus()
|
||||
iframeWindow.print()
|
||||
setTimeout(() => {
|
||||
iframe.remove()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (iframeDocument.readyState === 'complete') {
|
||||
setTimeout(triggerPrint, 50)
|
||||
} else {
|
||||
iframe.onload = () => setTimeout(triggerPrint, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const formatSize = (size) => {
|
||||
if (size === undefined || size === null) return '—'
|
||||
if (size === 0) return '0 B'
|
||||
@@ -868,6 +1016,14 @@ const toggleEditMode = () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [components.value.length, machinePieces.value.length],
|
||||
() => {
|
||||
ensurePrintSelectionEntries()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadMachineData()
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- 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">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Gestion des Machines</h1>
|
||||
<p class="mb-5">
|
||||
Gérez vos machines industrielles par site et par type.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Gestion des Machines"
|
||||
subtitle="Gérez vos machines industrielles par site et par type."
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Machines Management -->
|
||||
<div class="my-8">
|
||||
@@ -250,15 +246,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useSites } from '~/composables/useSites'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { machines, loading, loadMachines, createMachineFromType, deleteMachine } = useMachines()
|
||||
const { sites, loadSites } = useSites()
|
||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||
const route = useRoute()
|
||||
|
||||
// Data
|
||||
const showAddMachineModal = ref(false)
|
||||
@@ -367,13 +365,16 @@ onMounted(async () => {
|
||||
loadSites(),
|
||||
loadMachineTypes()
|
||||
])
|
||||
|
||||
// Vérifier si on doit ouvrir automatiquement la modale d'ajout
|
||||
const route = useRoute()
|
||||
if (route.query.add === 'true') {
|
||||
showAddMachineModal.value = true
|
||||
// Nettoyer l'URL
|
||||
await navigateTo('/machines', { replace: true })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
watch(
|
||||
() => route.query.add,
|
||||
async (shouldOpen) => {
|
||||
if (shouldOpen === 'true') {
|
||||
showAddMachineModal.value = true
|
||||
await navigateTo('/machines', { replace: true })
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
90
app/pages/profiles/index.vue
Normal file
90
app/pages/profiles/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<main class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
|
||||
<div class="w-full max-w-2xl">
|
||||
<div class="card bg-base-100 shadow-2xl">
|
||||
<div class="card-body">
|
||||
<h1 class="text-2xl font-bold mb-2">Choisir un profil</h1>
|
||||
<p class="text-sm text-base-content/70 mb-6">
|
||||
Sélectionnez votre profil pour accéder à l'application. La création et la gestion se font via le menu utilisateur.
|
||||
</p>
|
||||
|
||||
<section class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="font-semibold">Profils disponibles</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs"
|
||||
@click="refreshProfiles"
|
||||
:disabled="loadingProfiles"
|
||||
>
|
||||
<span v-if="loadingProfiles" class="loading loading-spinner loading-xs"></span>
|
||||
<span v-else>Rafraîchir</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto" v-if="profiles.length">
|
||||
<button
|
||||
v-for="profile in profiles"
|
||||
:key="profile.id"
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm w-full justify-between"
|
||||
@click="selectProfile(profile.id)"
|
||||
>
|
||||
<span>{{ profile.firstName }} {{ profile.lastName }}</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="text-sm text-base-content/60">Aucun profil enregistré.</p>
|
||||
</section>
|
||||
|
||||
<footer class="mt-6 flex justify-between items-center" v-if="activeProfile">
|
||||
<div class="text-sm text-base-content/70">
|
||||
Profil actuel :
|
||||
<span class="font-semibold">{{ activeProfile.firstName }} {{ activeProfile.lastName }}</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline btn-sm" @click="handleLogout">
|
||||
Déconnexion
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProfiles } from '#imports'
|
||||
import { useProfileSession } from '#imports'
|
||||
|
||||
const router = useRouter()
|
||||
const { profiles, loadingProfiles, fetchProfiles } = useProfiles()
|
||||
const { activeProfile, activateProfile, fetchCurrentProfile, logout } = useProfileSession()
|
||||
|
||||
const refreshProfiles = async () => {
|
||||
await fetchProfiles()
|
||||
}
|
||||
|
||||
const selectProfile = async (profileId) => {
|
||||
try {
|
||||
await activateProfile(profileId)
|
||||
await fetchProfiles()
|
||||
await router.push('/')
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sélection du profil', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
await router.push('/profiles')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchProfiles()
|
||||
await fetchCurrentProfile()
|
||||
})
|
||||
</script>
|
||||
174
app/pages/profiles/manage.vue
Normal file
174
app/pages/profiles/manage.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<main class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Gestion des profils</h1>
|
||||
<p class="text-sm text-base-content/70">Sélectionnez, créez ou supprimez des profils.</p>
|
||||
</div>
|
||||
<NuxtLink to="/" class="btn btn-ghost btn-sm">Retour</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<section class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="card-title text-lg">Profils existants</h2>
|
||||
<button type="button" class="btn btn-ghost btn-xs" @click="refresh" :disabled="loadingProfiles">
|
||||
<span v-if="loadingProfiles" class="loading loading-spinner loading-xs"></span>
|
||||
<span v-else>Rafraîchir</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="space-y-2 max-h-80 overflow-y-auto" v-if="profiles.length">
|
||||
<div
|
||||
v-for="profile in profiles"
|
||||
:key="profile.id"
|
||||
class="flex items-center justify-between rounded-lg border border-base-200 bg-base-100 px-3 py-2"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium">{{ profile.firstName }} {{ profile.lastName }}</p>
|
||||
<p class="text-xs text-base-content/60">ID : {{ profile.id }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
:class="profile.id === activeProfile?.id ? 'btn-primary' : 'btn-outline'"
|
||||
@click="select(profile.id)"
|
||||
>
|
||||
{{ profile.id === activeProfile?.id ? 'Actif' : 'Activer' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-error btn-sm"
|
||||
@click="remove(profile.id)"
|
||||
>
|
||||
<span v-if="deleting === profile.id" class="loading loading-spinner loading-xs"></span>
|
||||
<span v-else>Supprimer</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-base-content/60">Aucun profil enregistré.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-4">
|
||||
<h2 class="card-title text-lg">Créer un profil</h2>
|
||||
<form class="space-y-3" @submit.prevent="create">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Prénom</span></label>
|
||||
<input
|
||||
v-model="createForm.firstName"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
placeholder="Prénom"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Nom</span></label>
|
||||
<input
|
||||
v-model="createForm.lastName"
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
placeholder="Nom"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full" :disabled="creating">
|
||||
<span v-if="creating" class="loading loading-spinner loading-sm"></span>
|
||||
<span v-else>Créer et activer</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center justify-between bg-base-100 shadow-lg rounded-lg p-4" v-if="activeProfile">
|
||||
<div>
|
||||
<p class="text-sm text-base-content/70">Profil actif :</p>
|
||||
<p class="font-semibold text-base-content">{{ activeProfile.firstName }} {{ activeProfile.lastName }}</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline" @click="handleLogout">
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProfiles, useProfileSession } from '#imports'
|
||||
|
||||
const router = useRouter()
|
||||
const { profiles, loadingProfiles, fetchProfiles, createProfile, deleteProfile } = useProfiles()
|
||||
const { activeProfile, activateProfile, fetchCurrentProfile, logout } = useProfileSession()
|
||||
|
||||
const createForm = reactive({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
})
|
||||
|
||||
const creating = ref(false)
|
||||
const deleting = ref(null)
|
||||
|
||||
const refresh = async () => {
|
||||
await fetchProfiles()
|
||||
await fetchCurrentProfile()
|
||||
}
|
||||
|
||||
const select = async (profileId) => {
|
||||
try {
|
||||
await activateProfile(profileId)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sélection du profil', error)
|
||||
}
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
creating.value = true
|
||||
try {
|
||||
const profile = await createProfile({
|
||||
firstName: createForm.firstName,
|
||||
lastName: createForm.lastName,
|
||||
})
|
||||
createForm.firstName = ''
|
||||
createForm.lastName = ''
|
||||
await activateProfile(profile.id)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création du profil', error)
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (profileId) => {
|
||||
if (!confirm('Supprimer ce profil ?')) return
|
||||
deleting.value = profileId
|
||||
try {
|
||||
await deleteProfile(profileId)
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression du profil', error)
|
||||
} finally {
|
||||
deleting.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
await refresh()
|
||||
await router.push('/profiles')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh()
|
||||
})
|
||||
</script>
|
||||
@@ -6,16 +6,12 @@
|
||||
@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">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Gestion des Sites</h1>
|
||||
<p class="mb-5">
|
||||
Gérez vos sites industriels et leurs emplacements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Gestion des Sites"
|
||||
subtitle="Gérez vos sites industriels et leurs emplacements."
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Sites Management -->
|
||||
<div class="my-8">
|
||||
@@ -374,7 +370,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useSites } from '~/composables/useSites'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
@@ -382,9 +378,11 @@ import { getFileIcon } from '~/utils/fileIcons'
|
||||
import { canPreviewDocument } from '~/utils/documentPreview'
|
||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { sites, loading, loadSites, createSite, updateSite, deleteSite } = useSites()
|
||||
const { uploadDocuments, deleteDocument, loadDocumentsBySite } = useDocuments()
|
||||
const route = useRoute()
|
||||
|
||||
// Data
|
||||
const showAddSiteModal = ref(false)
|
||||
@@ -617,13 +615,16 @@ const confirmDeleteSite = async (site) => {
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await loadSites()
|
||||
|
||||
// Vérifier si on doit ouvrir automatiquement la modale d'ajout
|
||||
const route = useRoute()
|
||||
if (route.query.add === 'true') {
|
||||
showAddSiteModal.value = true
|
||||
// Nettoyer l'URL
|
||||
await navigateTo('/sites', { replace: true })
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.add,
|
||||
async (shouldOpen) => {
|
||||
if (shouldOpen === 'true') {
|
||||
showAddSiteModal.value = true
|
||||
await navigateTo('/sites', { replace: true })
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero min-h-[30vh] bg-gradient-to-r from-secondary to-primary">
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Modifier le Type</h1>
|
||||
<p class="mb-5">
|
||||
Ajoutez des éléments au type de machine existant.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Modifier le Type"
|
||||
subtitle="Ajoutez des éléments au type de machine existant."
|
||||
gradient-from="from-secondary"
|
||||
gradient-to="to-primary"
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="my-8 text-center">
|
||||
@@ -122,6 +120,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { getMachineTypeById } = useMachineTypesApi()
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero min-h-[30vh] bg-gradient-to-r from-secondary to-primary">
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Modifier le Type</h1>
|
||||
<p class="mb-5">
|
||||
Modifiez les informations du type de machine.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Modifier le Type"
|
||||
subtitle="Modifiez les informations du type de machine."
|
||||
gradient-from="from-secondary"
|
||||
gradient-to="to-primary"
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="my-8 text-center">
|
||||
@@ -62,6 +60,7 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -218,4 +217,4 @@ onMounted(async () => {
|
||||
console.log('Loading finished, loading.value:', loading.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- 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">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-4xl font-bold">Types de Machines</h1>
|
||||
<p class="mb-5">
|
||||
Consultez et gérez les différents types de machines avec leur structure hiérarchique.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageHero
|
||||
title="Types de Machines"
|
||||
subtitle="Consultez et gérez les différents types de machines avec leur structure hiérarchique."
|
||||
min-height="min-h-[24vh]"
|
||||
max-width="max-w-md"
|
||||
/>
|
||||
|
||||
<!-- Machine Types List -->
|
||||
<div class="my-8">
|
||||
@@ -102,6 +98,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
|
||||
const { machineTypes, loading, loadMachineTypes, deleteMachineType } = useMachineTypesApi()
|
||||
|
||||
@@ -144,4 +141,4 @@ const confirmDeleteType = async (type) => {
|
||||
onMounted(async () => {
|
||||
await loadMachineTypes()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user