feat: add profile management flow

This commit is contained in:
Matthieu
2025-09-17 23:11:13 +02:00
parent 37c66ac3d6
commit 316dcb6339
27 changed files with 2717 additions and 1556 deletions

View File

@@ -0,0 +1,136 @@
<template>
<dialog class="modal" :class="{ 'modal-open': open }">
<div class="modal-box max-w-3xl">
<form method="dialog" class="modal-close" @submit.prevent></form>
<h3 class="font-bold text-lg mb-2">Préparer l'impression</h3>
<p class="text-sm text-base-content/70 mb-4">
Choisissez les sections à inclure avant de lancer l'impression.
</p>
<div class="flex flex-wrap gap-2 mb-4">
<button type="button" class="btn btn-xs btn-outline" @click="emit('select-all')">
Tout sélectionner
</button>
<button type="button" class="btn btn-xs btn-outline" @click="emit('deselect-all')">
Tout désélectionner
</button>
</div>
<div class="max-h-[420px] overflow-y-auto pr-2 space-y-6">
<section class="bg-base-200/50 rounded-xl p-4 space-y-3">
<h4 class="font-semibold text-sm uppercase tracking-wide text-base-content/70">
Machine
</h4>
<label class="flex items-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.info"
/>
<div>
<p class="font-medium">Informations générales</p>
<p class="text-xs text-base-content/60">
Nom, emplacement, site et constructeur de la machine.
</p>
</div>
</label>
<label class="flex items-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.customFields"
/>
<div>
<p class="font-medium">Champs personnalisés</p>
<p class="text-xs text-base-content/60">
Valeurs spécifiques configurées pour cette machine.
</p>
</div>
</label>
<label class="flex items-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.machine.documents"
/>
<div>
<p class="font-medium">Documents</p>
<p class="text-xs text-base-content/60">
Pièces jointes liées directement à la machine.
</p>
</div>
</label>
</section>
<section class="bg-base-200/30 rounded-xl p-4 space-y-3" v-if="hasComponents">
<h4 class="font-semibold text-sm uppercase tracking-wide text-base-content/70">
Composants & pièces
</h4>
<div class="space-y-3">
<MachinePrintSelectionNode
v-for="component in componentsList"
:key="component.id"
:component="component"
:selection="selection"
/>
</div>
</section>
<section class="bg-base-200/30 rounded-xl p-4 space-y-3" v-if="hasPieces">
<h4 class="font-semibold text-sm uppercase tracking-wide text-base-content/70">
Pièces indépendantes
</h4>
<div class="space-y-2">
<label
v-for="piece in piecesList"
:key="piece.id"
class="flex items-start gap-3"
>
<input
type="checkbox"
class="checkbox checkbox-secondary mt-1"
v-model="selection.pieces[piece.id]"
/>
<div>
<p class="font-medium">{{ piece.name }}</p>
<p class="text-xs text-base-content/60">
{{ piece.reference || 'Référence inconnue' }}
</p>
</div>
</label>
</div>
</section>
</div>
<div class="modal-action">
<button type="button" class="btn btn-ghost" @click="emit('close')">
Annuler
</button>
<button type="button" class="btn btn-primary" @click="emit('confirm')">
Imprimer
</button>
</div>
</div>
</dialog>
</template>
<script setup>
import { computed, toRef } from 'vue'
import MachinePrintSelectionNode from '~/components/MachinePrintSelectionNode.vue'
const props = defineProps({
open: { type: Boolean, default: false },
selection: { type: Object, required: true },
components: { type: Array, default: () => [] },
pieces: { type: Array, default: () => [] },
})
const emit = defineEmits(['close', 'confirm', 'select-all', 'deselect-all'])
const selection = toRef(props, 'selection')
const componentsList = computed(() => props.components || [])
const piecesList = computed(() => props.pieces || [])
const hasComponents = computed(() => componentsList.value.length > 0)
const hasPieces = computed(() => piecesList.value.length > 0)
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="rounded-lg border border-base-300 bg-base-100/80 p-3 space-y-3">
<label class="flex items-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary mt-1"
v-model="selection.components[component.id]"
/>
<div class="flex-1">
<p class="font-medium">{{ component.name }}</p>
<p v-if="component.reference" class="text-xs text-base-content/60">
{{ component.reference }}
</p>
</div>
</label>
<div v-if="childPieces.length" class="pl-6 space-y-2">
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
Pièces
</p>
<label
v-for="piece in childPieces"
:key="piece.id"
class="flex items-start gap-3"
>
<input
type="checkbox"
class="checkbox checkbox-secondary mt-1"
v-model="selection.pieces[piece.id]"
/>
<div>
<p class="font-medium">{{ piece.name }}</p>
<p class="text-xs text-base-content/60">
{{ piece.reference || 'Référence inconnue' }}
</p>
</div>
</label>
</div>
<div v-if="childComponents.length" class="pl-6 space-y-3 border-l border-base-200">
<MachinePrintSelectionNode
v-for="child in childComponents"
:key="child.id"
:component="child"
:selection="selection"
/>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
defineOptions({ name: 'MachinePrintSelectionNode' })
const props = defineProps({
component: { type: Object, required: true },
selection: { type: Object, required: true },
})
const childComponents = computed(() => props.component.subComponents || [])
const childPieces = computed(() => props.component.pieces || [])
</script>

