diff --git a/app/components/machine/create/MachineCreatePreview.vue b/app/components/machine/create/MachineCreatePreview.vue new file mode 100644 index 0000000..b50dcd6 --- /dev/null +++ b/app/components/machine/create/MachineCreatePreview.vue @@ -0,0 +1,205 @@ + + + diff --git a/app/components/machine/create/PreviewRequirementGroup.vue b/app/components/machine/create/PreviewRequirementGroup.vue new file mode 100644 index 0000000..69013a2 --- /dev/null +++ b/app/components/machine/create/PreviewRequirementGroup.vue @@ -0,0 +1,59 @@ + + + diff --git a/app/components/machine/create/RequirementComponentSelector.vue b/app/components/machine/create/RequirementComponentSelector.vue new file mode 100644 index 0000000..0f5a157 --- /dev/null +++ b/app/components/machine/create/RequirementComponentSelector.vue @@ -0,0 +1,126 @@ + + + diff --git a/app/components/machine/create/RequirementPieceSelector.vue b/app/components/machine/create/RequirementPieceSelector.vue new file mode 100644 index 0000000..8b0952e --- /dev/null +++ b/app/components/machine/create/RequirementPieceSelector.vue @@ -0,0 +1,130 @@ + + + diff --git a/app/components/machine/create/RequirementProductSelector.vue b/app/components/machine/create/RequirementProductSelector.vue new file mode 100644 index 0000000..5c5e026 --- /dev/null +++ b/app/components/machine/create/RequirementProductSelector.vue @@ -0,0 +1,142 @@ + + + diff --git a/app/composables/useMachineCreatePage.ts b/app/composables/useMachineCreatePage.ts new file mode 100644 index 0000000..cd63389 --- /dev/null +++ b/app/composables/useMachineCreatePage.ts @@ -0,0 +1,458 @@ +/** + * 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. + */ + +import { ref, reactive, computed, watch, 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 { 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 } = 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() + + // --------------------------------------------------------------------------- + // Local state + // --------------------------------------------------------------------------- + + const submitting = ref(false) + + const newMachine = reactive({ + name: '', + siteId: '', + typeMachineId: '', + reference: '', + }) + + 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') + return + } + + submitting.value = true + try { + const baseMachineData = { + name: newMachine.name, + siteId: newMachine.siteId, + reference: newMachine.reference, + typeMachineId: type.id, + } + + 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) { + if (hasRequirements && result.data?.id) { + const skeletonResult: any = await reconfigureSkeleton(result.data.id, { + componentLinks, + pieceLinks, + productLinks, + } as any) + if (!skeletonResult.success) { + toast.showError(skeletonResult.error || 'Impossible d\'enregistrer les pièces/composants') + return + } + } + newMachine.name = '' + newMachine.siteId = '' + newMachine.typeMachineId = '' + newMachine.reference = '' + clearRequirementSelections() + await navigateTo('/machines') + } else if (result.error) { + toast.showError(`Impossible de créer la machine: ${result.error}`) + } + } catch (error: any) { + toast.showError(`Erreur lors de la création: ${error.message}`) + } finally { + submitting.value = false + } + } + + // --------------------------------------------------------------------------- + // Watchers & 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(), + loadPieces(), + loadProducts(), + ]) + }) + + // --------------------------------------------------------------------------- + // Public API + // --------------------------------------------------------------------------- + + 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, + + // Actions + finalizeMachineCreation, + } +} diff --git a/app/composables/useMachineCreateSelections.ts b/app/composables/useMachineCreateSelections.ts index f1dcc6b..3bbbad3 100644 --- a/app/composables/useMachineCreateSelections.ts +++ b/app/composables/useMachineCreateSelections.ts @@ -6,6 +6,7 @@ */ import { ref, reactive, computed } from 'vue' +import { extractCollection } from '~/shared/utils/apiHelpers' type AnyRecord = Record @@ -17,14 +18,6 @@ export interface MachineCreateSelectionsDeps { toast: { showError: (msg: string) => void } } -const extractCollection = (payload: unknown): unknown[] => { - if (Array.isArray(payload)) return payload - if (Array.isArray((payload as AnyRecord)?.member)) return (payload as AnyRecord).member as unknown[] - if (Array.isArray((payload as AnyRecord)?.['hydra:member'])) return (payload as AnyRecord)['hydra:member'] as unknown[] - if (Array.isArray((payload as AnyRecord)?.data)) return (payload as AnyRecord).data as unknown[] - return [] -} - export function useMachineCreateSelections(deps: MachineCreateSelectionsDeps) { const { findComponentById, findPieceById, pieces, get, toast } = deps @@ -317,11 +310,12 @@ export function useMachineCreateSelections(deps: MachineCreateSelectionsDeps) { const min = (requirement.minCount ?? (requirement.required ? 1 : 0)) as number const initialCount = Math.max(min, requirement.required ? 1 : 0) if (initialCount > 0) { - pieceRequirementSelections[requirement.id as string] = Array.from( + const entries = Array.from( { length: initialCount }, () => createPieceSelectionEntry(requirement), ) - pieceRequirementSelections[requirement.id as string].forEach((_: unknown, index: number) => { + pieceRequirementSelections[requirement.id as string] = entries + entries.forEach((_: unknown, index: number) => { fetchPieceOptions(requirement, index).catch(() => {}) }) } else { diff --git a/app/pages/machines/new.vue b/app/pages/machines/new.vue index 557ea2b..7a63418 100644 --- a/app/pages/machines/new.vue +++ b/app/pages/machines/new.vue @@ -15,9 +15,10 @@ -
+
+
Site - - @@ -54,13 +55,13 @@ Type de machine
@@ -69,7 +70,7 @@ Référence
-
+ +

Structure du type sélectionné :

Familles de composants : - {{ selectedMachineType.componentRequirements?.length || 0 }} + {{ c.selectedMachineType.componentRequirements?.length || 0 }} Groupes de pièces : - {{ selectedMachineType.pieceRequirements?.length || 0 }} + {{ c.selectedMachineType.pieceRequirements?.length || 0 }} Produits requis : - {{ selectedMachineType.productRequirements?.length || 0 }} + {{ c.selectedMachineType.productRequirements?.length || 0 }} Catégorie : - {{ selectedMachineType.category || 'N/A' }} + {{ c.selectedMachineType.category || 'N/A' }}

Ce type n'a pas encore de familles configurées. La machine héritera de la structure legacy du type.

-
-

- 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é : - {{ resolveComponentRequirementTypeLabel(requirement, entry) }} - - -
- -
-
-
- - -
-

- Aucun composant disponible pour cette famille. -

-
- -
-
- {{ (findComponentById(entry.composantId)?.name) || "Composant" }} -
-
- Référence : {{ findComponentById(entry.composantId)?.reference || "—" }} -
-
- Fournisseur : - {{ findComponentById(entry.composantId)?.constructeur?.name || findComponentById(entry.composantId)?.constructeurName || "—" }} -
- -
-
-
-
-
-
-

- 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é : - {{ resolvePieceRequirementTypeLabel(requirement, entry) }} - - -
- -
-
-
- - -
-

- Aucune pièce disponible pour cette famille. -

-
- -
-
- {{ (findPieceById(entry.pieceId)?.name) || "Pièce" }} -
-
- Référence : {{ findPieceById(entry.pieceId)?.reference || "—" }} -
-
- Fournisseur : - {{ findPieceById(entry.pieceId)?.constructeur?.name || findPieceById(entry.pieceId)?.constructeurName || "—" }} -
- -
-
-
-
-
-
-

- 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. -

-
- -
-
- {{ findProductById(entry.productId)?.name || 'Produit' }} -
-
- Référence : {{ findProductById(entry.productId)?.reference || "—" }} -
-
- Prix indicatif : - - {{ Number(findProductById(entry.productId)?.supplierPrice).toFixed(2) }} € - - - — - -
-
- Fournisseurs : - - {{ findProductById(entry.productId)?.constructeurs.map(constructeur => constructeur?.name).filter(Boolean).join(', ') }} - - - — - -
-
-
-
-
-
-
-
-
-
-
-
- - {{ machinePreview.status === 'ready' ? 'Prête à créer' : machinePreview.status === 'warning' ? 'À compléter' : 'Bloquante' }} - -
- -
-
- {{ field.label }} - - {{ field.display }} - -
-
- -
- Type : {{ machinePreview.type.name }} - Catégorie : {{ machinePreview.type.category }} - Structure JSON : {{ machinePreview.type.hasStructuredDefinition ? 'Oui' : 'Legacy' }} -
- -
-

- Informations générales incomplètes : -

-
    -
  • - -
  • -
-
- -
-
- Composants hérités -
-
-
-
-

- {{ 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 }} -
  • -
-
- -
    -
  • -
  • -
-
-
- -
- Aucun composant n'est requis pour ce type de machine. -
- -
-
- Pièces associées -
-
-
-
-

- {{ 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 }} -
  • -
-
- -
    -
  • -
  • -
-
-
- -
- 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 }} -
  • -
-
- -
    -
  • -
  • -
- -

- Couverture assurée via composants ou pièces liés. -

-
-
- -
-
-
-
-
-
-
- -
Compléter les informations bloquantes avant de créer la machine.
+
Annuler @@ -723,8 +171,8 @@ @@ -737,495 +185,12 @@