feat: reorganize machine skeleton pages

This commit is contained in:
Matthieu
2025-09-29 15:05:54 +02:00
parent a78938a4d1
commit 43b615ac3e
8 changed files with 187 additions and 1381 deletions

View File

@@ -125,7 +125,7 @@
class="rounded-md px-2 py-1 transition-colors cursor-pointer" class="rounded-md px-2 py-1 transition-colors cursor-pointer"
:class=" :class="
isActive('/component-catalog') || isActive('/component-catalog') ||
isActive('/component-category') isActive('/component-category')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
@@ -178,8 +178,8 @@
class="rounded-md px-2 py-1 transition-colors cursor-pointer" class="rounded-md px-2 py-1 transition-colors cursor-pointer"
:class=" :class="
isActive('/sites') || isActive('/sites') ||
isActive('/documents') || isActive('/documents') ||
isActive('/constructeurs') isActive('/constructeurs')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
@@ -241,10 +241,7 @@
<IconLucideBoxes class="w-6 h-6" aria-hidden="true" /> <IconLucideBoxes class="w-6 h-6" aria-hidden="true" />
</div> </div>
</div> </div>
<NuxtLink <NuxtLink to="/" class="btn btn-ghost text-xl">
to="/"
class="btn btn-ghost text-xl"
>
Inventaire Pro Inventaire Pro
</NuxtLink> </NuxtLink>
</div> </div>
@@ -356,7 +353,7 @@
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer" class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
:class=" :class="
isActive('/component-category') || isActive('/component-category') ||
isActive('/component-catalog') isActive('/component-catalog')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
@@ -409,8 +406,8 @@
class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer" class="transition-colors px-3 py-2 rounded-md inline-flex items-center gap-1 cursor-pointer"
:class=" :class="
isActive('/sites') || isActive('/sites') ||
isActive('/documents') || isActive('/documents') ||
isActive('/constructeurs') isActive('/constructeurs')
? 'bg-primary text-primary-content font-semibold shadow-sm' ? 'bg-primary text-primary-content font-semibold shadow-sm'
: 'text-base-content hover:bg-primary/10 hover:text-primary' : 'text-base-content hover:bg-primary/10 hover:text-primary'
" "
@@ -475,40 +472,6 @@
<IconLucideSettings class="w-5 h-5" aria-hidden="true" /> <IconLucideSettings class="w-5 h-5" aria-hidden="true" />
</button> </button>
<!-- Menu Nouveau -->
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-primary">
<IconLucidePlus class="w-5 h-5 mr-2" aria-hidden="true" />
Nouveau
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<li>
<NuxtLink
to="/machines?add=true"
class="flex items-center gap-2"
>
<IconLucideCpu class="w-4 h-4" aria-hidden="true" />
Nouvelle Machine
</NuxtLink>
</li>
<li>
<NuxtLink to="/generator" class="flex items-center gap-2">
<IconLucideFilePlus class="w-4 h-4" aria-hidden="true" />
Nouveau Type
</NuxtLink>
</li>
<li>
<NuxtLink to="/sites?add=true" class="flex items-center gap-2">
<IconLucideMapPin class="w-4 h-4" aria-hidden="true" />
Nouveau Site
</NuxtLink>
</li>
</ul>
</div>
<ClientOnly> <ClientOnly>
<div v-if="activeProfile" class="dropdown dropdown-end"> <div v-if="activeProfile" class="dropdown dropdown-end">
<div <div
@@ -531,12 +494,12 @@
class="menu dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-64" class="menu dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-64"
> >
<li class="px-2 py-1 text-sm text-base-content/70"> <li class="px-2 py-1 text-sm text-base-content/70">
Connecté en tant que<br> Connecté en tant que<br />
<span class="font-semibold text-base-content">{{ <span class="font-semibold text-base-content">{{
activeProfileLabel activeProfileLabel
}}</span> }}</span>
</li> </li>
<li><hr class="my-1"></li>
<li> <li>
<NuxtLink to="/profiles/manage" class="justify-between"> <NuxtLink to="/profiles/manage" class="justify-between">
Gestion des profils Gestion des profils
@@ -586,99 +549,103 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRoute, navigateTo } from '#imports' import { useRoute, navigateTo } from "#imports";
import { useProfileSession } from '~/composables/useProfileSession' import { useProfileSession } from "~/composables/useProfileSession";
import IconLucideMenu from '~icons/lucide/menu' import IconLucideMenu from "~icons/lucide/menu";
import IconLucideSettings from '~icons/lucide/settings' import IconLucideSettings from "~icons/lucide/settings";
import IconLucideBoxes from '~icons/lucide/boxes' import IconLucideBoxes from "~icons/lucide/boxes";
import IconLucidePlus from '~icons/lucide/plus' import IconLucidePlus from "~icons/lucide/plus";
import IconLucideCpu from '~icons/lucide/cpu' import IconLucideCpu from "~icons/lucide/cpu";
import IconLucideFilePlus from '~icons/lucide/file-plus' import IconLucideFilePlus from "~icons/lucide/file-plus";
import IconLucideMapPin from '~icons/lucide/map-pin' import IconLucideMapPin from "~icons/lucide/map-pin";
import IconLucideChevronRight from '~icons/lucide/chevron-right' import IconLucideChevronRight from "~icons/lucide/chevron-right";
import IconLucideLogOut from '~icons/lucide/log-out' import IconLucideLogOut from "~icons/lucide/log-out";
// État du modal des paramètres d'affichage // État du modal des paramètres d'affichage
const displaySettingsOpen = ref(false) const displaySettingsOpen = ref(false);
const { activeProfile, ensureSession, logout } = useProfileSession() const { activeProfile, ensureSession, logout } = useProfileSession();
// Route active pour souligner l'onglet sélectionné dans la navbar // Route active pour souligner l'onglet sélectionné dans la navbar
const route = useRoute() const route = useRoute();
const isActive = (path) => { const isActive = (path) => {
if (path === '/') { if (path === "/") {
return route.path === '/' return route.path === "/";
} }
return route.path.startsWith(path) return route.path.startsWith(path);
} };
// Ouvrir les paramètres d'affichage // Ouvrir les paramètres d'affichage
const openDisplaySettings = () => { const openDisplaySettings = () => {
displaySettingsOpen.value = true displaySettingsOpen.value = true;
} };
// Fermer les paramètres d'affichage // Fermer les paramètres d'affichage
const closeDisplaySettings = () => { const closeDisplaySettings = () => {
displaySettingsOpen.value = false displaySettingsOpen.value = false;
} };
// Gérer les mises à jour des paramètres // Gérer les mises à jour des paramètres
const handleSettingsUpdate = (settings) => { const handleSettingsUpdate = (settings) => {
console.log("Paramètres d'affichage mis à jour:", settings) console.log("Paramètres d'affichage mis à jour:", settings);
} };
const handleLogout = async () => { const handleLogout = async () => {
await logout() await logout();
await navigateTo('/profiles') await navigateTo("/profiles");
} };
const openDropdown = ref(null) const openDropdown = ref(null);
let dropdownCloseTimer = null let dropdownCloseTimer = null;
const setDropdown = (name) => { const setDropdown = (name) => {
if (dropdownCloseTimer) { if (dropdownCloseTimer) {
clearTimeout(dropdownCloseTimer) clearTimeout(dropdownCloseTimer);
dropdownCloseTimer = null dropdownCloseTimer = null;
} }
if (openDropdown.value !== name) { if (openDropdown.value !== name) {
openDropdown.value = name openDropdown.value = name;
} }
} };
const scheduleDropdownClose = (name) => { const scheduleDropdownClose = (name) => {
if (dropdownCloseTimer) { if (dropdownCloseTimer) {
clearTimeout(dropdownCloseTimer) clearTimeout(dropdownCloseTimer);
} }
dropdownCloseTimer = setTimeout(() => { dropdownCloseTimer = setTimeout(() => {
if (openDropdown.value === name) { if (openDropdown.value === name) {
openDropdown.value = null openDropdown.value = null;
} }
dropdownCloseTimer = null dropdownCloseTimer = null;
}, 200) }, 200);
} };
const activeProfileLabel = computed(() => { const activeProfileLabel = computed(() => {
if (!activeProfile.value) { return 'Profil inconnu' } if (!activeProfile.value) {
return `${activeProfile.value.firstName} ${activeProfile.value.lastName}` return "Profil inconnu";
}) }
return `${activeProfile.value.firstName} ${activeProfile.value.lastName}`;
});
const activeProfileInitials = computed(() => { const activeProfileInitials = computed(() => {
if (!activeProfile.value) { return '??' } if (!activeProfile.value) {
const { firstName = '', lastName = '' } = activeProfile.value return "??";
}
const { firstName = "", lastName = "" } = activeProfile.value;
return ( return (
`${firstName.charAt(0) || ''}${lastName.charAt(0) || ''}`.toUpperCase() || `${firstName.charAt(0) || ""}${lastName.charAt(0) || ""}`.toUpperCase() ||
'??' "??"
) );
}) });
onMounted(async () => { onMounted(async () => {
await ensureSession() await ensureSession();
}) });
onUnmounted(() => { onUnmounted(() => {
if (dropdownCloseTimer) { if (dropdownCloseTimer) {
clearTimeout(dropdownCloseTimer) clearTimeout(dropdownCloseTimer);
dropdownCloseTimer = null dropdownCloseTimer = null;
} }
}) });
</script> </script>