View File

@@ -0,0 +1,79 @@
<template>
<section :class="sectionClasses">
<div :class="contentClasses">
<div :class="['space-y-4', maxWidthClass]">
<component :is="headingTag" v-if="title" class="text-4xl font-bold">
{{ title }}
</component>
<p v-if="subtitle" class="text-sm opacity-90">{{ subtitle }}</p>
<slot />
</div>
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
gradientFrom: {
type: String,
default: 'from-primary',
},
gradientTo: {
type: String,
default: 'to-secondary',
},
minHeight: {
type: String,
default: 'min-h-[25vh]',
},
maxWidth: {
type: String,
default: 'max-w-xl',
},
rounded: {
type: Boolean,
default: false,
},
alignment: {
type: String,
default: 'center',
validator: (value) => ['center', 'start', 'end'].includes(value),
},
headingTag: {
type: String,
default: 'h1',
},
})
const sectionClasses = computed(() => {
const classes = ['hero', 'bg-gradient-to-r', props.gradientFrom, props.gradientTo, props.minHeight]
if (props.rounded) {
classes.push('rounded-lg')
}
return classes
})
const contentClasses = computed(() => {
const base = ['hero-content', 'text-neutral-content']
if (props.alignment === 'center') {
base.push('text-center')
} else if (props.alignment === 'start') {
base.push('justify-start', 'text-left')
} else if (props.alignment === 'end') {
base.push('justify-end', 'text-right')
}
return base
})
const maxWidthClass = computed(() => props.maxWidth)
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="card-actions justify-end">
<button type="button" @click="$emit('reset')" class="btn btn-outline">
Réinitialiser
</button>
<button type="submit" class="btn btn-primary" :disabled="saving">
<svg
v-if="saving"
class="w-5 h-5 mr-2 animate-spin"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></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="M5 13l4 4L19 7"></path>
</svg>
{{ saving ? 'Sauvegarde...' : 'Sauvegarder les modifications' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
saving: {
type: Boolean,
default: false,
},
})
defineEmits(['reset'])
</script>

View File

@@ -0,0 +1,103 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Informations de base</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du type</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
v-model="nameModel"
type="text"
placeholder="Nom du type de machine"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Catégorie</span>
</label>
<input
v-model="categoryModel"
type="text"
placeholder="Catégorie du type"
class="input input-bordered"
/>
</div>
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text">Description</span>
</label>
<textarea
v-model="descriptionModel"
placeholder="Description du type de machine"
class="textarea textarea-bordered h-24"
></textarea>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Fréquence de maintenance</span>
</label>
<input
v-model="maintenanceModel"
type="text"
placeholder="ex: Mensuelle, Trimestrielle"
class="input input-bordered"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
default: '',
},
category: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
maintenanceFrequency: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:name', 'update:category', 'update:description', 'update:maintenanceFrequency'])
const nameModel = computed({
get: () => props.name,
set: (value) => emit('update:name', value),
})
const categoryModel = computed({
get: () => props.category,
set: (value) => emit('update:category', value),
})
const descriptionModel = computed({
get: () => props.description,
set: (value) => emit('update:description', value),
})
const maintenanceModel = computed({
get: () => props.maintenanceFrequency,
set: (value) => emit('update:maintenanceFrequency', value),
})
</script>

View File

