+
+
Produits associés
- Produits sélectionnés directement pour cette machine selon le squelette.
+ Produits sélectionnés directement pour cette machine.
-
- {{ products.length }} produit{{ products.length > 1 ? 's' : '' }}
-
+
+
+
+ {{ products.length }} produit{{ products.length > 1 ? 's' : '' }}
+
+
+
{{ product.name }}
-
+
{{ product.groupLabel }}
@@ -39,6 +65,53 @@
Prix indicatif :
{{ product.priceLabel }}
+
+
+
+
Documents :
+
+
+
+
+
+
+
{{ doc.name }}
+
+ {{ doc.mimeType || 'Inconnu' }} • {{ formatSize(doc.size) }}
+
+
+
+
+
+
+
+
+
@@ -49,14 +122,53 @@
diff --git a/app/components/machine/MachineSkeletonSummary.vue b/app/components/machine/MachineSkeletonSummary.vue
deleted file mode 100644
index b38c860..0000000
--- a/app/components/machine/MachineSkeletonSummary.vue
+++ /dev/null
@@ -1,193 +0,0 @@
-
-
-
-
-
Structure sélectionnée
-
- Synthèse des familles définies dans le type et des modèles utilisés pour cette machine.
-
-
-
-
-
-
Composants
-
-
-
-
- {{ group.requirement.label || group.requirement.typeComposant?.name || 'Famille de composants' }}
-
-
- Type : {{ group.requirement.typeComposant?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
-
-
-
{{ group.components.length }} composant(s)
-
-
-
-
-
{{ component.name }}
-
- (Sous-composant)
-
-
-
- {{ field.label }} :
- {{ field.value }}
-
-
-
-
-
-
Aucun composant rattaché à ce groupe.
-
-
-
-
-
-
Pièces principales
-
-
-
-
- {{ group.requirement.label || group.requirement.typePiece?.name || 'Groupe de pièces' }}
-
-
- Type : {{ group.requirement.typePiece?.name || 'Non défini' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
-
-
-
{{ group.pieces.length }} pièce(s)
-
-
-
-
-
{{ piece.name }}
-
- (Rattachée à {{ piece.parentComponentName }})
-
-
-
- {{ field.label }} :
- {{ field.value }}
-
-
-
-
-
-
Aucune pièce rattachée à ce groupe.
-
-
-
-
-
-
Produits requis
-
-
-
-
- {{ group.requirement.label || group.requirement.typeProduct?.name || 'Groupe de produits' }}
-
-
- Catégorie : {{ group.requirement.typeProduct?.name || 'Non définie' }} · Min {{ group.requirement.minCount ?? (group.requirement.required ? 1 : 0) }} · Max {{ group.requirement.maxCount ?? '∞' }}
-
-
-
- Total {{ group.totalCount }}
- Direct {{ group.directProducts.length }}
-
-
-
-
- Via composants : {{ group.componentCount }} • Via pièces : {{ group.pieceCount }}
-
-
-
-
-
{{ product.name }}
-
- Référence : {{ product.reference }}
-
-
- Fournisseurs : {{ product.supplierLabel }}
-
-
- Prix indicatif : {{ product.priceLabel }}
-
-
-
-
- Aucune sélection directe. Couverture assurée via composants ou pièces associés.
-
-
-
-
-
-
-
-
diff --git a/app/components/machine/create/MachineCreatePreview.vue b/app/components/machine/create/MachineCreatePreview.vue
deleted file mode 100644
index b50dcd6..0000000
--- a/app/components/machine/create/MachineCreatePreview.vue
+++ /dev/null
@@ -1,205 +0,0 @@
-
-
-
-
-
-
-
- Prévisualisation avant création
-
-
- {{ preview.status === 'ready' ? 'Prête à créer' : preview.status === 'warning' ? 'À compléter' : 'Bloquante' }}
-
-
-
-
-
- {{ field.label }}
-
- {{ field.display }}
-
-
-
-
-
- Type : {{ preview.type.name }}
- Catégorie : {{ preview.type.category }}
- Structure JSON : {{ preview.type.hasStructuredDefinition ? 'Oui' : 'Legacy' }}
-
-
-
-
-
- Informations générales incomplètes :
-
-
- -
-
-
-
-
-
-
-
-
- Composants hérités
-
-
-
-
- Aucun composant n'est requis pour ce type de machine.
-
-
-
-
-
- Pièces associées
-
-
-
-
- Aucun groupe de pièces à configurer pour ce type.
-
-
-
-
-
- Produits requis
-
-
-
-
-
- {{ group.label }}
-
-
- Catégorie : {{ group.typeName }} · Min {{ group.min }} ·
- {{ group.max !== null ? `Max ${group.max}` : 'Max ∞' }}
-
-
-
-
- Couverture : {{ group.count }}
-
-
- Direct {{ group.completed }} / {{ group.total || 0 }}
-
-
-
-
-
-
- -
- {{ issue.message }}
-
-
-
-
-
- -
-
-
-
- {{ entry.title }}
-
-
- {{ entry.subtitle }}
-
-
-
-
-
-
- Couverture assurée via composants ou pièces liés.
-
-
-
-
-
-
-
-
-
-
- Points à vérifier avant la création :
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/components/machine/create/PreviewRequirementGroup.vue b/app/components/machine/create/PreviewRequirementGroup.vue
deleted file mode 100644
index 69013a2..0000000
--- a/app/components/machine/create/PreviewRequirementGroup.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
- {{ group.label }}
-
-
- Type : {{ group.typeName }} · Min {{ group.min }} ·
- {{ group.max !== null ? `Max ${group.max}` : 'Max ∞' }}
-
-
-
- {{ group.completed }} / {{ group.total || 0 }} complétée(s)
-
-
-
-
-
- -
- {{ issue.message }}
-
-
-
-
-
- -
-
-
-
- {{ entry.title }}
-
-
- {{ entry.subtitle }}
-
-
-
-
-
-
-
-
diff --git a/app/components/machine/create/RequirementComponentSelector.vue b/app/components/machine/create/RequirementComponentSelector.vue
deleted file mode 100644
index 0f5a157..0000000
--- a/app/components/machine/create/RequirementComponentSelector.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-
-
-
- Sélection des composants
-
-
-
-
-
-
- {{ requirement.label || requirement.typeComposant?.name || 'Famille de composants' }}
-
-
- Type : {{ requirement.typeComposant?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
- · Max : {{ requirement.maxCount ?? '∞' }}
-
-
-
-
-
-
-
- Aucun composant sélectionné pour ce groupe.
-
-
-
-
-
- Type appliqué :
- {{ resolveTypeLabel(requirement, entry) }}
-
-
-
-
-
-
-
-
-
-
-
- Aucun composant disponible pour cette famille.
-
-
-
-
-
- {{ findById(entry.composantId)?.name || "Composant" }}
-
-
- Référence : {{ findById(entry.composantId)?.reference || "—" }}
-
-
- Fournisseur :
- {{ findById(entry.composantId)?.constructeur?.name || findById(entry.composantId)?.constructeurName || "—" }}
-
-
-
-
-
-
-
-
-
diff --git a/app/components/machine/create/RequirementPieceSelector.vue b/app/components/machine/create/RequirementPieceSelector.vue
deleted file mode 100644
index 8b0952e..0000000
--- a/app/components/machine/create/RequirementPieceSelector.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
- Sélection des pièces principales
-
-
-
-
-
-
- {{ requirement.label || requirement.typePiece?.name || 'Groupe de pièces' }}
-
-
- Type : {{ requirement.typePiece?.name || 'Non défini' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
- · Max : {{ requirement.maxCount ?? '∞' }}
-
-
-
-
-
-
-
- Aucune pièce sélectionnée pour ce groupe.
-
-
-
-
-
- Type appliqué :
- {{ resolveTypeLabel(requirement, entry) }}
-
-
-
-
-
-
-
-
- $emit('search', requirement, entryIndex, term)"
- @update:modelValue="$emit('set-piece', requirement, entryIndex, $event || '')"
- />
-
-
- Aucune pièce disponible pour cette famille.
-
-
-
-
-
- {{ findById(entry.pieceId)?.name || "Pièce" }}
-
-
- Référence : {{ findById(entry.pieceId)?.reference || "—" }}
-
-
- Fournisseur :
- {{ findById(entry.pieceId)?.constructeur?.name || findById(entry.pieceId)?.constructeurName || "—" }}
-
-
-
-
-
-
-
-
-
diff --git a/app/components/machine/create/RequirementProductSelector.vue b/app/components/machine/create/RequirementProductSelector.vue
deleted file mode 100644
index 5c5e026..0000000
--- a/app/components/machine/create/RequirementProductSelector.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
-
- Produits catalogue requis
-
-
-
-
-
-
- {{ requirement.label || requirement.typeProduct?.name || 'Groupe de produits' }}
-
-
- Catégorie : {{ requirement.typeProduct?.name || 'Non définie' }} · Min : {{ requirement.minCount ?? (requirement.required ? 1 : 0) }}
- · Max : {{ requirement.maxCount ?? '∞' }}
-
-
- Sélection de produits existants uniquement.
-
-
-
-
-
-
-
- Aucun produit sélectionné pour ce groupe.
-
-
-
-
-
- Catégorie appliquée :
- {{ requirement.typeProduct?.name || 'Non définie' }}
-
-
-
-
-
-
-
-
- Aucun produit existant pour cette catégorie. Créez-en un depuis le catalogue.
-
-
-
-
-
- {{ findById(entry.productId)?.name || 'Produit' }}
-
-
- Référence : {{ findById(entry.productId)?.reference || "—" }}
-
-
- Prix indicatif :
-
- {{ Number(findById(entry.productId)?.supplierPrice).toFixed(2) }} €
-
-
- —
-
-
-
- Fournisseurs :
-
- {{ findById(entry.productId)?.constructeurs.map((constructeur: any) => constructeur?.name).filter(Boolean).join(', ') }}
-
-
- —
-
-
-
-
-
-
-
-
-
-
diff --git a/app/composables/useMachineCreatePage.ts b/app/composables/useMachineCreatePage.ts
index 6db7a74..5bf49b7 100644
--- a/app/composables/useMachineCreatePage.ts
+++ b/app/composables/useMachineCreatePage.ts
@@ -1,47 +1,23 @@
/**
* Machine creation page – orchestration composable.
*
- * Consolidates entity lookup maps, option filters, label helpers,
- * template wrappers, and the finalization logic that were previously
- * inlined in pages/machines/new.vue.
+ * Simplified: no more TypeMachine / skeleton system.
+ * Supports direct creation or cloning from an existing machine.
*/
-import { ref, reactive, computed, watch, onMounted } from 'vue'
+import { ref, reactive, onMounted } from 'vue'
import { useMachines } from '~/composables/useMachines'
import { useSites } from '~/composables/useSites'
-import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
-import { useComposants } from '~/composables/useComposants'
-import { usePieces } from '~/composables/usePieces'
-import { useProducts } from '~/composables/useProducts'
-import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
-import { useMachineCreateSelections } from '~/composables/useMachineCreateSelections'
-import {
- useMachineCreatePreview,
- validateRequirementSelections as _validateRequirementSelections,
- resolveComponentRequirementTypeLabel as _resolveComponentRequirementTypeLabel,
- resolvePieceRequirementTypeLabel as _resolvePieceRequirementTypeLabel,
-} from '~/composables/useMachineCreatePreview'
-import {
- getComponentMachineAssignments,
- getPieceMachineAssignments,
- getPieceComponentAssignments,
- formatAssignmentList,
-} from '~/shared/utils/assignmentUtils'
export function useMachineCreatePage() {
// ---------------------------------------------------------------------------
// Composable calls
// ---------------------------------------------------------------------------
- const { createMachine, createMachineFromType, reconfigureSkeleton, addMissingCustomFields, deleteMachine } = useMachines()
+ const { machines, loadMachines, createMachine, cloneMachine } = useMachines()
const { sites, loadSites } = useSites()
- const { machineTypes, loadMachineTypes, loading: machineTypesLoading } = useMachineTypesApi()
- const { composants, loadComposants, loading: composantsLoading } = useComposants()
- const { pieces, loadPieces, loading: piecesLoading } = usePieces()
- const { products, loadProducts, loading: productsLoading } = useProducts()
- const { get } = useApi()
const toast = useToast()
// ---------------------------------------------------------------------------
@@ -49,322 +25,60 @@ export function useMachineCreatePage() {
// ---------------------------------------------------------------------------
const submitting = ref(false)
+ const loading = ref(true)
const newMachine = reactive({
name: '',
siteId: '',
- typeMachineId: '',
reference: '',
+ cloneFromMachineId: '',
})
- const selectedMachineType = computed(() => {
- if (!newMachine.typeMachineId) return null
- return (machineTypes as any).value.find((type: any) => type.id === newMachine.typeMachineId) || null
- })
-
- // ---------------------------------------------------------------------------
- // Entity lookup maps
- // ---------------------------------------------------------------------------
-
- const componentById = computed(() => {
- const map = new Map()
- ;((composants as any).value || []).forEach((component: any) => {
- if (component?.id) map.set(component.id, component)
- })
- return map
- })
-
- const pieceById = computed(() => {
- const map = new Map()
- ;((pieces as any).value || []).forEach((piece: any) => {
- if (piece?.id) map.set(piece.id, piece)
- })
- return map
- })
-
- const componentInventory = computed(() => (composants as any).value || [])
- const pieceInventory = computed(() => (pieces as any).value || [])
- const productInventory = computed(() => (products as any).value || [])
-
- const productById = computed(() => {
- const map = new Map()
- ;(productInventory.value || []).forEach((product: any) => {
- if (product?.id) map.set(product.id, product)
- })
- return map
- })
-
- // ---------------------------------------------------------------------------
- // Entity finders
- // ---------------------------------------------------------------------------
-
- const findComponentById = (id: string) => {
- if (!id) return null
- return componentById.value.get(id) || null
- }
-
- const findPieceById = (id: string): any => {
- if (!id) return null
- return pieceById.value.get(id) || findPieceInCachedOptions(id) || null
- }
-
- const findProductById = (id: string) => {
- if (!id) return null
- return productById.value.get(id) || null
- }
-
- // ---------------------------------------------------------------------------
- // Selection state (from composable)
- // ---------------------------------------------------------------------------
-
- const {
- pieceOptionsByKey,
- pieceLoadingByKey,
- selectedPieceIds,
- getPieceKey,
- findPieceInCachedOptions,
- fetchPieceOptions,
- getComponentRequirementEntries,
- getPieceRequirementEntries,
- getProductRequirementEntries,
- addComponentSelectionEntry,
- removeComponentSelectionEntry,
- addPieceSelectionEntry,
- removePieceSelectionEntry,
- addProductSelectionEntry,
- removeProductSelectionEntry,
- setComponentRequirementComponent,
- setPieceRequirementPiece,
- setProductRequirementProduct: _setProductRequirementProduct,
- clearRequirementSelections,
- initializeRequirementSelections,
- } = useMachineCreateSelections({
- findComponentById,
- findPieceById,
- pieces: pieces as any,
- get: get as any,
- toast,
- })
-
- // ---------------------------------------------------------------------------
- // Preview / validation (from composable)
- // ---------------------------------------------------------------------------
-
- const { machinePreview, blockingPreviewIssues, canCreateMachine } = useMachineCreatePreview({
- newMachine,
- sites: sites as any,
- selectedMachineType,
- findComponentById,
- findPieceById,
- findProductById,
- getComponentRequirementEntries,
- getPieceRequirementEntries,
- getProductRequirementEntries,
- })
-
- // ---------------------------------------------------------------------------
- // Template wrappers
- // ---------------------------------------------------------------------------
-
- const resolveComponentRequirementTypeLabel = (requirement: any, entry: any) =>
- _resolveComponentRequirementTypeLabel(requirement, entry, findComponentById)
-
- const resolvePieceRequirementTypeLabel = (requirement: any, entry: any) =>
- _resolvePieceRequirementTypeLabel(requirement, entry, findPieceById)
-
- const setProductRequirementProduct = (requirement: any, index: number, productId: string) =>
- _setProductRequirementProduct(requirement, index, productId, findProductById)
-
- const validateRequirementSelections = (type: any) =>
- _validateRequirementSelections(type, {
- newMachine,
- sites: sites as any,
- selectedMachineType,
- findComponentById,
- findPieceById,
- findProductById,
- getComponentRequirementEntries,
- getPieceRequirementEntries,
- getProductRequirementEntries,
- })
-
- // ---------------------------------------------------------------------------
- // Machine type helpers
- // ---------------------------------------------------------------------------
-
- const machineTypeLabel = (type: any) => {
- if (!type) return ''
- return type.name || 'Type de machine'
- }
-
- const machineTypeDescription = (type: any) => {
- if (!type) return ''
- const parts: string[] = []
- if (type.category) parts.push(`Catégorie : ${type.category}`)
- const componentCount = type.componentRequirements?.length ?? 0
- const pieceCount = type.pieceRequirements?.length ?? 0
- const productCount = type.productRequirements?.length ?? 0
- parts.push(
- `${componentCount} composant(s)`,
- `${pieceCount} pièce(s)`,
- `${productCount} produit(s)`,
- )
- return parts.join(' • ')
- }
-
- // ---------------------------------------------------------------------------
- // Option filters
- // ---------------------------------------------------------------------------
-
- const getComponentOptions = (requirement: any, currentEntry: any) => {
- const requirementTypeId = requirement?.typeComposantId || requirement?.typeComposant?.id || null
- return componentInventory.value.filter((component: any) => {
- if (!component?.id) return false
- if (requirementTypeId && component.typeComposantId !== requirementTypeId) {
- return currentEntry?.composantId === component.id
- }
- return true
- })
- }
-
- const getPieceOptions = (requirement: any, currentEntry: any, entryIndex: number) => {
- const key = getPieceKey(requirement, entryIndex)
- const cached = pieceOptionsByKey.value[key]
- if (cached) return cached
- const requirementTypeId = requirement?.typePieceId || requirement?.typePiece?.id || null
- const usedIds = new Set(
- selectedPieceIds.value.filter((id: any) => id && (!currentEntry || id !== currentEntry.pieceId)),
- )
- return pieceInventory.value.filter((piece: any) => {
- if (requirementTypeId && piece.typePieceId !== requirementTypeId) return false
- if (!piece.id) return false
- if (currentEntry?.pieceId === piece.id) return true
- return !usedIds.has(piece.id)
- })
- }
-
- const getProductOptions = (requirement: any) => {
- const requirementTypeId = requirement?.typeProductId || requirement?.typeProduct?.id || null
- return productInventory.value.filter((product: any) => {
- if (!product?.id) return false
- if (!requirementTypeId) return true
- const productTypeId = product.typeProductId || product.typeProduct?.id || null
- return productTypeId === requirementTypeId
- })
- }
-
- // ---------------------------------------------------------------------------
- // Option label / description helpers
- // ---------------------------------------------------------------------------
-
- const componentOptionLabel = (component: any) => component?.name || 'Composant'
-
- const componentOptionDescription = (component: any) => {
- if (!component) return ''
- const parts: string[] = []
- if (component.reference) parts.push(`Réf. ${component.reference}`)
- const constructeurName = component.constructeur?.name || component.constructeurName
- if (constructeurName) parts.push(constructeurName)
- const machineAssignments = getComponentMachineAssignments(component)
- if (machineAssignments.length) parts.push(`Machines: ${formatAssignmentList(machineAssignments)}`)
- const productTypeName = component.product?.typeProduct?.name
- const productLabel = component.product?.name || component.product?.reference
- if (productTypeName || productLabel) parts.push(`Produit: ${productTypeName || productLabel}`)
- return parts.join(' • ')
- }
-
- const pieceOptionLabel = (piece: any) => piece?.name || 'Pièce'
-
- const pieceOptionDescription = (piece: any) => {
- if (!piece) return ''
- const parts: string[] = []
- if (piece.reference) parts.push(`Réf. ${piece.reference}`)
- const constructeurName = piece.constructeur?.name || piece.constructeurName
- if (constructeurName) parts.push(constructeurName)
- const machineAssignments = getPieceMachineAssignments(piece)
- if (machineAssignments.length) parts.push(`Machines: ${formatAssignmentList(machineAssignments)}`)
- const componentAssignments = getPieceComponentAssignments(piece)
- if (componentAssignments.length) parts.push(`Composants: ${formatAssignmentList(componentAssignments)}`)
- const productTypeName = piece.product?.typeProduct?.name
- const productLabel = piece.product?.name || piece.product?.reference
- if (productTypeName || productLabel) parts.push(`Produit: ${productTypeName || productLabel}`)
- return parts.join(' • ')
- }
-
// ---------------------------------------------------------------------------
// Machine creation
// ---------------------------------------------------------------------------
const finalizeMachineCreation = async () => {
if (submitting.value) return
- const type = selectedMachineType.value
- if (!type) {
- toast.showError('Merci de sélectionner un type de machine')
- return
- }
- if (!canCreateMachine.value) {
- toast.showError('Compléter les informations obligatoires avant de créer la machine')
+
+ if (!newMachine.name?.trim()) {
+ toast.showError('Merci de renseigner un nom pour la machine')
return
}
submitting.value = true
try {
- const baseMachineData = {
- name: newMachine.name,
- siteId: newMachine.siteId,
- reference: newMachine.reference,
- typeMachineId: type.id,
+ let result: any
+
+ if (newMachine.cloneFromMachineId) {
+ result = await cloneMachine(newMachine.cloneFromMachineId, {
+ name: newMachine.name,
+ siteId: newMachine.siteId,
+ ...(newMachine.reference ? { reference: newMachine.reference } : {}),
+ })
+ } else {
+ result = await createMachine({
+ name: newMachine.name,
+ siteId: newMachine.siteId || undefined,
+ reference: newMachine.reference || undefined,
+ } as any)
}
- const hasRequirements =
- (type.componentRequirements?.length || 0) > 0 ||
- (type.pieceRequirements?.length || 0) > 0 ||
- (type.productRequirements?.length || 0) > 0
-
- let componentLinks: any[] = []
- let pieceLinks: any[] = []
- let productLinks: any[] = []
-
- if (hasRequirements) {
- const validationResult = validateRequirementSelections(type)
- if (!validationResult.valid) {
- toast.showError(validationResult.error as string)
- return
- }
- componentLinks = validationResult.componentLinks as any[]
- pieceLinks = validationResult.pieceLinks as any[]
- productLinks = validationResult.productLinks as any[]
- }
-
- const result: any = hasRequirements
- ? await createMachine(baseMachineData as any)
- : await createMachineFromType(baseMachineData as any, type)
-
if (result.success) {
const machineId = result.data?.id
- if (hasRequirements && machineId) {
- const skeletonResult: any = await reconfigureSkeleton(machineId, {
- componentLinks,
- pieceLinks,
- productLinks,
- } as any)
- if (!skeletonResult.success) {
- // Rollback: delete the orphaned machine
- await deleteMachine(machineId).catch(() => {})
- toast.showError(skeletonResult.error || 'Impossible d\'enregistrer les pièces/composants. La machine n\'a pas été créée.')
- return
- }
- }
- // Initialize custom fields for the machine type
- if (machineId) {
- await addMissingCustomFields(machineId, { showToast: false }).catch(() => {})
- }
+ || (result.data?.machine as any)?.id
+ || null
+
newMachine.name = ''
newMachine.siteId = ''
- newMachine.typeMachineId = ''
newMachine.reference = ''
- clearRequirementSelections()
- await navigateTo('/machines')
+ newMachine.cloneFromMachineId = ''
+
+ if (machineId) {
+ await navigateTo(`/machine/${machineId}`)
+ } else {
+ await navigateTo('/machines')
+ }
} else if (result.error) {
toast.showError(`Impossible de créer la machine : ${humanizeError(result.error)}`)
}
@@ -376,28 +90,19 @@ export function useMachineCreatePage() {
}
// ---------------------------------------------------------------------------
- // Watchers & lifecycle
+ // Lifecycle
// ---------------------------------------------------------------------------
- watch(
- () => newMachine.typeMachineId,
- (typeId) => {
- clearRequirementSelections()
- if (!typeId) return
- const type = (machineTypes as any).value.find((item: any) => item.id === typeId)
- if (!type) return
- initializeRequirementSelections(type)
- },
- )
-
onMounted(async () => {
- await Promise.all([
- loadSites(),
- loadMachineTypes(),
- loadComposants({ itemsPerPage: 200, force: true }),
- loadPieces({ itemsPerPage: 200, force: true }),
- loadProducts({ itemsPerPage: 200, force: true }),
- ])
+ loading.value = true
+ try {
+ await Promise.all([
+ loadSites(),
+ loadMachines(),
+ ])
+ } finally {
+ loading.value = false
+ }
})
// ---------------------------------------------------------------------------
@@ -406,59 +111,11 @@ export function useMachineCreatePage() {
return {
// State
- submitting,
newMachine,
sites,
- machineTypes,
- machineTypesLoading,
- composantsLoading,
- piecesLoading,
- productsLoading,
- selectedMachineType,
-
- // Selection state
- pieceLoadingByKey,
- getPieceKey,
- fetchPieceOptions,
- getComponentRequirementEntries,
- getPieceRequirementEntries,
- getProductRequirementEntries,
- addComponentSelectionEntry,
- removeComponentSelectionEntry,
- addPieceSelectionEntry,
- removePieceSelectionEntry,
- addProductSelectionEntry,
- removeProductSelectionEntry,
- setComponentRequirementComponent,
- setPieceRequirementPiece,
- setProductRequirementProduct,
-
- // Preview
- machinePreview,
- blockingPreviewIssues,
- canCreateMachine,
-
- // Entity finders
- findComponentById,
- findPieceById,
- findProductById,
-
- // Options
- getComponentOptions,
- getPieceOptions,
- getProductOptions,
-
- // Label helpers
- machineTypeLabel,
- machineTypeDescription,
- componentOptionLabel,
- componentOptionDescription,
- pieceOptionLabel,
- pieceOptionDescription,
-
- // Type label resolvers
- resolveComponentRequirementTypeLabel,
- resolvePieceRequirementTypeLabel,
+ machines,
+ submitting,
+ loading,
// Actions
finalizeMachineCreation,
diff --git a/app/composables/useMachineCreatePreview.ts b/app/composables/useMachineCreatePreview.ts
deleted file mode 100644
index 161e02c..0000000
--- a/app/composables/useMachineCreatePreview.ts
+++ /dev/null
@@ -1,572 +0,0 @@
-/**
- * Machine creation – preview computation and validation.
- *
- * Extracted from pages/machines/new.vue. Builds the live preview model
- * and validates requirement selections before machine creation.
- */
-
-import { computed, type Ref, type ComputedRef } from 'vue'
-import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
-import { extractParentLinkIdentifiers } from '~/shared/utils/productDisplayUtils'
-import {
- getComponentMachineAssignments,
- getPieceMachineAssignments,
- getPieceComponentAssignments,
- formatAssignmentList,
-} from '~/shared/utils/assignmentUtils'
-
-type AnyRecord = Record
-
-export interface MachineCreatePreviewDeps {
- newMachine: { name: string; siteId: string; typeMachineId: string; reference: string }
- sites: Ref
- selectedMachineType: ComputedRef
- findComponentById: (id: string) => AnyRecord | null
- findPieceById: (id: string) => AnyRecord | null
- findProductById: (id: string) => AnyRecord | null
- getComponentRequirementEntries: (requirementId: string) => AnyRecord[]
- getPieceRequirementEntries: (requirementId: string) => AnyRecord[]
- getProductRequirementEntries: (requirementId: string) => AnyRecord[]
-}
-
-// ---------------------------------------------------------------------------
-// Product type ID extractors
-// ---------------------------------------------------------------------------
-
-const getProductTypeIdFromComponent = (component: AnyRecord | null): string | null => {
- if (!component || typeof component !== 'object') return null
- return (
- (component.product as AnyRecord)?.typeProductId ||
- ((component.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
- component.productTypeId ||
- null
- ) as string | null
-}
-
-const getProductTypeIdFromPiece = (piece: AnyRecord | null): string | null => {
- if (!piece || typeof piece !== 'object') return null
- return (
- (piece.product as AnyRecord)?.typeProductId ||
- ((piece.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
- piece.productTypeId ||
- null
- ) as string | null
-}
-
-// ---------------------------------------------------------------------------
-// Status badge helper
-// ---------------------------------------------------------------------------
-
-export const getStatusBadgeClass = (status: string): string => {
- if (status === 'ready') return 'badge-success'
- if (status === 'warning') return 'badge-warning'
- return 'badge-error'
-}
-
-// ---------------------------------------------------------------------------
-// Scroll / issue click helpers
-// ---------------------------------------------------------------------------
-
-const highlightClasses = ['ring', 'ring-primary', 'ring-offset-2']
-
-export const scrollToAnchor = (anchor: string): void => {
- if (!anchor || typeof window === 'undefined' || typeof document === 'undefined') return
- const target = document.getElementById(anchor)
- if (!target) return
- target.scrollIntoView({ behavior: 'smooth', block: 'center' })
- highlightClasses.forEach((cls) => target.classList.add(cls))
- window.setTimeout(() => {
- highlightClasses.forEach((cls) => target.classList.remove(cls))
- }, 1500)
-}
-
-export const handleIssueClick = (issue: AnyRecord): void => {
- if (!issue?.anchor) return
- scrollToAnchor(issue.anchor as string)
-}
-
-// ---------------------------------------------------------------------------
-// Type label resolvers
-// ---------------------------------------------------------------------------
-
-export const resolveComponentRequirementTypeLabel = (
- requirement: AnyRecord,
- entry: AnyRecord,
- findComponentById: (id: string) => AnyRecord | null,
-): string => {
- if (entry?.composantId) {
- const component = findComponentById(entry.composantId as string)
- if ((component?.typeComposant as AnyRecord)?.name) {
- return (component!.typeComposant as AnyRecord).name as string
- }
- }
- return ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini'
-}
-
-export const resolvePieceRequirementTypeLabel = (
- requirement: AnyRecord,
- entry: AnyRecord,
- findPieceById: (id: string) => AnyRecord | null,
-): string => {
- if (entry?.pieceId) {
- const piece = findPieceById(entry.pieceId as string)
- if ((piece?.typePiece as AnyRecord)?.name) {
- return (piece!.typePiece as AnyRecord).name as string
- }
- }
- return ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini'
-}
-
-// ---------------------------------------------------------------------------
-// Product requirement stats
-// ---------------------------------------------------------------------------
-
-const computeProductUsageFromSelections = (
- type: AnyRecord,
- deps: MachineCreatePreviewDeps,
-): Map => {
- const usage = new Map()
-
- const increment = (typeProductId: string | null) => {
- if (!typeProductId) return
- usage.set(typeProductId, (usage.get(typeProductId) ?? 0) + 1)
- }
-
- for (const requirement of (type.componentRequirements || []) as AnyRecord[]) {
- const entries = deps.getComponentRequirementEntries(requirement.id as string)
- entries.forEach((entry) => {
- if (!entry?.composantId) return
- const component = deps.findComponentById(entry.composantId as string)
- increment(getProductTypeIdFromComponent(component))
- })
- }
-
- for (const requirement of (type.pieceRequirements || []) as AnyRecord[]) {
- const entries = deps.getPieceRequirementEntries(requirement.id as string)
- entries.forEach((entry) => {
- if (!entry?.pieceId) return
- const piece = deps.findPieceById(entry.pieceId as string)
- increment(getProductTypeIdFromPiece(piece))
- })
- }
-
- for (const requirement of (type.productRequirements || []) as AnyRecord[]) {
- const entries = deps.getProductRequirementEntries(requirement.id as string)
- entries.forEach((entry) => {
- if (!entry?.productId) return
- const product = deps.findProductById(entry.productId as string)
- const typeProductId = (
- product?.typeProductId ||
- (product?.typeProduct as AnyRecord)?.id ||
- entry?.typeProductId ||
- requirement?.typeProductId ||
- (requirement?.typeProduct as AnyRecord)?.id ||
- null
- ) as string | null
- increment(typeProductId)
- })
- }
-
- return usage
-}
-
-const buildProductRequirementStats = (
- type: AnyRecord,
- deps: MachineCreatePreviewDeps,
-): { stats: AnyRecord[]; usage: Map } => {
- const usage = computeProductUsageFromSelections(type, deps)
-
- const stats = ((type.productRequirements || []) as AnyRecord[]).map((requirement) => {
- const typeProductId = (
- requirement.typeProductId || (requirement.typeProduct as AnyRecord)?.id || null
- ) as string | null
-
- const label = (
- (requirement.label as string)?.trim() ||
- (requirement.typeProduct as AnyRecord)?.name ||
- (requirement.typeProduct as AnyRecord)?.code ||
- 'Produit requis'
- ) as string
-
- const typeName = ((requirement.typeProduct as AnyRecord)?.name || 'Non défini') as string
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const max = (requirement.maxCount ?? null) as number | null
- const count = typeProductId ? usage.get(typeProductId) ?? 0 : 0
-
- const rawEntries = deps.getProductRequirementEntries(requirement.id as string)
- const normalizedEntries = rawEntries.map((entry, index) => {
- const product = entry?.productId ? deps.findProductById(entry.productId as string) : null
- const subtitleParts: string[] = []
- if (product?.reference) subtitleParts.push(`Réf. ${product.reference}`)
- if (product?.supplierPrice !== undefined && product?.supplierPrice !== null) {
- const price = Number(product.supplierPrice)
- if (!Number.isNaN(price)) subtitleParts.push(`${price.toFixed(2)} €`)
- }
- if (Array.isArray(product?.constructeurs) && (product!.constructeurs as AnyRecord[]).length) {
- const cLabel = (product!.constructeurs as AnyRecord[])
- .map((c) => c?.name)
- .filter(Boolean)
- .join(', ')
- if (cLabel) subtitleParts.push(`Fournisseurs: ${cLabel}`)
- }
- return {
- key: `${requirement.id}-${index}`,
- status: product ? 'complete' : 'pending',
- title: (product?.name || product?.reference || `Sélection #${index + 1}`) as string,
- subtitle: subtitleParts.length ? subtitleParts.join(' • ') : null,
- }
- })
-
- const issues: AnyRecord[] = []
- if (count < min) {
- issues.push({
- message: `Le produit "${label}" nécessite au moins ${min} sélection(s). Actuellement ${count}.`,
- kind: 'error',
- anchor: `product-group-${requirement.id}`,
- })
- }
- if (max !== null && count > max) {
- issues.push({
- message: `Le produit "${label}" ne peut pas dépasser ${max} sélection(s). Actuellement ${count}.`,
- kind: 'error',
- anchor: `product-group-${requirement.id}`,
- })
- }
- if (normalizedEntries.length > 0 && normalizedEntries.some((e) => e.status !== 'complete')) {
- issues.push({
- message: 'Sélectionner un produit pour chaque entrée ajoutée.',
- kind: 'error',
- anchor: `product-group-${requirement.id}`,
- })
- }
-
- const completed = normalizedEntries.filter((e) => e.status === 'complete').length
- const total = normalizedEntries.length
- const status = issues.some((i) => i.kind === 'error')
- ? 'error'
- : issues.some((i) => i.kind === 'warning')
- ? 'warning'
- : 'ready'
-
- return {
- id: requirement.id,
- requirement,
- label,
- typeName,
- count,
- min,
- max,
- completed,
- total,
- entries: normalizedEntries,
- issues,
- allowNewModels: requirement.allowNewModels ?? true,
- status,
- }
- })
-
- return { stats, usage }
-}
-
-// ---------------------------------------------------------------------------
-// Validation
-// ---------------------------------------------------------------------------
-
-export const validateRequirementSelections = (
- type: AnyRecord,
- deps: MachineCreatePreviewDeps,
-): AnyRecord => {
- const errors: string[] = []
- const componentLinksPayload: AnyRecord[] = []
- const pieceLinksPayload: AnyRecord[] = []
- const productLinksPayload: AnyRecord[] = []
-
- for (const requirement of (type.componentRequirements || []) as AnyRecord[]) {
- const entries = deps.getComponentRequirementEntries(requirement.id as string)
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const max = (requirement.maxCount ?? null) as number | null
-
- if (entries.length < min) {
- errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" nécessite au moins ${min} élément(s).`)
- }
- if (max !== null && entries.length > max) {
- errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" ne peut dépasser ${max} élément(s).`)
- }
-
- entries.forEach((entry) => {
- if (!entry.composantId) {
- errors.push(`Sélectionner un composant existant pour "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}".`)
- return
- }
- const component = deps.findComponentById(entry.composantId as string)
- if (!component) {
- errors.push(`Le composant sélectionné est introuvable (ID: ${entry.composantId}).`)
- return
- }
- const requiredTypeId = (requirement.typeComposantId || (requirement.typeComposant as AnyRecord)?.id || null) as string | null
- if (requiredTypeId && component.typeComposantId && component.typeComposantId !== requiredTypeId) {
- errors.push(`Le composant "${component.name || component.id}" n'appartient pas à la famille attendue.`)
- return
- }
- const payload: AnyRecord = { requirementId: requirement.id, composantId: entry.composantId }
- const overrides = sanitizeDefinitionOverrides(entry.definition as AnyRecord)
- if (overrides) payload.overrides = overrides
- Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
- componentLinksPayload.push(payload)
- })
- }
-
- for (const requirement of (type.pieceRequirements || []) as AnyRecord[]) {
- const entries = deps.getPieceRequirementEntries(requirement.id as string)
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const max = (requirement.maxCount ?? null) as number | null
-
- if (entries.length < min) {
- errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" nécessite au moins ${min} élément(s).`)
- }
- if (max !== null && entries.length > max) {
- errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" ne peut dépasser ${max} élément(s).`)
- }
-
- entries.forEach((entry) => {
- if (!entry.pieceId) {
- errors.push(`Sélectionner une pièce existante pour "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}".`)
- return
- }
- const piece = deps.findPieceById(entry.pieceId as string)
- if (!piece) {
- errors.push(`La pièce sélectionnée est introuvable (ID: ${entry.pieceId}).`)
- return
- }
- const requiredTypeId = (requirement.typePieceId || (requirement.typePiece as AnyRecord)?.id || null) as string | null
- if (requiredTypeId && piece.typePieceId && piece.typePieceId !== requiredTypeId) {
- errors.push(`La pièce "${piece.name || piece.id}" n'appartient pas à la famille attendue.`)
- return
- }
- const payload: AnyRecord = { requirementId: requirement.id, pieceId: entry.pieceId }
- const overrides = sanitizeDefinitionOverrides(entry.definition as AnyRecord)
- if (overrides) payload.overrides = overrides
- Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
- pieceLinksPayload.push(payload)
- })
- }
-
- const { stats: productStats } = buildProductRequirementStats(type, deps)
- for (const requirement of (type.productRequirements || []) as AnyRecord[]) {
- const entries = deps.getProductRequirementEntries(requirement.id as string)
- const max = (requirement.maxCount ?? null) as number | null
-
- if (max !== null && entries.length > max) {
- errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} entrée(s) directe(s).`)
- }
-
- entries.forEach((entry) => {
- if (!entry.productId) {
- errors.push(`Sélectionner un produit pour "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}".`)
- return
- }
- const product = deps.findProductById(entry.productId as string)
- if (!product) {
- errors.push(`Le produit sélectionné est introuvable (ID: ${entry.productId}).`)
- return
- }
- const requiredTypeId = (requirement.typeProductId || (requirement.typeProduct as AnyRecord)?.id || null) as string | null
- const productTypeId = (product.typeProductId || (product.typeProduct as AnyRecord)?.id || entry.typeProductId || null) as string | null
- if (requiredTypeId && productTypeId && productTypeId !== requiredTypeId) {
- errors.push(`Le produit "${product.name || product.reference || product.id}" n'appartient pas à la catégorie attendue.`)
- return
- }
- const payload: AnyRecord = { requirementId: requirement.id, productId: entry.productId }
- Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
- productLinksPayload.push(payload)
- })
- }
-
- productStats.forEach((stat) => {
- ((stat.issues || []) as AnyRecord[])
- .filter((issue) => issue.kind === 'error')
- .forEach((issue) => errors.push(issue.message as string))
- })
-
- if (errors.length > 0) return { valid: false, error: errors[0] }
-
- return {
- valid: true,
- componentLinks: componentLinksPayload,
- pieceLinks: pieceLinksPayload,
- productLinks: productLinksPayload,
- }
-}
-
-// ---------------------------------------------------------------------------
-// Main preview composable
-// ---------------------------------------------------------------------------
-
-export function useMachineCreatePreview(deps: MachineCreatePreviewDeps) {
- const machinePreview = computed(() => {
- const type = deps.selectedMachineType.value
- if (!type) return null
-
- const trimmedName = (deps.newMachine.name || '').trim()
- const currentSite = deps.newMachine.siteId
- ? deps.sites.value.find((site) => site.id === deps.newMachine.siteId) || null
- : null
- const trimmedReference = (deps.newMachine.reference || '').trim()
-
- const baseFields = [
- { key: 'name', label: 'Nom', display: trimmedName || 'À renseigner', status: trimmedName ? 'complete' : 'missing' },
- { key: 'site', label: 'Site', display: (currentSite?.name || 'Sélectionner un site') as string, status: currentSite ? 'complete' : 'missing' },
- { key: 'type', label: 'Type sélectionné', display: type.name as string, status: 'complete' },
- { key: 'reference', label: 'Référence', display: trimmedReference || 'Non renseignée', status: trimmedReference ? 'complete' : 'optional' },
- ]
-
- const baseIssues: AnyRecord[] = []
- if (!trimmedName) baseIssues.push({ message: 'Renseigner un nom de machine.', kind: 'error', anchor: 'machine-field-name' })
- if (!currentSite) baseIssues.push({ message: "Sélectionner un site d'affectation.", kind: 'error', anchor: 'machine-field-site' })
-
- const baseStatus = baseIssues.some((issue) => issue.kind === 'error') ? 'error' : 'ready'
-
- // Component groups
- const componentGroups = ((type.componentRequirements || []) as AnyRecord[]).map((requirement) => {
- const entries = deps.getComponentRequirementEntries(requirement.id as string)
- const normalizedEntries = entries.map((entry, index) => {
- const selectedComponent = entry.composantId ? deps.findComponentById(entry.composantId as string) : null
- const displayName = (selectedComponent?.name || (requirement.typeComposant as AnyRecord)?.name || 'Composant') as string
- const subtitleParts: string[] = []
- if (selectedComponent?.reference) subtitleParts.push(`Réf. ${selectedComponent.reference}`)
- const constructeurName = (selectedComponent?.constructeur as AnyRecord)?.name || selectedComponent?.constructeurName
- if (constructeurName) subtitleParts.push(constructeurName as string)
- const machineAssignments = selectedComponent ? getComponentMachineAssignments(selectedComponent) : []
- const assignmentLabel = formatAssignmentList(machineAssignments)
- if (assignmentLabel) subtitleParts.push(`Liée à ${assignmentLabel}`)
- return {
- key: `${requirement.id}-${index}`,
- status: entry.composantId ? 'complete' : 'pending',
- title: displayName,
- subtitle: subtitleParts.join(' • ') || null,
- assignmentLabel,
- assignments: machineAssignments,
- }
- })
-
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const max = (requirement.maxCount ?? null) as number | null
- const completed = normalizedEntries.filter((e) => e.status === 'complete').length
- const issues: AnyRecord[] = []
- if (entries.length < min) issues.push({ message: `Minimum ${min} sélection(s) requise(s)`, kind: 'error', anchor: `component-group-${requirement.id}` })
- if (max !== null && entries.length > max) issues.push({ message: `Maximum ${max} dépassé`, kind: 'error', anchor: `component-group-${requirement.id}` })
- if (normalizedEntries.some((e) => e.status !== 'complete')) issues.push({ message: 'Sélectionner un composant pour chaque entrée.', kind: 'error', anchor: `component-group-${requirement.id}` })
-
- const hasErrors = issues.some((i) => i.kind === 'error')
- const hasWarnings = completed < entries.length
- const status = hasErrors ? 'error' : hasWarnings ? 'warning' : 'ready'
-
- return {
- id: requirement.id,
- label: (requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Famille de composants') as string,
- typeName: ((requirement.typeComposant as AnyRecord)?.name || 'Non défini') as string,
- min, max, entries: normalizedEntries, issues, completed, total: entries.length, status,
- }
- })
-
- // Piece groups
- const pieceGroups = ((type.pieceRequirements || []) as AnyRecord[]).map((requirement) => {
- const entries = deps.getPieceRequirementEntries(requirement.id as string)
- const normalizedEntries = entries.map((entry, index) => {
- const selectedPiece = entry.pieceId ? deps.findPieceById(entry.pieceId as string) : null
- const displayName = (selectedPiece?.name || (requirement.typePiece as AnyRecord)?.name || 'Pièce') as string
- const subtitleParts: string[] = []
- if (selectedPiece?.reference) subtitleParts.push(`Réf. ${selectedPiece.reference}`)
- const constructeurName = (selectedPiece?.constructeur as AnyRecord)?.name || selectedPiece?.constructeurName
- if (constructeurName) subtitleParts.push(constructeurName as string)
- const machineAssignments = selectedPiece ? getPieceMachineAssignments(selectedPiece) : []
- const machineAssignmentLabel = formatAssignmentList(machineAssignments)
- if (machineAssignmentLabel) subtitleParts.push(`Machines: ${machineAssignmentLabel}`)
- const componentAssignments = selectedPiece ? getPieceComponentAssignments(selectedPiece) : []
- const componentAssignmentLabel = formatAssignmentList(componentAssignments)
- if (componentAssignmentLabel) subtitleParts.push(`Composants: ${componentAssignmentLabel}`)
- return {
- key: `${requirement.id}-${index}`,
- status: entry.pieceId ? 'complete' : 'pending',
- title: displayName,
- subtitle: subtitleParts.join(' • ') || null,
- machineAssignmentLabel, componentAssignmentLabel,
- machineAssignments, componentAssignments,
- }
- })
-
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const max = (requirement.maxCount ?? null) as number | null
- const completed = normalizedEntries.filter((e) => e.status === 'complete').length
- const issues: AnyRecord[] = []
- if (entries.length < min) issues.push({ message: `Minimum ${min} sélection(s) requise(s)`, kind: 'error', anchor: `piece-group-${requirement.id}` })
- if (max !== null && entries.length > max) issues.push({ message: `Maximum ${max} dépassé`, kind: 'error', anchor: `piece-group-${requirement.id}` })
- if (normalizedEntries.some((e) => e.status !== 'complete')) issues.push({ message: 'Sélectionner une pièce pour chaque entrée.', kind: 'error', anchor: `piece-group-${requirement.id}` })
-
- const hasErrors = issues.some((i) => i.kind === 'error')
- const hasWarnings = completed < entries.length
- const status = hasErrors ? 'error' : hasWarnings ? 'warning' : 'ready'
-
- return {
- id: requirement.id,
- label: (requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Groupe de pièces') as string,
- typeName: ((requirement.typePiece as AnyRecord)?.name || 'Non défini') as string,
- min, max, entries: normalizedEntries, issues, completed, total: entries.length, status,
- }
- })
-
- // Product groups
- const { stats: productGroups } = buildProductRequirementStats(type, deps)
-
- // Aggregate
- const aggregatedIssues = [
- ...baseIssues.map((issue) => ({ ...issue, scope: 'Informations générales' })),
- ...componentGroups.flatMap((group) => group.issues.map((issue) => ({ ...issue, scope: group.label }))),
- ...pieceGroups.flatMap((group) => group.issues.map((issue) => ({ ...issue, scope: group.label }))),
- ...productGroups.flatMap((group: AnyRecord) => ((group.issues || []) as AnyRecord[]).map((issue) => ({ ...issue, scope: group.label }))),
- ]
-
- const statuses = [
- baseStatus,
- ...componentGroups.map((g) => g.status),
- ...pieceGroups.map((g) => g.status),
- ...productGroups.map((g: AnyRecord) => g.status as string),
- ]
-
- const overallStatus = statuses.includes('error') ? 'error' : statuses.includes('warning') ? 'warning' : 'ready'
-
- return {
- base: { fields: baseFields, issues: baseIssues, status: baseStatus },
- componentGroups,
- pieceGroups,
- productGroups,
- type: {
- name: type.name,
- category: type.category || null,
- hasStructuredDefinition:
- ((type.componentRequirements as unknown[])?.length || 0) > 0 ||
- ((type.pieceRequirements as unknown[])?.length || 0) > 0 ||
- ((type.productRequirements as unknown[])?.length || 0) > 0,
- },
- status: overallStatus,
- ready: overallStatus === 'ready',
- issues: aggregatedIssues,
- }
- })
-
- const blockingPreviewIssues = computed(() => {
- if (!machinePreview.value) return []
- return (machinePreview.value.issues as AnyRecord[]).filter((issue) => issue.kind === 'error')
- })
-
- const canCreateMachine = computed(() => {
- if (!machinePreview.value) return false
- return blockingPreviewIssues.value.length === 0
- })
-
- return {
- machinePreview,
- blockingPreviewIssues,
- canCreateMachine,
- }
-}
diff --git a/app/composables/useMachineCreateSelections.ts b/app/composables/useMachineCreateSelections.ts
deleted file mode 100644
index 3bbbad3..0000000
--- a/app/composables/useMachineCreateSelections.ts
+++ /dev/null
@@ -1,365 +0,0 @@
-/**
- * Machine creation – requirement selection state management.
- *
- * Extracted from pages/machines/new.vue. Manages the reactive selection state
- * for component / piece / product requirements when creating a new machine.
- */
-
-import { ref, reactive, computed } from 'vue'
-import { extractCollection } from '~/shared/utils/apiHelpers'
-
-type AnyRecord = Record
-
-export interface MachineCreateSelectionsDeps {
- findComponentById: (id: string) => AnyRecord | null
- findPieceById: (id: string) => AnyRecord | null
- pieces: { value: AnyRecord[] }
- get: (url: string) => Promise
- toast: { showError: (msg: string) => void }
-}
-
-export function useMachineCreateSelections(deps: MachineCreateSelectionsDeps) {
- const { findComponentById, findPieceById, pieces, get, toast } = deps
-
- // ---------------------------------------------------------------------------
- // Reactive state
- // ---------------------------------------------------------------------------
-
- const componentRequirementSelections = reactive>({})
- const pieceRequirementSelections = reactive>({})
- const productRequirementSelections = reactive>({})
-
- const pieceOptionsByKey = ref>({})
- const pieceLoadingByKey = ref>({})
-
- // ---------------------------------------------------------------------------
- // Piece option caching
- // ---------------------------------------------------------------------------
-
- const getPieceKey = (requirement: AnyRecord, entryIndex: number): string =>
- `${requirement?.id || 'req'}:${entryIndex}`
-
- const findPieceInCachedOptions = (id: string): AnyRecord | null => {
- if (!id) return null
- const buckets = Object.values(pieceOptionsByKey.value || {})
- for (const bucket of buckets) {
- if (!Array.isArray(bucket)) continue
- const found = bucket.find((piece) => piece?.id === id)
- if (found) return found
- }
- return null
- }
-
- const cachePieceIfMissing = (piece: AnyRecord): void => {
- if (!piece?.id) return
- const current = Array.isArray(pieces.value) ? pieces.value : []
- if (current.some((p: AnyRecord) => p?.id === piece.id)) return
- pieces.value = [...current, piece]
- }
-
- const fetchPieceOptions = async (
- requirement: AnyRecord,
- entryIndex: number,
- term = '',
- ): Promise => {
- const key = getPieceKey(requirement, entryIndex)
- if (pieceLoadingByKey.value[key]) return
-
- const requirementTypeId =
- (requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null) as string | null
- const params = new URLSearchParams()
- params.set('itemsPerPage', '50')
- if (term && term.trim()) params.set('name', term.trim())
- if (requirementTypeId) params.set('typePiece', `/api/model_types/${requirementTypeId}`)
-
- pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: true }
- try {
- const result = await get(`/pieces?${params.toString()}`)
- if (result.success) {
- pieceOptionsByKey.value = {
- ...pieceOptionsByKey.value,
- [key]: extractCollection(result.data) as AnyRecord[],
- }
- }
- } finally {
- pieceLoadingByKey.value = { ...pieceLoadingByKey.value, [key]: false }
- }
- }
-
- // ---------------------------------------------------------------------------
- // Entry getters
- // ---------------------------------------------------------------------------
-
- const getComponentRequirementEntries = (requirementId: string): AnyRecord[] =>
- componentRequirementSelections[requirementId] || []
-
- const getPieceRequirementEntries = (requirementId: string): AnyRecord[] =>
- pieceRequirementSelections[requirementId] || []
-
- const getProductRequirementEntries = (requirementId: string): AnyRecord[] =>
- productRequirementSelections[requirementId] || []
-
- // ---------------------------------------------------------------------------
- // Entry factories
- // ---------------------------------------------------------------------------
-
- const createComponentSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
- typeComposantId: requirement?.typeComposantId || (requirement?.typeComposant as AnyRecord)?.id || null,
- composantId: source?.composantId || null,
- definition: {},
- })
-
- const createPieceSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
- typePieceId: requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null,
- pieceId: source?.pieceId || null,
- definition: {},
- })
-
- const createProductSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => ({
- typeProductId:
- source?.typeProductId ||
- requirement?.typeProductId ||
- (requirement?.typeProduct as AnyRecord)?.id ||
- null,
- productId: source?.productId || null,
- })
-
- // ---------------------------------------------------------------------------
- // Selected piece IDs (for dedup)
- // ---------------------------------------------------------------------------
-
- const selectedPieceIds = computed(() => {
- const ids: string[] = []
- Object.values(pieceRequirementSelections).forEach((entries) => {
- ;(entries || []).forEach((entry) => {
- if (entry?.pieceId) ids.push(entry.pieceId as string)
- })
- })
- return ids
- })
-
- // ---------------------------------------------------------------------------
- // CRUD operations
- // ---------------------------------------------------------------------------
-
- const addComponentSelectionEntry = (requirement: AnyRecord): void => {
- const entries = getComponentRequirementEntries(requirement.id as string)
- const max = (requirement.maxCount ?? null) as number | null
- if (max !== null && entries.length >= max) {
- toast.showError(
- `Vous ne pouvez pas ajouter plus de ${max} composant(s) pour ${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'ce groupe'}`,
- )
- return
- }
- componentRequirementSelections[requirement.id as string] = [
- ...entries,
- createComponentSelectionEntry(requirement),
- ]
- }
-
- const removeComponentSelectionEntry = (requirementId: string, index: number): void => {
- const entries = getComponentRequirementEntries(requirementId)
- componentRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
- }
-
- const addPieceSelectionEntry = (requirement: AnyRecord): void => {
- const entries = getPieceRequirementEntries(requirement.id as string)
- const max = (requirement.maxCount ?? null) as number | null
- if (max !== null && entries.length >= max) {
- toast.showError(
- `Vous ne pouvez pas ajouter plus de ${max} pièce(s) pour ${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'ce groupe'}`,
- )
- return
- }
- pieceRequirementSelections[requirement.id as string] = [
- ...entries,
- createPieceSelectionEntry(requirement),
- ]
- fetchPieceOptions(requirement, entries.length).catch(() => {})
- }
-
- const removePieceSelectionEntry = (requirementId: string, index: number): void => {
- const entries = getPieceRequirementEntries(requirementId)
- pieceRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
- }
-
- const addProductSelectionEntry = (requirement: AnyRecord): void => {
- const entries = getProductRequirementEntries(requirement.id as string)
- const max = (requirement.maxCount ?? null) as number | null
- if (max !== null && entries.length >= max) {
- toast.showError(
- `Vous ne pouvez pas ajouter plus de ${max} produit(s) pour ${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'ce groupe'}`,
- )
- return
- }
- productRequirementSelections[requirement.id as string] = [
- ...entries,
- createProductSelectionEntry(requirement),
- ]
- }
-
- const removeProductSelectionEntry = (requirementId: string, index: number): void => {
- const entries = getProductRequirementEntries(requirementId)
- productRequirementSelections[requirementId] = entries.filter((_: unknown, i: number) => i !== index)
- }
-
- // ---------------------------------------------------------------------------
- // Selection setters
- // ---------------------------------------------------------------------------
-
- const setComponentRequirementComponent = (
- requirement: AnyRecord,
- index: number,
- componentId: string,
- ): void => {
- const entries = getComponentRequirementEntries(requirement.id as string)
- const entry = entries[index]
- if (!entry) return
- entry.composantId = componentId || null
- if (componentId) {
- const component = findComponentById(componentId)
- entry.typeComposantId = component?.typeComposantId || requirement?.typeComposantId || null
- } else {
- entry.typeComposantId = requirement?.typeComposantId || null
- }
- }
-
- const setPieceRequirementPiece = (
- requirement: AnyRecord,
- index: number,
- pieceId: string,
- ): void => {
- const entries = getPieceRequirementEntries(requirement.id as string)
- const entry = entries[index]
- if (!entry) return
- entry.pieceId = pieceId || null
- if (pieceId) {
- const piece = findPieceById(pieceId) || findPieceInCachedOptions(pieceId)
- entry.typePieceId = piece?.typePieceId || requirement?.typePieceId || null
- if (piece) cachePieceIfMissing(piece as AnyRecord)
- } else {
- entry.typePieceId = requirement?.typePieceId || null
- }
- }
-
- const setProductRequirementProduct = (
- requirement: AnyRecord,
- index: number,
- productId: string,
- findProductById: (id: string) => AnyRecord | null,
- ): void => {
- const entries = getProductRequirementEntries(requirement.id as string)
- const entry = entries[index]
- if (!entry) return
-
- const normalizedProductId = productId || null
- entry.productId = normalizedProductId
-
- if (normalizedProductId) {
- const product = findProductById(normalizedProductId)
- entry.typeProductId =
- product?.typeProductId ||
- (product?.typeProduct as AnyRecord)?.id ||
- entry.typeProductId ||
- requirement?.typeProductId ||
- (requirement?.typeProduct as AnyRecord)?.id ||
- null
- } else {
- entry.typeProductId =
- requirement?.typeProductId ||
- (requirement?.typeProduct as AnyRecord)?.id ||
- null
- }
- }
-
- // ---------------------------------------------------------------------------
- // Bulk operations
- // ---------------------------------------------------------------------------
-
- const clearRequirementSelections = (): void => {
- Object.keys(componentRequirementSelections).forEach((key) => {
- delete componentRequirementSelections[key]
- })
- Object.keys(pieceRequirementSelections).forEach((key) => {
- delete pieceRequirementSelections[key]
- })
- Object.keys(productRequirementSelections).forEach((key) => {
- delete productRequirementSelections[key]
- })
- }
-
- const initializeRequirementSelections = (type: AnyRecord): void => {
- const componentRequirements = (type.componentRequirements || []) as AnyRecord[]
- const pieceRequirements = (type.pieceRequirements || []) as AnyRecord[]
- const productRequirements = (type.productRequirements || []) as AnyRecord[]
-
- componentRequirements.forEach((requirement) => {
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const initialCount = Math.max(min, requirement.required ? 1 : 0)
- if (initialCount > 0) {
- componentRequirementSelections[requirement.id as string] = Array.from(
- { length: initialCount },
- () => createComponentSelectionEntry(requirement),
- )
- } else {
- componentRequirementSelections[requirement.id as string] = []
- }
- })
-
- pieceRequirements.forEach((requirement) => {
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const initialCount = Math.max(min, requirement.required ? 1 : 0)
- if (initialCount > 0) {
- const entries = Array.from(
- { length: initialCount },
- () => createPieceSelectionEntry(requirement),
- )
- pieceRequirementSelections[requirement.id as string] = entries
- entries.forEach((_: unknown, index: number) => {
- fetchPieceOptions(requirement, index).catch(() => {})
- })
- } else {
- pieceRequirementSelections[requirement.id as string] = []
- }
- })
-
- productRequirements.forEach((requirement) => {
- const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number
- const initialCount = Math.max(min, requirement.required ? 1 : 0)
- if (initialCount > 0) {
- productRequirementSelections[requirement.id as string] = Array.from(
- { length: initialCount },
- () => createProductSelectionEntry(requirement),
- )
- } else {
- productRequirementSelections[requirement.id as string] = []
- }
- })
- }
-
- return {
- componentRequirementSelections,
- pieceRequirementSelections,
- productRequirementSelections,
- pieceOptionsByKey,
- pieceLoadingByKey,
- selectedPieceIds,
- getPieceKey,
- findPieceInCachedOptions,
- fetchPieceOptions,
- getComponentRequirementEntries,
- getPieceRequirementEntries,
- getProductRequirementEntries,
- addComponentSelectionEntry,
- removeComponentSelectionEntry,
- addPieceSelectionEntry,
- removePieceSelectionEntry,
- addProductSelectionEntry,
- removeProductSelectionEntry,
- setComponentRequirementComponent,
- setPieceRequirementPiece,
- setProductRequirementProduct,
- clearRequirementSelections,
- initializeRequirementSelections,
- }
-}
diff --git a/app/composables/useMachineDetailData.ts b/app/composables/useMachineDetailData.ts
index 2624980..ebd4946 100644
--- a/app/composables/useMachineDetailData.ts
+++ b/app/composables/useMachineDetailData.ts
@@ -39,6 +39,8 @@ import {
resolveIdentifier,
resolveProductReference as _resolveProductReference,
getProductDisplay as _getProductDisplay,
+ getProductSuppliersLabel,
+ getProductPriceLabel,
extractParentLinkIdentifiers,
} from '~/shared/utils/productDisplayUtils'
import {
@@ -64,7 +66,7 @@ export function useMachineDetailData(machineId: string) {
const {
updateMachine: updateMachineApi,
- reconfigureSkeleton: reconfigureMachineSkeleton,
+ updateStructure: updateMachineStructure,
} = useMachines()
const { updateComposant: updateComposantApi } = useComposants()
const { updatePiece: updatePieceApi } = usePieces()
@@ -75,11 +77,12 @@ export function useMachineDetailData(machineId: string) {
upsertCustomFieldValue,
updateCustomFieldValue: updateCustomFieldValueApi,
} = useCustomFields()
- const { get } = useApi()
+ const { get, post: apiPost, delete: apiDel } = useApi()
const {
uploadDocuments,
deleteDocument,
loadDocumentsByMachine,
+ loadDocumentsByProduct,
} = useDocuments()
const toast = useToast()
const { constructeurs, loadConstructeurs } = useConstructeurs()
@@ -105,6 +108,7 @@ export function useMachineDetailData(machineId: string) {
const machineComponentLinks = ref([])
const machinePieceLinks = ref([])
const machineProductLinks = ref([])
+ const productDocumentsMap = ref
- {{ machine.typeMachine?.category || "N/A" }}
-