View File

@@ -70,43 +70,29 @@
> >
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3"> <div class="flex-1 space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <div class="form-control">
<div class="form-control"> <label class="label"><span class="label-text">Famille de pièce</span></label>
<label class="label"><span class="label-text">Famille de pièce</span></label> <div>
<div> <select
<input v-model="piece.typePieceId"
:list="`component-piece-type-options-${index}`" class="select select-bordered select-xs"
v-model="piece.typePieceLabel" @change="handlePieceTypeSelect(piece)"
type="search" >
autocomplete="off" <option value="">
class="input input-bordered input-xs" Sélectionner une famille
placeholder="Sélectionner une famille" </option>
@change="handlePieceTypeChange(piece)" <option
@blur="handlePieceTypeChange(piece)" v-for="type in availablePieceTypes"
/> :key="type.id"
<datalist :id="`component-piece-type-options-${index}`"> :value="type.id"
<option >
v-for="type in availablePieceTypes" {{ formatPieceTypeOption(type) }}
:key="type.id" </option>
:value="formatPieceTypeOption(type)" </select>
/>
</datalist>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Quantité (optionnel)</span></label>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div> </div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div> </div>
</div> </div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)"> <button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(index)">
@@ -134,7 +120,6 @@
:key="`root-sub-${index}`" :key="`root-sub-${index}`"
:node="subComponent" :node="subComponent"
:depth="0" :depth="0"
:piece-types="availablePieceTypes"
:component-types="availableComponentTypes" :component-types="availableComponentTypes"
@remove="removeSubComponent(index)" @remove="removeSubComponent(index)"
/> />
@@ -151,7 +136,7 @@ import StructureSubComponentEditor from '~/components/StructureSubComponentEdito
import { import {
defaultStructure, defaultStructure,
hydrateStructureForEditor, hydrateStructureForEditor,
normalizeStructureForSave, cloneStructure,
} from '~/shared/modelUtils' } from '~/shared/modelUtils'
import { usePieceTypes } from '~/composables/usePieceTypes' import { usePieceTypes } from '~/composables/usePieceTypes'
import { useComponentTypes } from '~/composables/useComponentTypes' import { useComponentTypes } from '~/composables/useComponentTypes'
@@ -174,7 +159,7 @@ const syncFromProps = (value: any) => {
localStructure.customFields = hydrated.customFields localStructure.customFields = hydrated.customFields
localStructure.pieces = hydrated.pieces localStructure.pieces = hydrated.pieces
localStructure.subComponents = hydrated.subComponents localStructure.subComponents = hydrated.subComponents
lastEmitted = JSON.stringify(normalizeStructureForSave(value)) lastEmitted = JSON.stringify(cloneStructure(value))
} }
watch( watch(
@@ -185,16 +170,16 @@ watch(
{ deep: true } { deep: true }
) )
let lastEmitted = JSON.stringify(normalizeStructureForSave(props.modelValue)) let lastEmitted = JSON.stringify(cloneStructure(props.modelValue))
watch( watch(
localStructure, localStructure,
(value) => { (value) => {
const normalized = normalizeStructureForSave(value) const payload = cloneStructure(value)
const serialized = JSON.stringify(normalized) const serialized = JSON.stringify(payload)
if (serialized !== lastEmitted) { if (serialized !== lastEmitted) {
lastEmitted = serialized lastEmitted = serialized
emit('update:modelValue', normalized) emit('update:modelValue', payload)
} }
}, },
{ deep: true } { deep: true }
@@ -302,34 +287,35 @@ const updatePieceTypeLabel = (piece: any) => {
piece.typePieceId = match.id piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match) piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match) piece.name = match.name || formatPieceTypeOption(match)
} else { } else if (!piece.name) {
piece.typePieceLabel = '' piece.name = piece.typePieceLabel
piece.name = ''
} }
} }
} }
const handlePieceTypeChange = (piece: any) => { const handlePieceTypeSelect = (piece: any) => {
if (!piece) { if (!piece) {
return return
} }
const value = typeof piece.typePieceLabel === 'string' ? piece.typePieceLabel.trim() : '' const id = typeof piece.typePieceId === 'string' ? piece.typePieceId : ''
if (!value) {
if (!id) {
piece.typePieceId = '' piece.typePieceId = ''
piece.typePieceLabel = '' piece.typePieceLabel = ''
piece.name = '' piece.name = ''
return return
} }
const match = resolvePieceType(value)
if (match) { const option = pieceTypeMap.value.get(id)
piece.typePieceId = match.id if (!option) {
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceId = '' piece.typePieceId = ''
piece.typePieceLabel = '' piece.typePieceLabel = ''
piece.name = '' piece.name = ''
return
} }
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || piece.typePieceLabel
} }
const applyPieceLabels = (pieces?: any[]) => { const applyPieceLabels = (pieces?: any[]) => {
@@ -345,9 +331,8 @@ const applyPieceLabels = (pieces?: any[]) => {
piece.typePieceId = match.id piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match) piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match) piece.name = match.name || formatPieceTypeOption(match)
} else { } else if (!piece.name) {
piece.typePieceLabel = '' piece.name = piece.typePieceLabel
piece.name = ''
} }
} else if (!piece?.name) { } else if (!piece?.name) {
piece.name = '' piece.name = ''
@@ -452,7 +437,6 @@ const addPiece = () => {
ensureArray('pieces') ensureArray('pieces')
localStructure.pieces.push({ localStructure.pieces.push({
name: '', name: '',
quantity: undefined,
typePieceId: '', typePieceId: '',
typePieceLabel: '', typePieceLabel: '',
}) })
@@ -467,13 +451,8 @@ const addSubComponent = () => {
ensureArray('subComponents') ensureArray('subComponents')
localStructure.subComponents.push({ localStructure.subComponents.push({
name: '', name: '',
description: '',
quantity: undefined,
typeComposantId: '', typeComposantId: '',
typeComposantLabel: '', typeComposantLabel: '',
customFields: [],
pieces: [],
subComponents: [],
}) })
} }

View File

@@ -1,213 +1,36 @@
<template> <template>
<div class="border border-base-200 rounded-lg bg-base-100" :class="depthPadding"> <div class="border border-base-200 rounded-lg bg-base-100" :class="depthPadding">
<div class="flex items-start justify-between gap-3 border-b border-base-200 px-4 py-3"> <div class="flex items-center justify-between gap-3 px-4 py-3">
<div class="flex flex-col gap-1 flex-1"> <div class="flex-1">
<div class="flex items-center gap-2"> <select
<button type="button" class="btn btn-ghost btn-xs" @click="toggle"> v-model="node.typeComposantId"
<IconLucideChevronRight class="select select-bordered select-sm w-full"
class="w-4 h-4 transition-transform" @change="handleComponentTypeSelect(node)"
:class="{ 'rotate-90': expanded }" >
aria-hidden="true" <option value="">
/> Sélectionner une famille de composant
</button> </option>
<div class="flex-1"> <option
<input v-for="type in componentTypes"
:list="componentTypeListId" :key="type.id"
v-model="node.typeComposantLabel" :value="type.id"
type="search" >
autocomplete="off" {{ formatComponentTypeOption(type) }}
class="input input-sm input-bordered w-full" </option>
placeholder="Sélectionner une famille de composant" </select>
@change="handleComponentTypeChange(node)" <p class="mt-1 text-[11px] text-gray-500">
@blur="handleComponentTypeChange(node)" {{ node.typeComposantId ? `Sélection : ${getComponentTypeLabel(node.typeComposantId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
/> </p>
<datalist :id="componentTypeListId">
<option
v-for="type in componentTypes"
:key="type.id"
:value="formatComponentTypeOption(type)"
/>
</datalist>
<p class="mt-1 text-[11px] text-gray-500">
{{ node.typeComposantId ? `Sélection : ${getComponentTypeLabel(node.typeComposantId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
</div>
<span v-if="!expanded && node.description" class="text-xs text-gray-500 truncate">
{{ node.description }}
</span>
</div> </div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="emit('remove')"> <button type="button" class="btn btn-error btn-xs btn-square" @click="emit('remove')">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" /> <IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button> </button>
</div> </div>
<div v-if="expanded" class="space-y-5 px-4 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label"><span class="label-text">Description</span></label>
<textarea
v-model="node.description"
class="textarea textarea-bordered textarea-sm"
rows="2"
placeholder="Notes optionnelles"
></textarea>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Quantité</span></label>
<input
v-model.number="node.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-sm"
placeholder="Quantité (optionnel)"
/>
</div>
</div>
<section class="space-y-2">
<h4 class="text-sm font-semibold">Champs personnalisés</h4>
<p v-if="!(node.customFields?.length)" class="text-xs text-gray-500">
Aucun champ défini.
</p>
<div v-else class="space-y-2">
<div
v-for="(field, fieldIndex) in node.customFields"
:key="`sub-${depth}-${fieldIndex}`"
class="border border-base-200 rounded-md p-3 space-y-2"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<input
v-model="field.name"
type="text"
class="input input-bordered input-xs"
placeholder="Nom du champ"
/>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="text">Texte</option>
<option value="number">Nombre</option>
<option value="select">Liste</option>
<option value="boolean">Oui/Non</option>
<option value="date">Date</option>
</select>
</div>
<div class="flex items-center gap-2 text-xs">
<input v-model="field.required" type="checkbox" class="checkbox checkbox-xs" />
Obligatoire
</div>
<textarea
v-if="field.type === 'select'"
v-model="field.optionsText"
class="textarea textarea-bordered textarea-xs h-20"
placeholder="Option 1&#10;Option 2"
></textarea>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removeCustomField(fieldIndex)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline btn-xs" @click="addCustomField">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter un champ
</button>
</section>
<section class="space-y-2">
<h4 class="text-sm font-semibold">Pièces associées</h4>
<p v-if="!(node.pieces?.length)" class="text-xs text-gray-500">Aucune pièce définie.</p>
<div v-else class="space-y-2">
<div
v-for="(piece, pieceIndex) in node.pieces"
:key="`piece-${depth}-${pieceIndex}`"
class="border border-base-200 rounded-md p-3 space-y-3"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 space-y-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="form-control">
<label class="label"><span class="label-text">Famille de pièce</span></label>
<div>
<input
:list="getPieceTypeListId(pieceIndex)"
v-model="piece.typePieceLabel"
type="search"
autocomplete="off"
class="input input-bordered input-xs"
placeholder="Sélectionner une famille"
@change="handlePieceTypeChange(piece)"
@blur="handlePieceTypeChange(piece)"
/>
<datalist :id="getPieceTypeListId(pieceIndex)">
<option
v-for="type in pieceTypes"
:key="type.id"
:value="formatPieceTypeOption(type)"
/>
</datalist>
</div>
<p class="mt-1 text-[11px] text-gray-500">
{{ piece.typePieceId ? `Sélection : ${getPieceTypeLabel(piece.typePieceId) || 'Inconnue'}` : 'Aucune famille sélectionnée' }}
</p>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Quantité (optionnel)</span></label>
<input
v-model.number="piece.quantity"
type="number"
min="0"
step="1"
class="input input-bordered input-xs"
placeholder="Quantité"
/>
</div>
</div>
</div>
<button type="button" class="btn btn-error btn-xs btn-square" @click="removePiece(pieceIndex)">
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline btn-xs" @click="addPiece">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter une pièce
</button>
</section>
<section class="space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold">Sous-composants</h4>
<button type="button" class="btn btn-outline btn-xs" @click="addSubComponent">
<IconLucidePlus class="w-3 h-3 mr-2" aria-hidden="true" />
Ajouter
</button>
</div>
<p v-if="!(node.subComponents?.length)" class="text-xs text-gray-500">Aucun sous-composant défini.</p>
<div v-else class="space-y-3">
<StructureSubComponentEditor
v-for="(sub, index) in node.subComponents"
:key="`sub-${depth}-${index}`"
:node="sub"
:depth="depth + 1"
:piece-types="pieceTypes"
:component-types="componentTypes"
@remove="removeSubComponent(index)"
/>
</div>
</section>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, getCurrentInstance } from 'vue' import { computed, watch } from 'vue'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash from '~icons/lucide/trash' import IconLucideTrash from '~icons/lucide/trash'
defineOptions({ name: 'StructureSubComponentEditor' }) defineOptions({ name: 'StructureSubComponentEditor' })
@@ -221,36 +44,30 @@ type ModelTypeOption = {
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
node: Record<string, any> node: Record<string, any>
depth?: number depth?: number
pieceTypes?: ModelTypeOption[]
componentTypes?: ModelTypeOption[] componentTypes?: ModelTypeOption[]
}>(), { }>(), {
depth: 0, depth: 0,
pieceTypes: () => [],
componentTypes: () => [], componentTypes: () => [],
}) })
const emit = defineEmits(['remove']) const emit = defineEmits(['remove'])
const pieceTypes = computed(() => props.pieceTypes ?? [])
const componentTypes = computed(() => props.componentTypes ?? []) const componentTypes = computed(() => props.componentTypes ?? [])
const instance = getCurrentInstance() const depthPadding = computed(() => {
const componentTypeListId = `sub-component-type-options-${instance?.uid ?? 0}` const level = props.depth ?? 0
return level > 0 ? `ml-${Math.min(level * 4, 12)}` : ''
})
const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => { const formatModelTypeOption = (type: ModelTypeOption | undefined | null) => {
if (!type) return '' if (!type) {
return ''
}
return type.code ? `${type.name} (${type.code})` : type.name return type.code ? `${type.name} (${type.code})` : type.name
} }
const pieceTypeMap = computed(() => { const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) =>
const map = new Map<string, ModelTypeOption>() formatModelTypeOption(type)
pieceTypes.value.forEach((type) => {
if (type && typeof type.id === 'string') {
map.set(type.id, type)
}
})
return map
})
const componentTypeMap = computed(() => { const componentTypeMap = computed(() => {
const map = new Map<string, ModelTypeOption>() const map = new Map<string, ModelTypeOption>()
@@ -262,275 +79,51 @@ const componentTypeMap = computed(() => {
return map return map
}) })
const formatPieceTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const formatComponentTypeOption = (type: ModelTypeOption | undefined | null) => formatModelTypeOption(type)
const resolvePieceType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
pieceTypes.value.find((type) => {
const formatted = formatPieceTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const resolveComponentType = (input: string) => {
const normalized = input.trim().toLowerCase()
if (!normalized) {
return null
}
return (
componentTypes.value.find((type) => {
const formatted = formatComponentTypeOption(type).toLowerCase()
const name = (type?.name ?? '').toLowerCase()
const code = (type?.code ?? '').toLowerCase()
return (
formatted === normalized
|| name === normalized
|| (!!code && code === normalized)
)
}) ?? null
)
}
const getPieceTypeLabel = (id?: string) => {
if (!id) return ''
const option = pieceTypeMap.value.get(id)
return formatPieceTypeOption(option)
}
const getComponentTypeLabel = (id?: string) => { const getComponentTypeLabel = (id?: string) => {
if (!id) return '' if (!id) return ''
const option = componentTypeMap.value.get(id) return formatModelTypeOption(componentTypeMap.value.get(id))
return formatComponentTypeOption(option)
} }
const updatePieceTypeLabel = (piece: any) => { const syncComponentType = (component: any) => {
if (!piece) {
return
}
if (piece.typePieceId) {
const option = pieceTypeMap.value.get(piece.typePieceId)
if (option) {
piece.typePieceLabel = formatPieceTypeOption(option)
piece.name = option.name || formatPieceTypeOption(option)
} else if (!piece.typePieceLabel) {
piece.name = ''
}
} else if (piece.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
}
}
const handlePieceTypeChange = (piece: any) => {
if (!piece) {
return
}
const value = typeof piece.typePieceLabel === 'string' ? piece.typePieceLabel.trim() : ''
if (!value) {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
return
}
const match = resolvePieceType(value)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceId = ''
piece.typePieceLabel = ''
piece.name = ''
}
}
const getPieceTypeListId = (pieceIndex: number) => `sub-piece-type-options-${props.depth ?? 0}-${pieceIndex}`
const applyPieceLabels = (pieces?: any[]) => {
if (!Array.isArray(pieces)) {
return
}
pieces.forEach((piece) => {
if (piece?.typePieceId) {
updatePieceTypeLabel(piece)
} else if (piece?.typePieceLabel) {
const match = resolvePieceType(piece.typePieceLabel)
if (match) {
piece.typePieceId = match.id
piece.typePieceLabel = formatPieceTypeOption(match)
piece.name = match.name || formatPieceTypeOption(match)
} else {
piece.typePieceLabel = ''
piece.name = ''
}
} else if (!piece?.name) {
piece.name = ''
}
})
}
const applyComponentTypeLabel = (component: any) => {
if (!component) { if (!component) {
return return
} }
if (component.typeComposantId) { const id = typeof component.typeComposantId === 'string'
const option = componentTypeMap.value.get(component.typeComposantId) ? component.typeComposantId
if (option) {
component.typeComposantLabel = formatComponentTypeOption(option)
component.name = option.name || formatComponentTypeOption(option)
} else if (!component.typeComposantLabel) {
component.name = ''
}
} else if (component.typeComposantLabel) {
const match = resolveComponentType(component.typeComposantLabel)
if (match) {
component.typeComposantId = match.id
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantLabel = ''
component.name = ''
}
}
}
const handleComponentTypeChange = (component: any) => {
if (!component) {
return
}
const value = typeof component.typeComposantLabel === 'string'
? component.typeComposantLabel.trim()
: '' : ''
if (!value) {
component.typeComposantId = '' if (!id) {
component.typeComposantLabel = '' component.typeComposantLabel = ''
component.name = '' component.name = ''
return return
} }
const match = resolveComponentType(value)
if (match) { const option = componentTypeMap.value.get(id)
component.typeComposantId = match.id if (!option) {
component.typeComposantLabel = formatComponentTypeOption(match)
component.name = match.name || formatComponentTypeOption(match)
} else {
component.typeComposantId = ''
component.typeComposantLabel = '' component.typeComposantLabel = ''
component.name = '' component.name = ''
}
}
const traverseSubComponents = (components?: any[]) => {
if (!Array.isArray(components)) {
return return
} }
components.forEach((component) => {
applyComponentTypeLabel(component) component.typeComposantLabel = formatModelTypeOption(option)
applyPieceLabels(component?.pieces) component.name = option.name || component.typeComposantLabel
traverseSubComponents(component?.subComponents)
})
} }
const syncTypeLabels = () => { const handleComponentTypeSelect = (component: any) => {
applyComponentTypeLabel(props.node) syncComponentType(component)
applyPieceLabels(props.node?.pieces)
traverseSubComponents(props.node?.subComponents)
} }
watch(pieceTypes, () => {
syncTypeLabels()
}, { deep: true, immediate: true })
watch(componentTypes, () => { watch(componentTypes, () => {
syncTypeLabels() syncComponentType(props.node)
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
watch( watch(
() => props.node, () => props.node.typeComposantId,
() => { () => {
syncTypeLabels() syncComponentType(props.node)
},
{ deep: true }
)
const expanded = ref(true)
const depthPadding = computed(() => (props.depth > 0 ? 'ml-4' : ''))
const toggle = () => {
expanded.value = !expanded.value
}
const ensureArray = (key: 'customFields' | 'pieces' | 'subComponents') => {
if (!Array.isArray(props.node[key])) {
props.node[key] = []
} }
} )
const addCustomField = () => {
ensureArray('customFields')
props.node.customFields.push({
name: '',
type: 'text',
required: false,
optionsText: '',
options: [],
})
}
const removeCustomField = (index: number) => {
if (!Array.isArray(props.node.customFields)) return
props.node.customFields.splice(index, 1)
}
const addPiece = () => {
ensureArray('pieces')
props.node.pieces.push({
name: '',
quantity: undefined,
typePieceId: '',
typePieceLabel: '',
})
}
const removePiece = (index: number) => {
if (!Array.isArray(props.node.pieces)) return
props.node.pieces.splice(index, 1)
}
const addSubComponent = () => {
ensureArray('subComponents')
props.node.subComponents.push({
name: '',
description: '',
quantity: undefined,
typeComposantId: '',
typeComposantLabel: '',
customFields: [],
pieces: [],
subComponents: [],
})
}
const removeSubComponent = (index: number) => {
if (!Array.isArray(props.node.subComponents)) return
props.node.subComponents.splice(index, 1)
}
</script> </script>
<style scoped>
</style>

View File

@@ -1,739 +0,0 @@
<template>
<div class="space-y-4">
<div class="flex justify-end">
<button
type="button"
class="btn btn-outline btn-sm"
@click="toggleAllComponents"
>
<IconLucideChevronRight
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
</button>
<h5 class="text-sm font-medium">
Nouveau composant {{ index + 1 }}
</h5>
<span v-if="!isComponentExpanded(index)" class="text-xs text-gray-500 truncate max-w-[160px]">
{{ component.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-sm"
@click="removeComponent(index)"
>
<IconLucideChevronRight
class="w-4 h-4"
aria-hidden="true"
/>
</button>
<span class="text-xs font-medium">Champ personnalisé {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentCustomFieldExpanded(index, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[120px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeComponentCustomField(index, fieldIndex)"
>
<IconLucideChevronRight
class="w-3 h-3"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isComponentPieceCustomFieldExpanded(index, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeComponentPieceCustomField(index, pieceIndex, fieldIndex)"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-3 h-3 text-green-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentCustomFieldExpanded(index, subIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeSubComponentCustomField(index, subIndex, fieldIndex)"
>
<IconLucideChevronRight
class="w-2 h-2"
aria-hidden="true"
/>
</button>
<IconLucideChevronRight
class="w-2 h-2 text-red-500"
aria-hidden="true"
/>
</button>
<span class="text-xs">Champ {{ fieldIndex + 1 }}</span>
<span v-if="!isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="text-[10px] text-gray-500 truncate max-w-[100px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
class="btn btn-square btn-error btn-xs"
@click="removeSubComponentPieceCustomField(index, subIndex, pieceIndex, fieldIndex)"
>
<IconLucideX
class="w-2 h-2"
aria-hidden="true"
/>
</button>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="grid grid-cols-2 gap-1">
<input
v-model="field.name"
type="text"
placeholder="Nom"
class="input input-bordered input-xs"
>
<select v-model="field.type" class="select select-bordered select-xs">
<option value="">
Type
</option>
<option value="text">
Texte
</option>
<option value="number">
Nombre
</option>
<option value="select">
Liste
</option>
<option value="boolean">
Oui/Non
</option>
<option value="date">
Date
</option>
</select>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex)" class="mt-1">
<label class="flex items-center gap-1 text-xs">
<input
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs"
>
Obligatoire
</label>
</div>
<div v-if="isSubComponentPieceCustomFieldExpanded(index, subIndex, pieceIndex, fieldIndex) && field.type === 'select'" class="mt-1">
<textarea
v-model="field.optionsText"
placeholder="Option 1&#10;Option 2&#10;Option 3"
class="textarea textarea-bordered textarea-xs w-full h-10"
@input="updateSubComponentPieceFieldOptions(index, subIndex, pieceIndex, fieldIndex)"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
class="btn btn-outline btn-sm"
@click="addComponent"
>
<IconLucidePlus
class="w-4 h-4 mr-2"
aria-hidden="true"
/>
Ajouter un composant
</button>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import IconLucideChevronRight from '~icons/lucide/chevron-right'
import IconLucideX from '~icons/lucide/x'
import IconLucidePlus from '~icons/lucide/plus'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:modelValue'])
const components = ref(props.modelValue)
watch(() => props.modelValue, (newValue) => {
components.value = newValue
initializeExpansionState()
})
const allExpanded = ref(false)
const expandedComponents = ref([])
const expandedComponentCustomFields = reactive({})
const expandedComponentPieces = reactive({})
const expandedComponentPieceCustomFields = reactive({})
const expandedSubComponents = reactive({})
const expandedSubComponentCustomFields = reactive({})
const expandedSubComponentPieces = reactive({})
const expandedSubComponentPieceCustomFields = reactive({})
const ensureComponentState = (componentIndex) => {
if (!expandedComponentCustomFields[componentIndex]) {
expandedComponentCustomFields[componentIndex] = []
}
if (!expandedComponentPieces[componentIndex]) {
expandedComponentPieces[componentIndex] = []
}
if (!expandedComponentPieceCustomFields[componentIndex]) {
expandedComponentPieceCustomFields[componentIndex] = {}
}
if (!expandedSubComponents[componentIndex]) {
expandedSubComponents[componentIndex] = []
}
if (!expandedSubComponentCustomFields[componentIndex]) {
expandedSubComponentCustomFields[componentIndex] = {}
}
if (!expandedSubComponentPieces[componentIndex]) {
expandedSubComponentPieces[componentIndex] = {}
}
if (!expandedSubComponentPieceCustomFields[componentIndex]) {
expandedSubComponentPieceCustomFields[componentIndex] = {}
}
}
const ensureComponentPieceState = (componentIndex, pieceIndex) => {
ensureComponentState(componentIndex)
if (!expandedComponentPieceCustomFields[componentIndex][pieceIndex]) {
expandedComponentPieceCustomFields[componentIndex][pieceIndex] = []
}
}
const ensureSubComponentState = (componentIndex, subIndex) => {
ensureComponentState(componentIndex)
if (!expandedSubComponentCustomFields[componentIndex][subIndex]) {
expandedSubComponentCustomFields[componentIndex][subIndex] = []
}
if (!expandedSubComponentPieces[componentIndex][subIndex]) {
expandedSubComponentPieces[componentIndex][subIndex] = []
}
if (!expandedSubComponentPieceCustomFields[componentIndex][subIndex]) {
expandedSubComponentPieceCustomFields[componentIndex][subIndex] = {}
}
}
const ensureSubComponentPieceState = (componentIndex, subIndex, pieceIndex) => {
ensureSubComponentState(componentIndex, subIndex)
if (!expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex]) {
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex] = []
}
}
const isComponentExpanded = (index) => {
if (expandedComponents.value[index] === undefined) {
expandedComponents.value[index] = allExpanded.value
}
return expandedComponents.value[index]
}
const toggleComponentDetails = (index) => {
expandedComponents.value[index] = !isComponentExpanded(index)
if (!expandedComponents.value[index]) {
allExpanded.value = false
}
}
const isComponentCustomFieldExpanded = (componentIndex, fieldIndex) => {
ensureComponentState(componentIndex)
if (expandedComponentCustomFields[componentIndex][fieldIndex] === undefined) {
expandedComponentCustomFields[componentIndex][fieldIndex] = allExpanded.value
}
return expandedComponentCustomFields[componentIndex][fieldIndex]
}
const toggleComponentCustomFieldDetails = (componentIndex, fieldIndex) => {
ensureComponentState(componentIndex)
expandedComponentCustomFields[componentIndex][fieldIndex] = !isComponentCustomFieldExpanded(componentIndex, fieldIndex)
if (!expandedComponentCustomFields[componentIndex][fieldIndex]) {
allExpanded.value = false
}
}
const isComponentPieceExpanded = (componentIndex, pieceIndex) => {
ensureComponentState(componentIndex)
if (expandedComponentPieces[componentIndex][pieceIndex] === undefined) {
expandedComponentPieces[componentIndex][pieceIndex] = allExpanded.value
}
return expandedComponentPieces[componentIndex][pieceIndex]
}
const toggleComponentPieceDetails = (componentIndex, pieceIndex) => {
ensureComponentState(componentIndex)
expandedComponentPieces[componentIndex][pieceIndex] = !isComponentPieceExpanded(componentIndex, pieceIndex)
if (!expandedComponentPieces[componentIndex][pieceIndex]) {
allExpanded.value = false
}
}
const isComponentPieceCustomFieldExpanded = (componentIndex, pieceIndex, fieldIndex) => {
ensureComponentPieceState(componentIndex, pieceIndex)
if (expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] === undefined) {
expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] = allExpanded.value
}
return expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex]
}
const toggleComponentPieceCustomFieldDetails = (componentIndex, pieceIndex, fieldIndex) => {
ensureComponentPieceState(componentIndex, pieceIndex)
expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex] = !isComponentPieceCustomFieldExpanded(componentIndex, pieceIndex, fieldIndex)
if (!expandedComponentPieceCustomFields[componentIndex][pieceIndex][fieldIndex]) {
allExpanded.value = false
}
}
const isSubComponentExpanded = (componentIndex, subIndex) => {
ensureComponentState(componentIndex)
if (expandedSubComponents[componentIndex][subIndex] === undefined) {
expandedSubComponents[componentIndex][subIndex] = allExpanded.value
}
return expandedSubComponents[componentIndex][subIndex]
}
const toggleSubComponentDetails = (componentIndex, subIndex) => {
ensureComponentState(componentIndex)
expandedSubComponents[componentIndex][subIndex] = !isSubComponentExpanded(componentIndex, subIndex)
if (!expandedSubComponents[componentIndex][subIndex]) {
allExpanded.value = false
}
}
const isSubComponentCustomFieldExpanded = (componentIndex, subIndex, fieldIndex) => {
ensureSubComponentState(componentIndex, subIndex)
if (expandedSubComponentCustomFields[componentIndex][subIndex][fieldIndex] === undefined) {
expandedSubComponentCustomFields[componentIndex][subIndex][fieldIndex] = allExpanded.value
}
return expandedSubComponentCustomFields[componentIndex][subIndex][fieldIndex]
}
const toggleSubComponentCustomFieldDetails = (componentIndex, subIndex, fieldIndex) => {
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentCustomFields[componentIndex][subIndex][fieldIndex] = !isSubComponentCustomFieldExpanded(componentIndex, subIndex, fieldIndex)
if (!expandedSubComponentCustomFields[componentIndex][subIndex][fieldIndex]) {
allExpanded.value = false
}
}
const isSubComponentPieceExpanded = (componentIndex, subIndex, pieceIndex) => {
ensureSubComponentState(componentIndex, subIndex)
if (expandedSubComponentPieces[componentIndex][subIndex][pieceIndex] === undefined) {
expandedSubComponentPieces[componentIndex][subIndex][pieceIndex] = allExpanded.value
}
return expandedSubComponentPieces[componentIndex][subIndex][pieceIndex]
}
const toggleSubComponentPieceDetails = (componentIndex, subIndex, pieceIndex) => {
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentPieces[componentIndex][subIndex][pieceIndex] = !isSubComponentPieceExpanded(componentIndex, subIndex, pieceIndex)
if (!expandedSubComponentPieces[componentIndex][subIndex][pieceIndex]) {
allExpanded.value = false
}
}
const isSubComponentPieceCustomFieldExpanded = (componentIndex, subIndex, pieceIndex, fieldIndex) => {
ensureSubComponentPieceState(componentIndex, subIndex, pieceIndex)
if (expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex][fieldIndex] === undefined) {
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex][fieldIndex] = allExpanded.value
}
return expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex][fieldIndex]
}
const toggleSubComponentPieceCustomFieldDetails = (componentIndex, subIndex, pieceIndex, fieldIndex) => {
ensureSubComponentPieceState(componentIndex, subIndex, pieceIndex)
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex][fieldIndex] = !isSubComponentPieceCustomFieldExpanded(componentIndex, subIndex, pieceIndex, fieldIndex)
if (!expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex][fieldIndex]) {
allExpanded.value = false
}
}
const reorderStore = (store) => {
const reordered = {}
Object.keys(store)
.map(Number)
.sort((a, b) => a - b)
.forEach((key, position) => {
reordered[position] = store[key]
})
Object.keys(store).forEach(key => delete store[key])
Object.assign(store, reordered)
}
const reorderNestedStore = (store, componentIndex) => {
const entries = store[componentIndex] || {}
const reordered = {}
Object.keys(entries)
.map(Number)
.sort((a, b) => a - b)
.forEach((key, position) => {
reordered[position] = entries[key]
})
Object.keys(entries).forEach(key => delete entries[key])
Object.assign(entries, reordered)
}
const clearExpansionState = () => {
expandedComponents.value = []
Object.keys(expandedComponentCustomFields).forEach(key => delete expandedComponentCustomFields[key])
Object.keys(expandedComponentPieces).forEach(key => delete expandedComponentPieces[key])
Object.keys(expandedComponentPieceCustomFields).forEach(key => delete expandedComponentPieceCustomFields[key])
Object.keys(expandedSubComponents).forEach(key => delete expandedSubComponents[key])
Object.keys(expandedSubComponentCustomFields).forEach(key => delete expandedSubComponentCustomFields[key])
Object.keys(expandedSubComponentPieces).forEach(key => delete expandedSubComponentPieces[key])
Object.keys(expandedSubComponentPieceCustomFields).forEach(key => delete expandedSubComponentPieceCustomFields[key])
}
const setAllExpanded = (value) => {
clearExpansionState()
allExpanded.value = value
expandedComponents.value = components.value.map(() => value)
components.value.forEach((component, componentIndex) => {
ensureComponentState(componentIndex)
expandedComponentCustomFields[componentIndex] = (component.customFields || []).map(() => value)
expandedComponentPieces[componentIndex] = (component.pieces || []).map(() => value)
expandedComponentPieceCustomFields[componentIndex] = {}
;(component.pieces || []).forEach((piece, pieceIndex) => {
expandedComponentPieceCustomFields[componentIndex][pieceIndex] = (piece.customFields || []).map(() => value)
})
expandedSubComponents[componentIndex] = (component.subComponents || []).map(() => value)
expandedSubComponentCustomFields[componentIndex] = {}
expandedSubComponentPieces[componentIndex] = {}
expandedSubComponentPieceCustomFields[componentIndex] = {}
;(component.subComponents || []).forEach((subComponent, subIndex) => {
expandedSubComponentCustomFields[componentIndex][subIndex] = (subComponent.customFields || []).map(() => value)
expandedSubComponentPieces[componentIndex][subIndex] = (subComponent.pieces || []).map(() => value)
expandedSubComponentPieceCustomFields[componentIndex][subIndex] = {}
;(subComponent.pieces || []).forEach((piece, pieceIndex) => {
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex] = (piece.customFields || []).map(() => value)
})
})
})
}
const toggleAllComponents = () => {
setAllExpanded(!allExpanded.value)
}
const initializeExpansionState = () => {
setAllExpanded(false)
}
onMounted(() => {
initializeExpansionState()
})
const addComponent = () => {
components.value.push({
name: '',
reference: '',
constructeur: '',
prix: null,
pieces: [],
customFields: [],
subComponents: []
})
emit('update:modelValue', components.value)
expandedComponents.value.push(allExpanded.value)
const newIndex = components.value.length - 1
ensureComponentState(newIndex)
expandedComponentCustomFields[newIndex] = []
expandedComponentPieces[newIndex] = []
expandedComponentPieceCustomFields[newIndex] = {}
expandedSubComponents[newIndex] = []
expandedSubComponentCustomFields[newIndex] = {}
expandedSubComponentPieces[newIndex] = {}
expandedSubComponentPieceCustomFields[newIndex] = {}
}
const removeComponent = (index) => {
components.value.splice(index, 1)
emit('update:modelValue', components.value)
expandedComponents.value.splice(index, 1)
delete expandedComponentCustomFields[index]
delete expandedComponentPieces[index]
delete expandedComponentPieceCustomFields[index]
delete expandedSubComponents[index]
delete expandedSubComponentCustomFields[index]
delete expandedSubComponentPieces[index]
delete expandedSubComponentPieceCustomFields[index]
reorderStore(expandedComponentCustomFields)
reorderStore(expandedComponentPieces)
reorderStore(expandedComponentPieceCustomFields)
reorderStore(expandedSubComponents)
reorderStore(expandedSubComponentCustomFields)
reorderStore(expandedSubComponentPieces)
reorderStore(expandedSubComponentPieceCustomFields)
}
const addComponentCustomField = (componentIndex) => {
const component = components.value[componentIndex]
if (!component.customFields) {
component.customFields = []
}
component.customFields.push({
name: '',
type: '',
required: false,
optionsText: ''
})
ensureComponentState(componentIndex)
expandedComponentCustomFields[componentIndex].push(allExpanded.value)
}
const removeComponentCustomField = (componentIndex, fieldIndex) => {
const component = components.value[componentIndex]
if (component?.customFields) {
component.customFields.splice(fieldIndex, 1)
}
ensureComponentState(componentIndex)
expandedComponentCustomFields[componentIndex].splice(fieldIndex, 1)
}
const updateComponentFieldOptions = (componentIndex, fieldIndex) => {
const component = components.value[componentIndex]
if (component?.customFields?.[fieldIndex]) {
component.customFields[fieldIndex].optionsText = component.customFields[fieldIndex].optionsText.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}
}
const addComponentPiece = (componentIndex) => {
const component = components.value[componentIndex]
if (!component.pieces) {
component.pieces = []
}
component.pieces.push({
name: '',
reference: '',
constructeur: '',
prix: null,
customFields: []
})
ensureComponentState(componentIndex)
expandedComponentPieces[componentIndex].push(allExpanded.value)
ensureComponentPieceState(componentIndex, component.pieces.length - 1)
}
const removeComponentPiece = (componentIndex, pieceIndex) => {
const component = components.value[componentIndex]
if (component?.pieces) {
component.pieces.splice(pieceIndex, 1)
}
ensureComponentState(componentIndex)
expandedComponentPieces[componentIndex].splice(pieceIndex, 1)
delete expandedComponentPieceCustomFields[componentIndex][pieceIndex]
reorderNestedStore(expandedComponentPieceCustomFields, componentIndex)
}
const addComponentPieceCustomField = (componentIndex, pieceIndex) => {
const component = components.value[componentIndex]
if (!component?.pieces?.[pieceIndex].customFields) {
component.pieces[pieceIndex].customFields = []
}
component.pieces[pieceIndex].customFields.push({
name: '',
type: '',
required: false,
optionsText: ''
})
ensureComponentPieceState(componentIndex, pieceIndex)
expandedComponentPieceCustomFields[componentIndex][pieceIndex].push(allExpanded.value)
}
const removeComponentPieceCustomField = (componentIndex, pieceIndex, fieldIndex) => {
const component = components.value[componentIndex]
if (component?.pieces?.[pieceIndex]?.customFields) {
component.pieces[pieceIndex].customFields.splice(fieldIndex, 1)
}
ensureComponentPieceState(componentIndex, pieceIndex)
expandedComponentPieceCustomFields[componentIndex][pieceIndex].splice(fieldIndex, 1)
}
const updateComponentPieceFieldOptions = (componentIndex, pieceIndex, fieldIndex) => {
const component = components.value[componentIndex]
if (component?.pieces?.[pieceIndex]?.customFields?.[fieldIndex]) {
component.pieces[pieceIndex].customFields[fieldIndex].optionsText = component.pieces[pieceIndex].customFields[fieldIndex].optionsText.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}
}
const addSubComponent = (componentIndex) => {
const component = components.value[componentIndex]
if (!component.subComponents) {
component.subComponents = []
}
component.subComponents.push({
name: '',
reference: '',
constructeur: '',
prix: null,
customFields: [],
pieces: [],
subComponents: []
})
ensureComponentState(componentIndex)
expandedSubComponents[componentIndex].push(allExpanded.value)
const newIndex = component.subComponents.length - 1
ensureSubComponentState(componentIndex, newIndex)
}
const removeSubComponent = (componentIndex, subIndex) => {
const component = components.value[componentIndex]
if (component?.subComponents) {
component.subComponents.splice(subIndex, 1)
}
ensureComponentState(componentIndex)
expandedSubComponents[componentIndex].splice(subIndex, 1)
delete expandedSubComponentCustomFields[componentIndex][subIndex]
delete expandedSubComponentPieces[componentIndex][subIndex]
delete expandedSubComponentPieceCustomFields[componentIndex][subIndex]
reorderNestedStore(expandedSubComponentCustomFields, componentIndex)
reorderNestedStore(expandedSubComponentPieces, componentIndex)
const pieceFieldEntries = expandedSubComponentPieceCustomFields[componentIndex] || {}
const reordered = {}
Object.keys(pieceFieldEntries)
.map(Number)
.sort((a, b) => a - b)
.forEach((key, position) => {
reordered[position] = pieceFieldEntries[key]
})
Object.keys(pieceFieldEntries).forEach(key => delete pieceFieldEntries[key])
Object.assign(pieceFieldEntries, reordered)
}
const addSubComponentCustomField = (componentIndex, subIndex) => {
const subComponent = components.value[componentIndex]?.subComponents?.[subIndex]
if (!subComponent.customFields) {
subComponent.customFields = []
}
subComponent.customFields.push({
name: '',
type: '',
required: false,
optionsText: ''
})
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentCustomFields[componentIndex][subIndex].push(allExpanded.value)
}
const removeSubComponentCustomField = (componentIndex, subIndex, fieldIndex) => {
const subComponent = components.value[componentIndex]?.subComponents?.[subIndex]
if (subComponent?.customFields) {
subComponent.customFields.splice(fieldIndex, 1)
}
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentCustomFields[componentIndex][subIndex].splice(fieldIndex, 1)
}
const addSubComponentPiece = (componentIndex, subIndex) => {
const subComponent = components.value[componentIndex]?.subComponents?.[subIndex]
if (!subComponent.pieces) {
subComponent.pieces = []
}
subComponent.pieces.push({
name: '',
reference: '',
constructeur: '',
prix: null,
customFields: []
})
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentPieces[componentIndex][subIndex].push(allExpanded.value)
ensureSubComponentPieceState(componentIndex, subIndex, subComponent.pieces.length - 1)
}
const removeSubComponentPiece = (componentIndex, subIndex, pieceIndex) => {
const subComponent = components.value[componentIndex]?.subComponents?.[subIndex]
if (subComponent?.pieces) {
subComponent.pieces.splice(pieceIndex, 1)
}
ensureSubComponentState(componentIndex, subIndex)
expandedSubComponentPieces[componentIndex][subIndex].splice(pieceIndex, 1)
if (expandedSubComponentPieceCustomFields[componentIndex]?.[subIndex]) {
delete expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex]
const store = expandedSubComponentPieceCustomFields[componentIndex][subIndex]
const reordered = {}
Object.keys(store || {})
.map(Number)
.sort((a, b) => a - b)
.forEach((key, position) => {
reordered[position] = store[key]
})
Object.keys(store || {}).forEach(key => delete store[key])
Object.assign(store || {}, reordered)
}
}
const addSubComponentPieceCustomField = (componentIndex, subIndex, pieceIndex) => {
const piece = components.value[componentIndex]?.subComponents?.[subIndex]?.pieces?.[pieceIndex]
if (!piece.customFields) {
piece.customFields = []
}
piece.customFields.push({
name: '',
type: '',
required: false,
optionsText: ''
})
ensureSubComponentPieceState(componentIndex, subIndex, pieceIndex)
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex].push(allExpanded.value)
}
const removeSubComponentPieceCustomField = (componentIndex, subIndex, pieceIndex, fieldIndex) => {
const piece = components.value[componentIndex]?.subComponents?.[subIndex]?.pieces?.[pieceIndex]
if (piece?.customFields) {
piece.customFields.splice(fieldIndex, 1)
}
ensureSubComponentPieceState(componentIndex, subIndex, pieceIndex)
expandedSubComponentPieceCustomFields[componentIndex][subIndex][pieceIndex].splice(fieldIndex, 1)
}
const updateSubComponentPieceFieldOptions = (componentIndex, subIndex, pieceIndex, fieldIndex) => {
const piece = components.value[componentIndex]?.subComponents?.[subIndex]?.pieces?.[pieceIndex]
if (piece?.customFields?.[fieldIndex]) {
piece.customFields[fieldIndex].optionsText = piece.customFields[fieldIndex].optionsText.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}
}
</script>

