feat: add profile management flow
This commit is contained in:
136
app/components/MachinePrintSelectionModal.vue
Normal file
136
app/components/MachinePrintSelectionModal.vue
Normal 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>
|
||||
63
app/components/MachinePrintSelectionNode.vue
Normal file
63
app/components/MachinePrintSelectionNode.vue
Normal 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>
|
||||
79
app/components/PageHero.vue
Normal file
79
app/components/PageHero.vue
Normal 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>
|
||||
37
app/components/TypeEditActionsBar.vue
Normal file
37
app/components/TypeEditActionsBar.vue
Normal 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>
|
||||
103
app/components/TypeEditBaseInfoSection.vue
Normal file
103
app/components/TypeEditBaseInfoSection.vue
Normal 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>
|
||||
294
app/components/TypeEditComponentsSection.vue
Normal file
294
app/components/TypeEditComponentsSection.vue
Normal 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>
|
||||
263
app/components/TypeEditCustomFieldsSection.vue
Normal file
263
app/components/TypeEditCustomFieldsSection.vue
Normal 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 Option 2 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
78
app/components/TypeEditMachinePiecesSection.vue
Normal file
78
app/components/TypeEditMachinePiecesSection.vue
Normal 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>
|
||||
34
app/components/TypeEditToolbar.vue
Normal file
34
app/components/TypeEditToolbar.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user