@@ -0,0 +1,294 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-sm p-1"
@click="toggleSection"
>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': expanded }"
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"></path>
</svg>
</button>
<h3 class="card-title text-lg">Composants</h3>
<span class="badge badge-accent">{{ components.length }}</span>
</div>
</div>
<div v-if="expanded" class="space-y-4">
<div
v-for="(component, index) in components"
:key="index"
class="border border-gray-200 rounded-lg p-4 bg-gray-50 space-y-4"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs p-1"
@click="toggleComponent(index)"
title="Plier / déplier le composant"
>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': isComponentExpanded(index) }"
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"></path>
</svg>
</button>
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path>
</svg>
<h5 class="text-sm font-medium">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"
@click="removeComponent(index)"
class="btn btn-square btn-error btn-sm"
title="Supprimer ce composant"
>
<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="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div v-if="isComponentExpanded(index)" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du composant</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
:value="component.name"
type="text"
placeholder="Nom du composant"
class="input input-bordered input-sm"
required
@input="updateComponent(index, { name: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Référence</span>
</label>
<input
:value="component.reference"
type="text"
placeholder="Référence"
class="input input-bordered input-sm"
@input="updateComponent(index, { reference: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Constructeur</span>
</label>
<ConstructeurSelect
:model-value="component.constructeurId || component.constructeur?.id || null"
placeholder="Rechercher un constructeur..."
@update:modelValue="(value) => setComponentConstructeur(index, value)"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Emplacement</span>
</label>
<input
:value="component.emplacement"
type="text"
placeholder="Emplacement"
class="input input-bordered input-sm"
@input="updateComponent(index, { emplacement: $event.target.value })"
/>
</div>
</div>
<TypeEditCustomFieldsSection
:model-value="component.customFields || []"
:all-expanded="allExpanded"
:expand-all-trigger="componentExpandTrigger"
@update:model-value="(value) => updateComponent(index, { customFields: value })"
/>
<TypeMachinePieceForm
:model-value="component.pieces || []"
:all-expanded="allExpanded"
:expand-all-trigger="componentExpandTrigger"
@update:model-value="(value) => updateComponent(index, { pieces: value })"
/>
</div>
</div>
<div class="flex justify-end">
<button type="button" @click="addComponent" class="btn btn-primary btn-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Ajouter un composant
</button>
</div>
</div>
<div v-else class="flex justify-end">
<button type="button" @click="addComponent" class="btn btn-primary btn-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Ajouter un composant
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import TypeEditCustomFieldsSection from '~/components/TypeEditCustomFieldsSection.vue'
import TypeMachinePieceForm from '~/components/TypeMachinePieceForm.vue'
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
import { useConstructeurs } from '~/composables/useConstructeurs'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
allExpanded: {
type: Boolean,
default: false,
},
expandAllTrigger: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:modelValue'])
const { constructeurs } = useConstructeurs()
const components = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const expanded = ref(false)
const expandedComponents = ref([])
const componentExpandTrigger = computed(() => props.expandAllTrigger)
watch(
() => props.expandAllTrigger,
() => {
expanded.value = props.allExpanded
expandedComponents.value = components.value.map(() => props.allExpanded)
},
{ immediate: true }
)
watch(
() => components.value.length,
(length) => {
expandedComponents.value = Array.from({ length }, (_, index) => expandedComponents.value[index] ?? props.allExpanded)
}
)
const toggleSection = () => {
expanded.value = !expanded.value
}
const ensureComponentState = (index) => {
if (expandedComponents.value[index] === undefined) {
expandedComponents.value[index] = props.allExpanded
}
}
const isComponentExpanded = (index) => {
ensureComponentState(index)
return expandedComponents.value[index]
}
const toggleComponent = (index) => {
ensureComponentState(index)
expandedComponents.value[index] = !expandedComponents.value[index]
}
const createComponent = () => ({
name: '',
reference: '',
constructeur: null,
constructeurId: null,
emplacement: '',
prix: null,
customFields: [],
pieces: [],
})
const addComponent = () => {
components.value = [...components.value, createComponent()]
expanded.value = true
expandedComponents.value.push(true)
}
const removeComponent = (index) => {
components.value = components.value.filter((_, i) => i !== index)
expandedComponents.value.splice(index, 1)
}
const updateComponent = (index, patch) => {
components.value = components.value.map((component, i) =>
i === index ? { ...component, ...patch } : component
)
}
const findConstructeurById = (id) => constructeurs.value.find(item => item.id === id) || null
const hydrateComponents = () => {
components.value.forEach((component) => {
if (component.constructeurId && (!component.constructeur || component.constructeur.id !== component.constructeurId)) {
component.constructeur = findConstructeurById(component.constructeurId)
}
(component.pieces || []).forEach((piece) => {
if (piece.constructeurId && (!piece.constructeur || piece.constructeur.id !== piece.constructeurId)) {
piece.constructeur = findConstructeurById(piece.constructeurId)
}
})
})
}
const setComponentConstructeur = (index, constructeurId) => {
updateComponent(index, {
constructeurId,
constructeur: constructeurId ? findConstructeurById(constructeurId) : null,
})
}
watch(
() => components.value,
() => hydrateComponents(),
{ deep: true }
)
watch(
() => constructeurs.value.length,
() => hydrateComponents()
)
hydrateComponents()
</script>