View File

@@ -1,23 +1,24 @@
import { useProfileSession } from '#imports' import { useProfileSession } from "#imports";
export default defineNuxtRouteMiddleware(async (to) => { export default defineNuxtRouteMiddleware(async (to) => {
const { ensureSession, fetchCurrentProfile, activeProfile } = useProfileSession() const { ensureSession, fetchCurrentProfile, activeProfile } =
await ensureSession() useProfileSession();
await ensureSession();
const rawPath = to?.path ?? '' const rawPath = to?.path ?? "";
const normalizedPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}` const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
const fullPath = to?.fullPath ?? normalizedPath const fullPath = to?.fullPath ?? normalizedPath;
const routeName = typeof to?.name === 'string' ? to.name : '' const routeName = typeof to?.name === "string" ? to.name : "";
const isProfilesRoute = const isProfilesRoute =
normalizedPath.startsWith('/profiles') || normalizedPath.startsWith("/profiles") ||
fullPath.startsWith('/profiles') || fullPath.startsWith("/profiles") ||
routeName.startsWith('profiles') routeName.startsWith("profiles");
if (process.client && !activeProfile.value) { if (process.client && !activeProfile.value) {
await fetchCurrentProfile() await fetchCurrentProfile();
} }
if (process.client && !activeProfile.value && !isProfilesRoute) { if (process.client && !activeProfile.value && !isProfilesRoute) {
return navigateTo('/profiles') return navigateTo("/profiles");
} }
}) });

View File

@@ -203,7 +203,12 @@ import { useComponentModels } from '~/composables/useComponentModels'
import { useComponentTypes } from '~/composables/useComponentTypes' import { useComponentTypes } from '~/composables/useComponentTypes'
import { useToast } from '~/composables/useToast' import { useToast } from '~/composables/useToast'
import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue' import ComponentModelStructureEditor from '~/components/ComponentModelStructureEditor.vue'
import { formatStructurePreview } from '~/shared/modelUtils' import {
formatStructurePreview,
defaultStructure,
cloneStructure,
normalizeStructureForSave,
} from '~/shared/modelUtils'
import { formatFrenchDate } from '~/utils/date' import { formatFrenchDate } from '~/utils/date'
import IconLucidePlus from '~icons/lucide/plus' import IconLucidePlus from '~icons/lucide/plus'
import IconLucideLayers from '~icons/lucide/layers' import IconLucideLayers from '~icons/lucide/layers'
@@ -231,7 +236,7 @@ const form = reactive({
name: '', name: '',
description: '', description: '',
typeComposantId: '', typeComposantId: '',
structure: {} structure: defaultStructure(),
} }
}) })
@@ -253,7 +258,7 @@ const startCreate = () => {
name: '', name: '',
description: '', description: '',
typeComposantId: selectedType.value !== 'all' ? selectedType.value : '', typeComposantId: selectedType.value !== 'all' ? selectedType.value : '',
structure: {} structure: defaultStructure(),
} }
ensureTypeSelected() ensureTypeSelected()
} }
@@ -265,7 +270,7 @@ const startEdit = (model) => {
name: model.name, name: model.name,
description: model.description || '', description: model.description || '',
typeComposantId: model.typeComposantId || model.typeComposant?.id || '', typeComposantId: model.typeComposantId || model.typeComposant?.id || '',
structure: model.structure || {} structure: cloneStructure(model.structure || defaultStructure()),
} }
} }
@@ -308,7 +313,7 @@ const handleSubmit = async () => {
name: form.data.name.trim(), name: form.data.name.trim(),
description: form.data.description.trim() || undefined, description: form.data.description.trim() || undefined,
typeComposantId: form.data.typeComposantId, typeComposantId: form.data.typeComposantId,
structure: form.data.structure || {} structure: normalizeStructureForSave(form.data.structure),
}) })
if (!result.success) { if (!result.success) {
showError(result.error || 'Impossible de créer le modèle') showError(result.error || 'Impossible de créer le modèle')
@@ -320,7 +325,7 @@ const handleSubmit = async () => {
name: form.data.name.trim(), name: form.data.name.trim(),
description: form.data.description.trim() || undefined, description: form.data.description.trim() || undefined,
typeComposantId: form.data.typeComposantId, typeComposantId: form.data.typeComposantId,
structure: form.data.structure || {} structure: normalizeStructureForSave(form.data.structure),
}) })
if (!result.success) { if (!result.success) {
showError(result.error || 'Impossible de mettre à jour le modèle') showError(result.error || 'Impossible de mettre à jour le modèle')

View File

@@ -7,7 +7,7 @@
<h2 class="text-2xl font-bold text-gray-800"> <h2 class="text-2xl font-bold text-gray-800">
Squelettes de machine Squelettes de machine
</h2> </h2>
<NuxtLink to="/generator" class="btn btn-primary"> <NuxtLink to="/machine-skeleton/new" class="btn btn-primary">
<IconLucidePlus <IconLucidePlus
class="w-5 h-5 mr-2" class="w-5 h-5 mr-2"
aria-hidden="true" aria-hidden="true"