fix(ui) : remove legacy edit pages and history composables, unify create/edit forms
Consolidate create and edit pages into single create pages with edit mode support. Remove obsolete catalog pages, history composables, and fix remaining code review issues. Include migration to relink orphaned custom fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,357 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<DocumentPreviewModal
|
||||
:document="previewDocument"
|
||||
:visible="previewVisible"
|
||||
:documents="pieceDocuments"
|
||||
@close="closePreview"
|
||||
/>
|
||||
<DocumentEditModal
|
||||
:visible="editModalVisible"
|
||||
:document="editingDocument"
|
||||
@close="editModalVisible = false"
|
||||
@updated="handleDocumentUpdated"
|
||||
/>
|
||||
<main class="container mx-auto px-6 py-10">
|
||||
<div v-if="loading" class="flex flex-col items-center gap-4 py-20 text-center">
|
||||
<span class="loading loading-spinner loading-lg" aria-hidden="true" />
|
||||
<p class="text-sm text-base-content/70">Chargement de la pièce…</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!piece" class="max-w-xl mx-auto">
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<div>
|
||||
<h2 class="font-semibold text-lg">Pièce introuvable</h2>
|
||||
<p class="text-sm text-base-content/80">
|
||||
Nous n'avons pas pu retrouver la pièce demandée. Elle a peut-être été supprimée.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary mt-6" @click="$router.back()">
|
||||
Retour au catalogue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section v-else class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
||||
<div class="card-body space-y-6">
|
||||
<header class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold text-base-content">Modifier la pièce</h1>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Ajustez les informations de la pièce et ses champs personnalisés.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
|
||||
Retour au catalogue
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie de pièce</span>
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedTypeId"
|
||||
class="select select-bordered select-sm md:select-md"
|
||||
disabled
|
||||
>
|
||||
<option value="">Sélectionner une catégorie</option>
|
||||
<option
|
||||
v-for="type in pieceTypeList"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
La catégorie d'origine ne peut pas être modifiée depuis cette page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom de la pièce</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="editionForm.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || saving"
|
||||
placeholder="Nom affiché dans le catalogue"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="editionForm.description"
|
||||
class="textarea textarea-bordered textarea-sm md:textarea-md"
|
||||
:disabled="!canEdit || saving"
|
||||
placeholder="Description de la pièce (optionnel)"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="editionForm.reference"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || saving"
|
||||
placeholder="Référence interne ou fournisseur"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="piece?.referenceAuto" class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence auto</span>
|
||||
</label>
|
||||
<input
|
||||
:value="piece.referenceAuto"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md bg-base-200"
|
||||
disabled
|
||||
title="Générée automatiquement à partir du type et des champs personnalisés"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Fournisseur</span>
|
||||
</label>
|
||||
<ConstructeurSelect
|
||||
v-model="editionForm.constructeurIds"
|
||||
class="w-full"
|
||||
:disabled="!canEdit || saving"
|
||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||
:initial-options="piece?.constructeurs || []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Prix indicatif (€)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="editionForm.prix"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || saving"
|
||||
placeholder="Valeur indicatrice"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="structureProducts.length"
|
||||
class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4"
|
||||
>
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">
|
||||
Produit requis par le squelette
|
||||
</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Cette pièce doit rester liée à un produit catalogue répondant aux critères suivants.
|
||||
</p>
|
||||
</header>
|
||||
<ul class="space-y-2 text-sm text-base-content/80">
|
||||
<li
|
||||
v-for="(description, index) in productRequirementDescriptions"
|
||||
:key="`edit-requirement-${index}`"
|
||||
class="flex items-start gap-2"
|
||||
>
|
||||
<span class="mt-0.5 inline-flex h-2 w-2 flex-shrink-0 rounded-full bg-primary"></span>
|
||||
<span>{{ description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
v-for="entry in productRequirementEntries"
|
||||
:key="entry.key"
|
||||
class="form-control"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="label-text text-xs font-medium">
|
||||
{{ entry.label }}
|
||||
</span>
|
||||
</label>
|
||||
<ProductSelect
|
||||
:model-value="productSelections[entry.index] || null"
|
||||
:disabled="!canEdit || saving"
|
||||
:type-product-id="entry.typeProductId"
|
||||
helper-text="Un produit valide est requis pour cette pièce."
|
||||
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StructureSkeletonPreview
|
||||
v-if="selectedType || resolvedStructure"
|
||||
:structure="resolvedStructure"
|
||||
:description="selectedType?.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.'"
|
||||
:preview-badge="formatPieceStructurePreview(resolvedStructure)"
|
||||
variant="piece"
|
||||
/>
|
||||
|
||||
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Mettez à jour les valeurs propres à cette pièce.
|
||||
</p>
|
||||
</header>
|
||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || saving" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="flex flex-col gap-1 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="font-semibold text-base-content">Documents</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Gérez les documents associés à cette pièce.
|
||||
</p>
|
||||
</div>
|
||||
<span v-if="selectedFiles.length" class="badge badge-outline">
|
||||
{{ selectedFiles.length }} document{{ selectedFiles.length > 1 ? 's' : '' }} prêt{{ selectedFiles.length > 1 ? 's' : '' }} à être ajouté{{ selectedFiles.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</header>
|
||||
<div :class="{ 'pointer-events-none opacity-60': !canEdit || saving || uploadingDocuments }">
|
||||
<DocumentUpload
|
||||
v-model="selectedFiles"
|
||||
title="Déposer vos fichiers"
|
||||
subtitle="Formats acceptés : PDF, images, documents…"
|
||||
@files-added="handleFilesAdded"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="uploadingDocuments" class="text-xs text-base-content/70">
|
||||
Téléversement des documents en cours…
|
||||
</p>
|
||||
<p v-else-if="loadingDocuments" class="text-xs text-base-content/70">
|
||||
Chargement des documents en cours…
|
||||
</p>
|
||||
<DocumentListInline
|
||||
v-else
|
||||
:documents="pieceDocuments"
|
||||
:can-delete="canEdit"
|
||||
:can-edit="true"
|
||||
:delete-disabled="uploadingDocuments"
|
||||
empty-text="Aucun document n'est associé à cette pièce pour le moment."
|
||||
@preview="openPreview"
|
||||
@edit="openEditModal"
|
||||
@delete="removeDocument"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EntityHistorySection
|
||||
:entries="history"
|
||||
:loading="historyLoading"
|
||||
:error="historyError"
|
||||
:field-labels="historyFieldLabels"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-3 md:flex-row md:justify-end">
|
||||
<NuxtLink to="/pieces-catalog" class="btn btn-ghost" :class="{ 'btn-disabled': saving }">
|
||||
Annuler
|
||||
</NuxtLink>
|
||||
<button type="button" class="btn btn-primary" :disabled="!canSubmit" @click="submitEdition">
|
||||
<span v-if="saving" class="loading loading-spinner loading-sm mr-2" />
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="mt-4">
|
||||
<CommentSection
|
||||
entity-type="piece"
|
||||
:entity-id="String(route.params.id)"
|
||||
:entity-name="piece?.name"
|
||||
show-resolved
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from '#imports'
|
||||
import { usePieceEdit } from '~/composables/usePieceEdit'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
|
||||
const route = useRoute()
|
||||
const { updateDocument } = useDocuments()
|
||||
|
||||
const {
|
||||
piece,
|
||||
loading,
|
||||
saving,
|
||||
selectedFiles,
|
||||
uploadingDocuments,
|
||||
loadingDocuments,
|
||||
pieceDocuments,
|
||||
previewDocument,
|
||||
previewVisible,
|
||||
selectedTypeId,
|
||||
editionForm,
|
||||
productSelections,
|
||||
customFieldInputs,
|
||||
canEdit,
|
||||
pieceTypeList,
|
||||
selectedType,
|
||||
resolvedStructure,
|
||||
structureProducts,
|
||||
productRequirementDescriptions,
|
||||
productRequirementEntries,
|
||||
canSubmit,
|
||||
historyFieldLabels,
|
||||
history,
|
||||
historyLoading,
|
||||
historyError,
|
||||
openPreview,
|
||||
closePreview,
|
||||
removeDocument,
|
||||
handleFilesAdded,
|
||||
setProductSelection,
|
||||
submitEdition,
|
||||
formatPieceStructurePreview,
|
||||
} = usePieceEdit(String(route.params.id))
|
||||
|
||||
const editingDocument = ref<any | null>(null)
|
||||
const editModalVisible = ref(false)
|
||||
|
||||
const openEditModal = (doc: any) => {
|
||||
editingDocument.value = doc
|
||||
editModalVisible.value = true
|
||||
}
|
||||
const handleDocumentUpdated = async (data: { name?: string; type?: string }) => {
|
||||
if (!editingDocument.value?.id) return
|
||||
const result = await updateDocument(editingDocument.value.id, data)
|
||||
if (result.success) {
|
||||
const idx = pieceDocuments.value.findIndex((d: any) => d.id === editingDocument.value?.id)
|
||||
if (idx !== -1) {
|
||||
pieceDocuments.value[idx] = { ...pieceDocuments.value[idx], ...data }
|
||||
}
|
||||
}
|
||||
editModalVisible.value = false
|
||||
editingDocument.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -1,206 +1,235 @@
|
||||
<template>
|
||||
<main class="mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-3xl font-semibold text-base-content">Nouvelle pièce</h1>
|
||||
<p class="text-sm text-base-content/70">
|
||||
Choisissez la catégorie adaptée puis renseignez toutes les informations de votre pièce.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost btn-sm md:btn-md self-start" @click="$router.back()">
|
||||
Retour au catalogue
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="card border border-base-200 bg-base-100 shadow-sm">
|
||||
<main class="container mx-auto px-6 py-10">
|
||||
<section class="card border border-base-200 bg-base-100 shadow-sm max-w-5xl mx-auto">
|
||||
<div class="card-body space-y-6">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie de pièce</span>
|
||||
</label>
|
||||
<SearchSelect
|
||||
v-model="selectedTypeId"
|
||||
:options="pieceTypeList"
|
||||
:loading="loadingTypes"
|
||||
size="sm"
|
||||
placeholder="Rechercher une catégorie..."
|
||||
empty-text="Aucune catégorie disponible"
|
||||
:option-label="typeOptionLabel"
|
||||
:option-description="typeOptionDescription"
|
||||
:disabled="!canEdit || loadingTypes || submitting"
|
||||
/>
|
||||
<p v-if="loadingTypes" class="text-xs text-gray-500 mt-1">
|
||||
Chargement des catégories…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom de la pièce</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Nom affiché dans le catalogue"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="creationForm.description"
|
||||
class="textarea textarea-bordered textarea-sm md:textarea-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Description de la pièce (optionnel)"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.reference"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Référence interne ou fournisseur"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Fournisseur</span>
|
||||
</label>
|
||||
<ConstructeurSelect
|
||||
v-model="creationForm.constructeurIds"
|
||||
class="w-full"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConstructeurLinksTable
|
||||
v-if="constructeurLinks.length"
|
||||
v-model="constructeurLinks"
|
||||
<DetailHeader
|
||||
title="Nouvelle pièce"
|
||||
subtitle="Choisissez la catégorie adaptée puis renseignez toutes les informations de votre pièce."
|
||||
:is-edit-mode="false"
|
||||
:can-edit="false"
|
||||
back-link="/catalogues/pieces"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Prix indicatif (€)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.prix"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Valeur indicatrice"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<EntityTabs v-model="activeTab" :tabs="entityTabs" aria-label="Sections de la pièce">
|
||||
<template #tab-general>
|
||||
<div class="space-y-6">
|
||||
<!-- Catégorie -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie de pièce</span>
|
||||
</label>
|
||||
<SearchSelect
|
||||
v-model="selectedTypeId"
|
||||
:options="pieceTypeList"
|
||||
:loading="loadingTypes"
|
||||
size="sm"
|
||||
placeholder="Rechercher une catégorie..."
|
||||
empty-text="Aucune catégorie disponible"
|
||||
:option-label="typeOptionLabel"
|
||||
:option-description="typeOptionDescription"
|
||||
:disabled="!canEdit || loadingTypes || submitting"
|
||||
/>
|
||||
<p v-if="loadingTypes" class="text-xs text-gray-500 mt-1">
|
||||
Chargement des catégories…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="structureProducts.length"
|
||||
class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4"
|
||||
>
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">
|
||||
Produit requis par le squelette
|
||||
</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Sélectionnez un produit catalogue compatible avec les exigences ci-dessous.
|
||||
</p>
|
||||
</header>
|
||||
<ul class="space-y-2 text-sm text-base-content/80">
|
||||
<li
|
||||
v-for="(description, index) in productRequirementDescriptions"
|
||||
:key="`requirement-${index}`"
|
||||
class="flex items-start gap-2"
|
||||
>
|
||||
<span class="mt-0.5 inline-flex h-2 w-2 flex-shrink-0 rounded-full bg-primary"></span>
|
||||
<span>{{ description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
v-for="entry in productRequirementEntries"
|
||||
:key="entry.key"
|
||||
class="form-control"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="label-text text-xs font-medium">
|
||||
{{ entry.label }}
|
||||
</span>
|
||||
</label>
|
||||
<ProductSelect
|
||||
:model-value="productSelections[entry.index] || null"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
:type-product-id="entry.typeProductId"
|
||||
helper-text="Un produit est requis pour cette pièce."
|
||||
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||
<!-- Nom -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Nom de la pièce</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.name"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Nom affiché dans le catalogue"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="creationForm.description"
|
||||
class="textarea textarea-bordered textarea-sm md:textarea-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Description de la pièce (optionnel)"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Référence + Fournisseurs -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.reference"
|
||||
type="text"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Référence interne ou fournisseur"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Fournisseur</span>
|
||||
</label>
|
||||
<ConstructeurSelect
|
||||
v-model="creationForm.constructeurIds"
|
||||
class="w-full"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConstructeurLinksTable
|
||||
v-if="constructeurLinks.length"
|
||||
v-model="constructeurLinks"
|
||||
/>
|
||||
|
||||
<!-- Prix -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Prix indicatif (€)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="creationForm.prix"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
placeholder="Valeur indicatrice"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skeleton preview -->
|
||||
<StructureSkeletonPreview
|
||||
v-if="selectedType"
|
||||
:structure="selectedType.structure"
|
||||
:description="selectedType.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.'"
|
||||
:preview-badge="formatPieceStructurePreview(selectedType.structure)"
|
||||
variant="piece"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<StructureSkeletonPreview
|
||||
v-if="selectedType"
|
||||
:structure="selectedType.structure"
|
||||
:description="selectedType.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.'"
|
||||
:preview-badge="formatPieceStructurePreview(selectedType.structure)"
|
||||
variant="piece"
|
||||
/>
|
||||
<template #tab-products>
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-if="structureProducts.length"
|
||||
class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4"
|
||||
>
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">
|
||||
Produit requis par le squelette
|
||||
</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Sélectionnez un produit catalogue compatible avec les exigences ci-dessous.
|
||||
</p>
|
||||
</header>
|
||||
<ul class="space-y-2 text-sm text-base-content/80">
|
||||
<li
|
||||
v-for="(description, index) in productRequirementDescriptions"
|
||||
:key="`requirement-${index}`"
|
||||
class="flex items-start gap-2"
|
||||
>
|
||||
<span class="mt-0.5 inline-flex h-2 w-2 flex-shrink-0 rounded-full bg-primary"></span>
|
||||
<span>{{ description }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
v-for="entry in productRequirementEntries"
|
||||
:key="entry.key"
|
||||
class="form-control"
|
||||
>
|
||||
<label class="label">
|
||||
<span class="label-text text-xs font-medium">
|
||||
{{ entry.label }}
|
||||
</span>
|
||||
</label>
|
||||
<ProductSelect
|
||||
:model-value="productSelections[entry.index] || null"
|
||||
:disabled="!canEdit || submitting || !selectedType"
|
||||
:type-product-id="entry.typeProductId"
|
||||
helper-text="Un produit est requis pour cette pièce."
|
||||
@update:model-value="(value) => setProductSelection(entry.index, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Renseignez les valeurs propres à cette pièce. Ces champs complètent le squelette sélectionné.
|
||||
</p>
|
||||
</header>
|
||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
|
||||
</div>
|
||||
<EmptyState
|
||||
v-if="!structureProducts.length"
|
||||
title="Aucun produit requis"
|
||||
:description="selectedType ? 'Cette catégorie ne requiert pas de produit lié.' : 'Sélectionnez une catégorie pour voir les produits requis.'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="flex flex-col gap-1 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="font-semibold text-base-content">Documents</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Ajoutez des documents (PDF, images, textes…) liés à cette pièce.
|
||||
<template #tab-documents>
|
||||
<div class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="flex flex-col gap-1 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="font-semibold text-base-content">Documents</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Ajoutez des documents (PDF, images, textes…) liés à cette pièce.
|
||||
</p>
|
||||
</div>
|
||||
<span v-if="selectedDocuments.length" class="badge badge-outline">
|
||||
{{ selectedDocuments.length }} document{{ selectedDocuments.length > 1 ? 's' : '' }} prêt{{ selectedDocuments.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</header>
|
||||
<div :class="{ 'pointer-events-none opacity-60': !canEdit || submitting }">
|
||||
<DocumentUpload
|
||||
v-model="selectedDocuments"
|
||||
title="Déposer vos fichiers"
|
||||
subtitle="Formats acceptés : PDF, images, documents…"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="uploadingDocuments" class="text-xs text-base-content/70">
|
||||
Téléversement des documents en cours…
|
||||
</p>
|
||||
</div>
|
||||
<span v-if="selectedDocuments.length" class="badge badge-outline">
|
||||
{{ selectedDocuments.length }} document{{ selectedDocuments.length > 1 ? 's' : '' }} prêt{{ selectedDocuments.length > 1 ? 's' : '' }} à être ajouté{{ selectedDocuments.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</header>
|
||||
<div :class="{ 'pointer-events-none opacity-60': !canEdit || submitting }">
|
||||
<DocumentUpload
|
||||
v-model="selectedDocuments"
|
||||
title="Déposer vos fichiers"
|
||||
subtitle="Formats acceptés : PDF, images, documents…"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="uploadingDocuments" class="text-xs text-base-content/70">
|
||||
Téléversement des documents en cours…
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #tab-custom-fields>
|
||||
<div v-if="customFieldInputs.length" class="space-y-4 rounded-lg border border-base-200 bg-base-200/40 p-4">
|
||||
<header class="space-y-1">
|
||||
<h2 class="font-semibold text-base-content">Champs personnalisés</h2>
|
||||
<p class="text-xs text-base-content/70">
|
||||
Renseignez les valeurs propres à cette pièce. Ces champs complètent le squelette sélectionné.
|
||||
</p>
|
||||
</header>
|
||||
<CustomFieldInputGrid :fields="customFieldInputs" :disabled="!canEdit || submitting" />
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
title="Aucun champ personnalisé"
|
||||
:description="selectedType ? 'Cette catégorie ne définit pas de champs personnalisés.' : 'Sélectionnez une catégorie pour voir les champs personnalisés.'"
|
||||
/>
|
||||
</template>
|
||||
</EntityTabs>
|
||||
|
||||
<!-- Save/Cancel buttons -->
|
||||
<div class="flex flex-col gap-3 md:flex-row md:justify-end">
|
||||
<NuxtLink to="/pieces-catalog" class="btn btn-ghost" :class="{ 'btn-disabled': submitting }">
|
||||
<NuxtLink to="/catalogues/pieces" class="btn btn-ghost" :class="{ 'btn-disabled': submitting }">
|
||||
Annuler
|
||||
</NuxtLink>
|
||||
<button type="button" class="btn btn-primary" :disabled="!canSubmit" @click="submitCreation">
|
||||
@@ -260,6 +289,7 @@ const { syncLinks } = useConstructeurLinks()
|
||||
const { getConstructeurById } = useConstructeurs()
|
||||
const { canEdit } = usePermissions()
|
||||
|
||||
const activeTab = ref('general')
|
||||
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
||||
const selectedTypeId = ref<string>(initialTypeId.value)
|
||||
const submitting = ref(false)
|
||||
@@ -382,6 +412,13 @@ const canSubmit = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const entityTabs = computed(() => [
|
||||
{ key: 'general', label: 'Général' },
|
||||
{ key: 'products', label: 'Produits liés', count: structureProducts.value.length },
|
||||
{ key: 'documents', label: 'Documents', count: selectedDocuments.value.length },
|
||||
{ key: 'custom-fields', label: 'Champs perso', count: customFieldInputs.value.length },
|
||||
])
|
||||
|
||||
const clearCreationForm = () => {
|
||||
creationForm.name = ''
|
||||
creationForm.description = ''
|
||||
@@ -508,5 +545,4 @@ watch(
|
||||
onMounted(async () => {
|
||||
await loadPieceTypes()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user