View File

@@ -0,0 +1,263 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-sm p-1"
@click="toggleSection"
>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': expanded }"
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"></path>
</svg>
</button>
<h3 class="card-title text-lg">Champs personnalisés du type</h3>
<span class="badge badge-primary">{{ fields.length }}</span>
</div>
</div>
<div v-if="expanded" class="space-y-4">
<div
v-for="(field, fieldIndex) in fields"
:key="fieldIndex"
class="border border-gray-200 rounded-lg p-4 bg-gray-50"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-xs p-1"
@click="toggleField(fieldIndex)"
title="Plier / déplier le champ"
>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': isFieldExpanded(fieldIndex) }"
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"></path>
</svg>
</button>
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
<h5 class="text-sm font-medium">Champ personnalisé {{ fieldIndex + 1 }}</h5>
<span v-if="!isFieldExpanded(fieldIndex)" class="text-xs text-gray-500 truncate max-w-[160px]">
{{ field.name || 'Sans nom' }}
</span>
</div>
<button
type="button"
@click="removeField(fieldIndex)"
class="btn btn-square btn-error btn-sm"
title="Supprimer ce champ"
>
<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="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div v-if="isFieldExpanded(fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du champ</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
:value="field.name"
type="text"
placeholder="Nom du champ"
class="input input-bordered input-sm"
required
@input="updateField(fieldIndex, { name: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Type de champ</span>
<span class="label-text-alt text-error">*</span>
</label>
<select
class="select select-bordered select-sm"
required
:value="field.type"
@change="updateField(fieldIndex, { type: $event.target.value })"
>
<option value="">Sélectionner un type</option>
<option value="text">Texte</option>
<option value="number">Nombre</option>
<option value="select">Liste déroulante</option>
<option value="boolean">Oui/Non</option>
<option value="date">Date</option>
</select>
</div>
</div>
<div v-if="isFieldExpanded(fieldIndex)" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
<div class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="field.required"
@change="updateField(fieldIndex, { required: $event.target.checked })"
/>
<span class="text-sm">Champ obligatoire</span>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Valeur par défaut</span>
</label>
<input
:value="field.defaultValue"
type="text"
placeholder="Valeur par défaut"
class="input input-bordered input-sm"
@input="updateField(fieldIndex, { defaultValue: $event.target.value })"
/>
</div>
</div>
<div
v-if="isFieldExpanded(fieldIndex) && field.type === 'select'"
class="mt-3"
>
<label class="label">
<span class="label-text">Options de la liste</span>
<span class="label-text-alt">Une option par ligne</span>
</label>
<textarea
:value="field.optionsText || ''"
placeholder="Option 1&#10;Option 2&#10;Option 3"
class="textarea textarea-bordered textarea-sm w-full h-20"
@input="updateOptions(fieldIndex, $event.target.value)"
></textarea>
</div>
</div>
<div class="flex justify-end">
<button type="button" @click="addField" class="btn btn-primary btn-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Ajouter un champ
</button>
</div>
</div>
<div v-else class="flex justify-end">
<button type="button" @click="addField" class="btn btn-primary btn-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Ajouter un champ
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
allExpanded: {
type: Boolean,
default: false,
},
expandAllTrigger: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:modelValue'])
const fields = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const expanded = ref(false)
const expandedFields = ref([])
watch(
() => props.expandAllTrigger,
() => {
expanded.value = props.allExpanded
expandedFields.value = fields.value.map(() => props.allExpanded)
},
{ immediate: true }
)
watch(
() => fields.value.length,
(length) => {
expandedFields.value = Array.from({ length }, (_, index) => expandedFields.value[index] ?? props.allExpanded)
}
)
const toggleSection = () => {
expanded.value = !expanded.value
}
const ensureFieldState = (index) => {
if (expandedFields.value[index] === undefined) {
expandedFields.value[index] = props.allExpanded
}
}
const isFieldExpanded = (index) => {
ensureFieldState(index)
return expandedFields.value[index]
}
const toggleField = (index) => {
ensureFieldState(index)
expandedFields.value[index] = !expandedFields.value[index]
}
const addField = () => {
fields.value = [
...fields.value,
{
name: '',
type: '',
required: false,
defaultValue: '',
optionsText: '',
},
]
expandedFields.value.push(true)
expanded.value = true
}
const removeField = (index) => {
fields.value = fields.value.filter((_, i) => i !== index)
expandedFields.value.splice(index, 1)
}
const updateField = (index, patch) => {
fields.value = fields.value.map((field, i) => (i === index ? { ...field, ...patch } : field))
}
const updateOptions = (index, value) => {
updateField(index, {
optionsText: value.replace(/\r\n/g, '\n').replace(/\r/g, '\n'),
})
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost btn-sm p-1"
@click="toggleSection"
>
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': expanded }"
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"></path>
</svg>
</button>
<h3 class="card-title text-lg">Pièces principales</h3>
<span class="badge badge-secondary">{{ pieces.length }}</span>
</div>
</div>
<div v-if="expanded">
<TypeMachinePieceForm
v-model="internalPieces"
:all-expanded="allExpanded"
:expand-all-trigger="expandAllTrigger"
/>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import TypeMachinePieceForm from '~/components/TypeMachinePieceForm.vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
allExpanded: {
type: Boolean,
default: false,
},
expandAllTrigger: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:modelValue'])
const expanded = ref(false)
watch(
() => props.expandAllTrigger,
() => {
expanded.value = props.allExpanded
},
{ immediate: true }
)
const internalPieces = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const pieces = computed(() => internalPieces.value)
const toggleSection = () => {
expanded.value = !expanded.value
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div class="flex justify-end">
<button type="button" class="btn btn-outline btn-sm" @click="$emit('toggle')">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="allExpanded"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 12H6"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v12m6-6H6"
/>
</svg>
{{ allExpanded ? 'Tout plier' : 'Tout déplier' }}
</button>
</div>
</template>
<script setup>
defineProps({
allExpanded: {
type: Boolean,
default: false,
},
})
defineEmits(['toggle'])
</script>

View File

@@ -89,11 +89,10 @@
<label class="label">
<span class="label-text">Constructeur</span>
</label>
<input
v-model="piece.constructeur"
type="text"
placeholder="Constructeur"
class="input input-bordered input-sm"
<ConstructeurSelect
:model-value="piece.constructeurId || piece.constructeur?.id || null"
placeholder="Rechercher un constructeur..."
@update:modelValue="(value) => setPieceConstructeur(index, value)"
/>
</div>
<div class="form-control">
@@ -235,22 +234,44 @@
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
import { useConstructeurs } from '~/composables/useConstructeurs'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
},
allExpanded: {
type: Boolean,
default: false,
},
expandAllTrigger: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:modelValue'])
const { constructeurs } = useConstructeurs()
const pieces = ref(props.modelValue)
watch(() => props.modelValue, (newValue) => {
pieces.value = newValue
initializeExpansionState()
})
watch(
() => props.modelValue,
(newValue) => {
pieces.value = newValue
initializeExpansionState()
hydrateConstructeurs()
}
)
watch(
() => props.expandAllTrigger,
() => {
setAllExpanded(props.allExpanded)
}
)
const allExpanded = ref(false)
const expandedPieces = ref([])
@@ -314,11 +335,12 @@ const toggleAllPieces = () => {
const initializeExpansionState = () => {
clearExpansionState()
setAllExpanded(false)
setAllExpanded(props.allExpanded)
}
onMounted(() => {
initializeExpansionState()
hydrateConstructeurs()
})
// Méthodes pour les pièces
@@ -326,7 +348,8 @@ const addPiece = () => {
pieces.value.push({
name: '',
reference: '',
constructeur: '',
constructeur: null,
constructeurId: null,
emplacement: '',
prix: null,
customFields: []
@@ -382,4 +405,27 @@ const updateFieldOptions = (pieceIndex, fieldIndex) => {
pieces.value[pieceIndex].customFields[fieldIndex].optionsText = pieces.value[pieceIndex].customFields[fieldIndex].optionsText.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}
}
const findConstructeurById = (id) => constructeurs.value.find(item => item.id === id) || null
const hydrateConstructeurs = () => {
pieces.value.forEach((piece) => {
if (piece.constructeurId && (!piece.constructeur || piece.constructeur.id !== piece.constructeurId)) {
piece.constructeur = findConstructeurById(piece.constructeurId)
}
})
}
const setPieceConstructeur = (index, constructeurId) => {
const piece = pieces.value[index]
if (!piece) return
piece.constructeurId = constructeurId
piece.constructeur = constructeurId ? findConstructeurById(constructeurId) : null
emit('update:modelValue', pieces.value)
}
watch(
() => constructeurs.value.length,
() => hydrateConstructeurs()
)
</script>