refactor(machines) : remove TypeMachine skeleton system, simplify machine creation

- Remove TypeEdit*, TypeInfoDisplay, MachineSkeletonSummary, MachineCreatePreview components
- Remove machine-skeleton pages and type pages
- Remove useMachineTypesApi, useMachineSkeletonEditor, useMachineCreateSelections composables
- Add AddEntityToMachineModal for direct entity linking
- Update machine detail/create pages for direct custom fields
- Fix SearchSelect, category display, and ipartial search filters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-05 17:25:23 +01:00
parent 6f1bac381d
commit 32d03b480d
49 changed files with 1058 additions and 6093 deletions

View File

@@ -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,

View File

@@ -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<string, unknown>
export interface MachineCreatePreviewDeps {
newMachine: { name: string; siteId: string; typeMachineId: string; reference: string }
sites: Ref<AnyRecord[]>
selectedMachineType: ComputedRef<AnyRecord | null>
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<string, number> => {
const usage = new Map<string, number>()
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<string, number> } => {
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,
}
}

View File

@@ -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<string, unknown>
export interface MachineCreateSelectionsDeps {
findComponentById: (id: string) => AnyRecord | null
findPieceById: (id: string) => AnyRecord | null
pieces: { value: AnyRecord[] }
get: (url: string) => Promise<AnyRecord>
toast: { showError: (msg: string) => void }
}
export function useMachineCreateSelections(deps: MachineCreateSelectionsDeps) {
const { findComponentById, findPieceById, pieces, get, toast } = deps
// ---------------------------------------------------------------------------
// Reactive state
// ---------------------------------------------------------------------------
const componentRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
const pieceRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
const productRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
const pieceOptionsByKey = ref<Record<string, AnyRecord[]>>({})
const pieceLoadingByKey = ref<Record<string, boolean>>({})
// ---------------------------------------------------------------------------
// 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<void> => {
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,
}
}

View File

@@ -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<AnyRecord[]>([])
const machinePieceLinks = ref<AnyRecord[]>([])
const machineProductLinks = ref<AnyRecord[]>([])
const productDocumentsMap = ref<Map<string, AnyRecord[]>>(new Map())
const printAreaRef = ref<HTMLElement | null>(null)
// ---------------------------------------------------------------------------
@@ -169,39 +173,21 @@ export function useMachineDetailData(machineId: string) {
const componentsCollapsed = ref(true)
const collapseToggleToken = ref(0)
const piecesCollapsed = ref(true)
const pieceCollapseToggleToken = ref(0)
// ---------------------------------------------------------------------------
// Product helpers
// ---------------------------------------------------------------------------
const machineType = computed(() => (machine.value as AnyRecord)?.typeMachine || null)
const componentTypeOptions = computed(() => componentTypes.value || [])
const pieceTypeOptions = computed(() => pieceTypes.value || [])
const componentRequirements = computed(
() => ((machineType.value as AnyRecord)?.componentRequirements as AnyRecord[]) || [],
)
const pieceRequirements = computed(
() => ((machineType.value as AnyRecord)?.pieceRequirements as AnyRecord[]) || [],
)
const productRequirements = computed(
() => ((machineType.value as AnyRecord)?.productRequirements as AnyRecord[]) || [],
)
const machineHasSkeletonRequirements = computed(() =>
componentRequirements.value.length > 0 ||
pieceRequirements.value.length > 0 ||
productRequirements.value.length > 0,
)
const componentTypeLabelMap = computed(() => {
const map = new Map<string, string>()
componentTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
componentRequirements.value.forEach((req: AnyRecord) => {
const type = req.typeComposant as AnyRecord | undefined
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
@@ -210,10 +196,6 @@ export function useMachineDetailData(machineId: string) {
pieceTypeOptions.value.forEach((type) => {
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
pieceRequirements.value.forEach((req: AnyRecord) => {
const type = req.typePiece as AnyRecord | undefined
if (type?.id) map.set(type.id as string, (type.name as string) || '')
})
return map
})
@@ -310,8 +292,7 @@ export function useMachineDetailData(machineId: string) {
const transformCustomFields = (piecesData: AnyRecord[]): AnyRecord[] => {
return (piecesData || []).map((piece) => {
const requirement = (piece.typeMachinePieceRequirement as AnyRecord) || {}
const typePiece = (requirement.typePiece as AnyRecord) || (piece.typePiece as AnyRecord) || {}
const typePiece = (piece.typePiece as AnyRecord) || {}
const normalizeStructureDefs = (structure: unknown) =>
structure ? normalizeStructureForEditor(structure as AnyRecord) : null
@@ -320,10 +301,6 @@ export function useMachineDetailData(machineId: string) {
normalizeStructureDefs((piece.definition as AnyRecord)?.structure),
normalizeStructureDefs((piece.typePiece as AnyRecord)?.structure),
normalizeStructureDefs(typePiece.structure),
normalizeStructureDefs(typePiece.pieceSkeleton),
normalizeStructureDefs((piece.typePiece as AnyRecord)?.pieceSkeleton),
normalizeStructureDefs(requirement.structure),
normalizeStructureDefs(requirement.pieceSkeleton),
]
const valueEntries = [
@@ -347,17 +324,16 @@ export function useMachineDetailData(machineId: string) {
normalizeExistingCustomFieldDefinitions((piece.definition as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions((piece.typePiece as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(typePiece.customFields),
normalizeExistingCustomFieldDefinitions((requirement.typePiece as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(requirement.customFields),
normalizeExistingCustomFieldDefinitions((requirement.definition as AnyRecord)?.customFields),
...normalizedStructureDefs.map((def) => getStructureCustomFields(def)),
),
)
const constructeurIds = uniqueConstructeurIds(
piece.constructeurs,
piece.constructeurIds,
piece.constructeurId,
piece.constructeur,
(piece.originalPiece as AnyRecord)?.constructeurs,
(piece.originalPiece as AnyRecord)?.constructeurIds,
(piece.originalPiece as AnyRecord)?.constructeurId,
(piece.originalPiece as AnyRecord)?.constructeur,
@@ -396,7 +372,6 @@ export function useMachineDetailData(machineId: string) {
constructeurId: constructeurIds[0] || null,
typePieceId:
piece.typePieceId ||
(piece.typeMachinePieceRequirement as AnyRecord)?.typePieceId ||
(piece.typePiece as AnyRecord)?.id ||
null,
__productDisplay: productDisplay,
@@ -409,16 +384,12 @@ export function useMachineDetailData(machineId: string) {
structure ? normalizeStructureForEditor(structure as AnyRecord) : null
return (componentsData || []).map((component) => {
const requirement = (component.typeMachineComponentRequirement as AnyRecord) || {}
const type = (requirement.typeComposant as AnyRecord) || (component.typeComposant as AnyRecord) || {}
const type = (component.typeComposant as AnyRecord) || {}
const normalizedStructureDefs = [
normalizeStructureDefs((component.definition as AnyRecord)?.structure),
normalizeStructureDefs((component.typeComposant as AnyRecord)?.structure),
normalizeStructureDefs(type.structure),
normalizeStructureDefs(type.componentSkeleton),
normalizeStructureDefs(requirement.structure),
normalizeStructureDefs(requirement.componentSkeleton),
]
const actualComponent = (component.originalComposant as AnyRecord) || component
@@ -445,9 +416,6 @@ export function useMachineDetailData(machineId: string) {
normalizeExistingCustomFieldDefinitions((component.typeComposant as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(type.customFields),
normalizeExistingCustomFieldDefinitions(actualComponent?.customFields),
normalizeExistingCustomFieldDefinitions((requirement.typeComposant as AnyRecord)?.customFields),
normalizeExistingCustomFieldDefinitions(requirement.customFields),
normalizeExistingCustomFieldDefinitions((requirement.definition as AnyRecord)?.customFields),
...normalizedStructureDefs.map((def) => getStructureCustomFields(def)),
),
)
@@ -464,9 +432,11 @@ export function useMachineDetailData(machineId: string) {
: []
const constructeurIds = uniqueConstructeurIds(
component.constructeurs,
component.constructeurIds,
component.constructeurId,
component.constructeur,
actualComponent?.constructeurs,
actualComponent?.constructeurIds,
actualComponent?.constructeurId,
actualComponent?.constructeur,
@@ -505,7 +475,6 @@ export function useMachineDetailData(machineId: string) {
constructeurId: constructeurIds[0] || null,
typeComposantId:
component.typeComposantId ||
(component.typeMachineComponentRequirement as AnyRecord)?.typeComposantId ||
(component.typeComposant as AnyRecord)?.id ||
null,
__productDisplay: productDisplay,
@@ -573,6 +542,87 @@ export function useMachineDetailData(machineId: string) {
})
})
const machineDirectProducts = computed(() => {
return machineProductLinks.value.map((link) => {
const productObj = link.product as AnyRecord | string | null
let resolved: AnyRecord | null = null
let productId: string | null = null
if (typeof productObj === 'string') {
productId = productObj.split('/').pop() || null
resolved = productId ? findProductById(productId) : null
} else if (productObj && typeof productObj === 'object') {
productId = (productObj as AnyRecord)?.id as string | null
// Prefer the embedded product from the structure endpoint — it has richer
// data (typeProduct as object, supplierPrice, constructeurs) than the
// global products cache which may store typeProduct as an IRI string.
const cached = productId ? findProductById(productId) : null
resolved = productObj as AnyRecord
if (cached) {
// Merge: use embedded as base, overlay any non-null cached fields
resolved = { ...resolved, ...Object.fromEntries(
Object.entries(cached as AnyRecord).filter(([, v]) => v != null && v !== ''),
) }
// But always prefer the embedded typeProduct when it's an object
if (productObj.typeProduct && typeof productObj.typeProduct === 'object') {
resolved.typeProduct = productObj.typeProduct
}
}
}
const constructeurIds = uniqueConstructeurIds(
resolved?.constructeurs,
resolved?.constructeurIds,
)
const resolvedConstructeurs = resolveConstructeurs(
constructeurIds,
resolved?.constructeurs as any[] || [],
constructeurs.value,
)
return {
id: (resolved?.id as string) || productId || null,
linkId: (link.id as string) || (typeof link['@id'] === 'string' ? link['@id'].split('/').pop() : null) || null,
name: (resolved?.name as string) || 'Produit inconnu',
reference: (resolved?.reference as string) || null,
supplierLabel: resolvedConstructeurs.length
? resolvedConstructeurs.map((c) => c.name).filter(Boolean).join(', ') || null
: getProductSuppliersLabel(resolved),
priceLabel: resolved ? getProductPriceLabel(resolved) : null,
groupLabel: ((resolved?.typeProduct as AnyRecord)?.name as string) || '',
documents: productId ? (productDocumentsMap.value.get(productId) || []) : [],
}
})
})
const loadProductDocuments = async () => {
const productIds = machineProductLinks.value
.map((link) => {
const p = link.product as AnyRecord | string | null
if (typeof p === 'string') return p.split('/').pop() || null
return (p as AnyRecord)?.id as string | null
})
.filter((id): id is string => !!id)
const results = await Promise.allSettled(
productIds.map(async (id) => {
const result: any = await loadDocumentsByProduct(id, { updateStore: false })
if (result.success && Array.isArray(result.data)) {
return { id, docs: result.data as AnyRecord[] }
}
return { id, docs: [] }
}),
)
const map = new Map<string, AnyRecord[]>()
results.forEach((r) => {
if (r.status === 'fulfilled' && r.value.docs.length) {
map.set(r.value.id, r.value.docs)
}
})
productDocumentsMap.value = map
}
const machineDocumentsList = computed(
() => ((machine.value as AnyRecord)?.documents as AnyRecord[]) || [],
)
@@ -583,166 +633,6 @@ export function useMachineDetailData(machineId: string) {
return fields.filter((field) => shouldDisplayCustomField(field))
})
const componentRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.componentRequirements as AnyRecord[] || []
if (!reqs.length) return []
const groups = reqs.map((requirement: AnyRecord) => ({
requirement,
components: [] as AnyRecord[],
}))
const map = new Map(groups.map((g) => [g.requirement.id, g]))
flattenedComponents.value.forEach((component) => {
const reqId = component.typeMachineComponentRequirementId as string
if (reqId && map.has(reqId)) {
map.get(reqId)!.components.push({
...component,
__productDisplay: getProductDisplay(component),
})
}
})
return groups
})
const pieceRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.pieceRequirements as AnyRecord[] || []
if (!reqs.length) return []
const groups = reqs.map((requirement: AnyRecord) => ({
requirement,
pieces: [] as AnyRecord[],
}))
const map = new Map(groups.map((g) => [g.requirement.id, g]))
const collectPieces = (): AnyRecord[] => {
const collected: AnyRecord[] = []
machinePieces.value.forEach((piece) => {
collected.push({
...piece,
constructeurs: piece.constructeurs || [],
parentComponentName: null,
__productDisplay: getProductDisplay(piece),
})
})
flattenedComponents.value.forEach((component) => {
if (Array.isArray(component.pieces) && (component.pieces as AnyRecord[]).length) {
;(component.pieces as AnyRecord[]).forEach((piece) => {
collected.push({
...piece,
constructeurs: piece.constructeurs || [],
parentComponentName: component.name,
__productDisplay: getProductDisplay(piece),
})
})
}
})
return collected
}
collectPieces().forEach((piece) => {
const reqId = piece.typeMachinePieceRequirementId as string
if (reqId && map.has(reqId)) {
map.get(reqId)!.pieces.push(piece)
}
})
return groups
})
const productRequirementGroups = computed(() => {
const reqs = ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.productRequirements as AnyRecord[] || []
if (!reqs.length) return []
const componentAggregates = flattenedComponents.value || []
const pieceAggregates = collectPiecesForSkeleton()
const links = Array.isArray(machineProductLinks.value) ? machineProductLinks.value : []
return reqs.map((requirement: AnyRecord) => {
const typeProductId =
(requirement.typeProductId as string) ||
(requirement.typeProduct as AnyRecord)?.id as string ||
null
const directProducts = links
.filter((link) => {
const requirementId = resolveIdentifier(
link?.typeMachineProductRequirementId,
link?.requirementId,
)
return requirementId === requirement.id
})
.map((link) => {
const productId = resolveIdentifier(link?.productId, (link?.product as AnyRecord)?.id)
const product =
productId ? findProductById(productId as string) : (link?.product as AnyRecord) ?? null
const supplierLabel = Array.isArray((product as AnyRecord)?.constructeurs)
? ((product as AnyRecord).constructeurs as AnyRecord[])
.map((c) => c?.name)
.filter(Boolean)
.join(', ')
: (link?.constructeursLabel as string) || null
const priceValue =
(product as AnyRecord)?.supplierPrice ?? link?.supplierPrice ?? null
let priceLabel: string | null = null
if (priceValue !== undefined && priceValue !== null) {
const numericPrice = Number(priceValue)
if (!Number.isNaN(numericPrice)) {
priceLabel = `${numericPrice.toFixed(2)}`
}
}
return {
id: productId || link?.id || null,
name: (product as AnyRecord)?.name || link?.productName || productId || 'Produit',
reference: (product as AnyRecord)?.reference || link?.reference || null,
supplierLabel,
priceLabel,
}
})
let componentCount = 0
componentAggregates.forEach((component) => {
const componentTypeProductId =
(component?.product as AnyRecord)?.typeProductId ||
((component?.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
null
if (typeProductId && componentTypeProductId === typeProductId) componentCount += 1
})
let pieceCount = 0
pieceAggregates.forEach((piece) => {
const pieceTypeProductId =
(piece?.product as AnyRecord)?.typeProductId ||
((piece?.product as AnyRecord)?.typeProduct as AnyRecord)?.id ||
null
if (typeProductId && pieceTypeProductId === typeProductId) pieceCount += 1
})
return {
requirement,
directProducts,
componentCount,
pieceCount,
totalCount: directProducts.length + componentCount + pieceCount,
}
})
})
const machineDirectProducts = computed(() => {
return productRequirementGroups.value.flatMap((group: AnyRecord) =>
((group.directProducts as AnyRecord[]) || []).map((product) => ({
...product,
groupLabel:
(group.requirement as AnyRecord).label ||
((group.requirement as AnyRecord).typeProduct as AnyRecord)?.name ||
'Produit requis',
})),
)
})
// ---------------------------------------------------------------------------
// Machine field methods
// ---------------------------------------------------------------------------
@@ -784,9 +674,6 @@ export function useMachineDetailData(machineId: string) {
mergeCustomFieldValuesWithDefinitions(
valueEntries,
normalizeExistingCustomFieldDefinitions(machine.value.customFields),
normalizeExistingCustomFieldDefinitions(
(machine.value.typeMachine as AnyRecord)?.customFields,
),
),
).map((field: AnyRecord) => ({ ...field, readOnly: false }))
machineCustomFields.value = merged
@@ -977,6 +864,7 @@ export function useMachineDetailData(machineId: string) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
}
loadProductDocuments().catch(() => {})
}
}
} catch (error) {
@@ -1126,6 +1014,11 @@ export function useMachineDetailData(machineId: string) {
collapseToggleToken.value += 1
}
const toggleAllPieces = () => {
piecesCollapsed.value = !piecesCollapsed.value
pieceCollapseToggleToken.value += 1
}
// ---------------------------------------------------------------------------
// Print wrappers
// ---------------------------------------------------------------------------
@@ -1146,16 +1039,118 @@ export function useMachineDetailData(machineId: string) {
)
// ---------------------------------------------------------------------------
// Piece aggregation (used by skeleton & product requirement groups)
// Structure link management
// ---------------------------------------------------------------------------
const collectPiecesForSkeleton = (): AnyRecord[] => {
const aggregated: AnyRecord[] = []
machinePieces.value.forEach((piece) => aggregated.push(piece))
flattenedComponents.value.forEach((component) => {
;((component.pieces as AnyRecord[]) || []).forEach((piece) => aggregated.push(piece))
const reloadMachineStructure = async () => {
const result: any = await get(`/machines/${machineId}/structure`)
if (result.success) {
const machinePayload =
result.data?.machine && typeof result.data.machine === 'object'
? result.data.machine
: result.data
if (machinePayload && typeof machinePayload === 'object') {
machine.value = {
...machine.value,
...machinePayload,
documents: machinePayload.documents || (machine.value as AnyRecord)?.documents || [],
customFieldValues: machinePayload.customFieldValues || (machine.value as AnyRecord)?.customFieldValues || [],
}
const linksApplied = applyMachineLinks(result.data)
if (linksApplied && machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
machine.value.productLinks = machineProductLinks.value
}
syncMachineCustomFields()
}
}
}
const addComponentLink = async (composantId: string) => {
const result: any = await apiPost('/machine_component_links', {
machine: `/api/machines/${machineId}`,
composant: `/api/composants/${composantId}`,
})
return aggregated
if (result.success) {
toast.showSuccess('Composant ajouté à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout du composant')
}
return result
}
const removeComponentLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_component_links/${linkId}`)
if (result.success) {
toast.showSuccess('Composant retiré de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression du composant')
}
return result
}
const addPieceLink = async (pieceId: string, parentComponentLinkId?: string) => {
const payload: any = {
machine: `/api/machines/${machineId}`,
piece: `/api/pieces/${pieceId}`,
}
if (parentComponentLinkId) {
payload.parentLink = `/api/machine_component_links/${parentComponentLinkId}`
}
const result: any = await apiPost('/machine_piece_links', payload)
if (result.success) {
toast.showSuccess('Pièce ajoutée à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout de la pièce')
}
return result
}
const removePieceLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_piece_links/${linkId}`)
if (result.success) {
toast.showSuccess('Pièce retirée de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression de la pièce')
}
return result
}
const addProductLink = async (productId: string, parentComponentLinkId?: string, parentPieceLinkId?: string) => {
const payload: any = {
machine: `/api/machines/${machineId}`,
product: `/api/products/${productId}`,
}
if (parentComponentLinkId) {
payload.parentComponentLink = `/api/machine_component_links/${parentComponentLinkId}`
}
if (parentPieceLinkId) {
payload.parentPieceLink = `/api/machine_piece_links/${parentPieceLinkId}`
}
const result: any = await apiPost('/machine_product_links', payload)
if (result.success) {
toast.showSuccess('Produit ajouté à la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de l\'ajout du produit')
}
return result
}
const removeProductLink = async (linkId: string) => {
const result: any = await apiDel(`/machine_product_links/${linkId}`)
if (result.success) {
toast.showSuccess('Produit retiré de la machine')
await reloadMachineStructure()
} else {
toast.showError('Erreur lors de la suppression du produit')
}
return result
}
// ---------------------------------------------------------------------------
@@ -1165,7 +1160,7 @@ export function useMachineDetailData(machineId: string) {
const loadMachineData = async () => {
loading.value = true
try {
const machineResult: any = await get(`/machines/${machineId}/skeleton`)
const machineResult: any = await get(`/machines/${machineId}/structure`)
if (!machineResult.success) {
console.error('Machine non trouvée:', machineId, machineResult.error)
@@ -1233,6 +1228,9 @@ export function useMachineDetailData(machineId: string) {
collapseAllComponents()
// Load product documents in background
loadProductDocuments().catch(() => {})
// Wait for documents if still loading
await documentPromise
} catch (error) {
@@ -1256,7 +1254,6 @@ export function useMachineDetailData(machineId: string) {
watch(() => (machine.value as AnyRecord)?.customFieldValues, () => syncMachineCustomFields(), { deep: true })
watch(() => (machine.value as AnyRecord)?.customFields, () => syncMachineCustomFields(), { deep: true })
watch(() => ((machine.value as AnyRecord)?.typeMachine as AnyRecord)?.customFields, () => syncMachineCustomFields(), { deep: true })
watch(
() => [components.value.length, machinePieces.value.length],
() => ensurePrintSelectionEntries(),
@@ -1298,13 +1295,10 @@ export function useMachineDetailData(machineId: string) {
debug,
componentsCollapsed,
collapseToggleToken,
piecesCollapsed,
pieceCollapseToggleToken,
// Computed
machineType,
componentRequirements,
pieceRequirements,
productRequirements,
machineHasSkeletonRequirements,
componentTypeOptions,
pieceTypeOptions,
componentTypeLabelMap,
@@ -1313,12 +1307,9 @@ export function useMachineDetailData(machineId: string) {
productById,
flattenedComponents,
machinePieces,
machineDirectProducts,
machineDocumentsList,
visibleMachineCustomFields,
componentRequirementGroups,
pieceRequirementGroups,
productRequirementGroups,
machineDirectProducts,
// Product helpers
findProductById,
@@ -1331,7 +1322,6 @@ export function useMachineDetailData(machineId: string) {
findComponentById,
findPieceById,
collectConstructeurs,
collectPiecesForSkeleton,
// Transform
transformCustomFields,
@@ -1370,6 +1360,7 @@ export function useMachineDetailData(machineId: string) {
toggleEditMode,
toggleAllComponents,
collapseAllComponents,
toggleAllPieces,
// Print
printModalOpen,
@@ -1384,10 +1375,19 @@ export function useMachineDetailData(machineId: string) {
loadMachineData,
loadInitialData,
// External (needed by skeleton editor)
// Structure link management
addComponentLink,
removeComponentLink,
addPieceLink,
removePieceLink,
addProductLink,
removeProductLink,
reloadMachineStructure,
// External
constructeurs,
loadProducts,
reconfigureMachineSkeleton,
updateMachineStructure,
toast,
// Re-exports for template

View File

@@ -153,8 +153,6 @@ export const buildMachineHierarchyFromLinks = (
const appliedPiece = (link.piece && typeof link.piece === 'object' ? link.piece : {}) as AnyRecord
const originalPiece = (link.originalPiece && typeof link.originalPiece === 'object' ? link.originalPiece : null) as AnyRecord | null
const requirement = (link.typeMachinePieceRequirement || appliedPiece.typeMachinePieceRequirement || originalPiece?.typeMachinePieceRequirement || null) as AnyRecord | null
const machinePieceLinkId = normalizePieceLinkId(link)
const pieceId = resolveIdentifier(appliedPiece.id, appliedPiece.pieceId, link.pieceId)
@@ -170,11 +168,8 @@ export const buildMachineHierarchyFromLinks = (
constructeur: appliedPiece.constructeur || originalPiece?.constructeur || null,
constructeurId: appliedPiece.constructeurId || (appliedPiece.constructeur as AnyRecord)?.id || originalPiece?.constructeurId || null,
documents: Array.isArray(appliedPiece.documents) ? appliedPiece.documents : Array.isArray(originalPiece?.documents) ? originalPiece!.documents : [],
typePiece: appliedPiece.typePiece || requirement?.typePiece || null,
typePieceId: appliedPiece.typePieceId || (appliedPiece.typePiece as AnyRecord)?.id || requirement?.typePieceId || (requirement?.typePiece as AnyRecord)?.id || null,
typeMachinePieceRequirement: requirement,
typeMachinePieceRequirementId: requirement?.id || null,
requirementId: requirement?.id || null,
typePiece: appliedPiece.typePiece || null,
typePieceId: appliedPiece.typePieceId || (appliedPiece.typePiece as AnyRecord)?.id || null,
overrides,
originalPiece,
machinePieceLink: link,
@@ -186,11 +181,8 @@ export const buildMachineHierarchyFromLinks = (
parentLinkId: resolveIdentifier(link.parentLinkId, link.parentMachinePieceLinkId, appliedPiece.parentLinkId),
parentPieceLinkId: resolveIdentifier(link.parentPieceLinkId, appliedPiece.parentPieceLinkId),
parentPieceId: resolveIdentifier(appliedPiece.parentPieceId, link.parentPieceId),
parentMachineComponentRequirementId: resolveIdentifier(appliedPiece.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
parentMachinePieceRequirementId: resolveIdentifier(appliedPiece.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
definition: appliedPiece.definition || originalPiece?.definition || {},
customFields: appliedPiece.customFields || [],
skeletonOnly: !pieceId,
}
const resolvedProductId = resolveIdentifier(appliedPiece.productId, (appliedPiece.product as AnyRecord)?.id, link.productId, (link.product as AnyRecord)?.id, originalPiece?.productId, (originalPiece?.product as AnyRecord)?.id)
@@ -215,8 +207,6 @@ export const buildMachineHierarchyFromLinks = (
const appliedComponent = (link.composant && typeof link.composant === 'object' ? link.composant : {}) as AnyRecord
const originalComponent = (link.originalComposant && typeof link.originalComposant === 'object' ? link.originalComposant : null) as AnyRecord | null
const requirement = (link.typeMachineComponentRequirement || appliedComponent.typeMachineComponentRequirement || originalComponent?.typeMachineComponentRequirement || null) as AnyRecord | null
const machineComponentLinkId = normalizeComponentLinkId(link)
const composantId = resolveIdentifier(appliedComponent.id, appliedComponent.composantId, link.composantId)
@@ -245,11 +235,8 @@ export const buildMachineHierarchyFromLinks = (
constructeur: appliedComponent.constructeur || originalComponent?.constructeur || null,
constructeurId: appliedComponent.constructeurId || (appliedComponent.constructeur as AnyRecord)?.id || originalComponent?.constructeurId || null,
documents: Array.isArray(appliedComponent.documents) ? appliedComponent.documents : Array.isArray(originalComponent?.documents) ? originalComponent!.documents : [],
typeComposant: appliedComponent.typeComposant || requirement?.typeComposant || null,
typeComposantId: appliedComponent.typeComposantId || (appliedComponent.typeComposant as AnyRecord)?.id || requirement?.typeComposantId || (requirement?.typeComposant as AnyRecord)?.id || null,
typeMachineComponentRequirement: requirement,
typeMachineComponentRequirementId: requirement?.id || null,
requirementId: requirement?.id || null,
typeComposant: appliedComponent.typeComposant || null,
typeComposantId: appliedComponent.typeComposantId || (appliedComponent.typeComposant as AnyRecord)?.id || null,
overrides: compOverrides,
machineComponentLinkOverrides: compOverrides,
definitionOverrides: compOverrides,
@@ -259,16 +246,12 @@ export const buildMachineHierarchyFromLinks = (
componentLinkId: machineComponentLinkId,
parentComponentLinkId: resolveIdentifier(link.parentComponentLinkId, link.parentLinkId, link.parentMachineComponentLinkId, appliedComponent.parentComponentLinkId),
parentComposantId: resolveIdentifier(appliedComponent.parentComposantId, link.parentComponentId),
parentRequirementId: resolveIdentifier(appliedComponent.parentRequirementId, link.parentRequirementId),
parentMachineComponentRequirementId: resolveIdentifier(appliedComponent.parentMachineComponentRequirementId, link.parentMachineComponentRequirementId),
parentMachinePieceRequirementId: resolveIdentifier(appliedComponent.parentMachinePieceRequirementId, link.parentMachinePieceRequirementId),
definition: appliedComponent.definition || originalComponent?.definition || {},
customFields: appliedComponent.customFields || [],
pieces,
subComponents,
subcomponents: subComponents,
sousComposants: subComponents,
skeletonOnly: !composantId,
}
const constructeurs = collectConstructeurs(allConstructeurs, appliedComponent.constructeurs, appliedComponent.constructeur, appliedComponent.constructeurIds, appliedComponent.constructeurId, originalComponent?.constructeurs, originalComponent?.constructeur, originalComponent?.constructeurIds, originalComponent?.constructeurId)

View File

@@ -1,838 +0,0 @@
/**
* Machine skeleton editor — selection state, validation & save logic.
*
* Extracted from pages/machine/[id].vue (F1.1).
* Manages the reactive selection state for component / piece / product
* skeleton requirements, validation, and reconfiguration API calls.
*/
import { ref, reactive, computed } from 'vue'
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
import {
resolveIdentifier,
extractParentLinkIdentifiers,
} from '~/shared/utils/productDisplayUtils'
import {
uniqueConstructeurIds,
} from '~/shared/constructeurUtils'
import { resolveLinkArray } from '~/composables/useMachineHierarchy'
import type { Ref, ComputedRef } from 'vue'
type AnyRecord = Record<string, unknown>
export interface MachineSkeletonEditorDeps {
machine: Ref<AnyRecord | null>
components: Ref<AnyRecord[]>
pieces: Ref<AnyRecord[]>
machineComponentLinks: Ref<AnyRecord[]>
machinePieceLinks: Ref<AnyRecord[]>
machineProductLinks: Ref<AnyRecord[]>
machineType: ComputedRef<AnyRecord | null>
machineHasSkeletonRequirements: ComputedRef<boolean>
componentRequirements: ComputedRef<AnyRecord[]>
pieceRequirements: ComputedRef<AnyRecord[]>
productRequirements: ComputedRef<AnyRecord[]>
componentTypeLabelMap: ComputedRef<Map<string, string>>
pieceTypeLabelMap: ComputedRef<Map<string, string>>
productInventory: ComputedRef<AnyRecord[]>
flattenedComponents: ComputedRef<AnyRecord[]>
machinePieces: ComputedRef<AnyRecord[]>
machineDocumentsLoaded: Ref<boolean>
findProductById: (id: string | null | undefined) => AnyRecord | null
findComponentById: (items: AnyRecord[] | undefined, id: string) => AnyRecord | null
findPieceById: (id: string) => AnyRecord | null
transformCustomFields: (pieces: AnyRecord[]) => AnyRecord[]
transformComponentCustomFields: (components: AnyRecord[]) => AnyRecord[]
applyMachineLinks: (source: AnyRecord) => boolean
collapseAllComponents: () => void
initMachineFields: () => void
collectPiecesForSkeleton: () => AnyRecord[]
constructeurs: Ref<AnyRecord[]>
loadProducts: () => Promise<void>
reconfigureMachineSkeleton: (id: string, payload: AnyRecord) => Promise<AnyRecord>
toast: { showError: (msg: string) => void; showInfo: (msg: string) => void }
}
export function useMachineSkeletonEditor(deps: MachineSkeletonEditorDeps) {
const {
machine,
components,
pieces,
machineComponentLinks,
machinePieceLinks,
machineProductLinks,
machineType,
machineHasSkeletonRequirements,
productRequirements,
componentTypeLabelMap,
pieceTypeLabelMap,
productInventory,
flattenedComponents,
machineDocumentsLoaded,
findProductById,
findComponentById,
findPieceById,
transformCustomFields,
transformComponentCustomFields,
applyMachineLinks,
collapseAllComponents,
initMachineFields,
collectPiecesForSkeleton,
loadProducts,
reconfigureMachineSkeleton,
toast,
} = deps
// ---------------------------------------------------------------------------
// View state
// ---------------------------------------------------------------------------
const activeMachineView = ref<'details' | 'skeleton'>('details')
const isDetailsView = computed(() => activeMachineView.value === 'details')
const isSkeletonView = computed(() => activeMachineView.value === 'skeleton')
// ---------------------------------------------------------------------------
// Editor state
// ---------------------------------------------------------------------------
const skeletonEditor = reactive({
open: false,
loading: false,
submitting: false,
})
const componentRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
const pieceRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
const productRequirementSelections = reactive<Record<string, AnyRecord[]>>({})
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const isPlainObject = (value: unknown): boolean =>
Object.prototype.toString.call(value) === '[object Object]'
const getComponentRequirementEntries = (requirementId: string): AnyRecord[] =>
componentRequirementSelections[requirementId] || []
const getPieceRequirementEntries = (requirementId: string): AnyRecord[] =>
pieceRequirementSelections[requirementId] || []
const getProductRequirementEntries = (requirementId: string): AnyRecord[] =>
productRequirementSelections[requirementId] || []
// ---------------------------------------------------------------------------
// Label resolvers
// ---------------------------------------------------------------------------
const resolveComponentRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => {
const typeId = (entry?.typeComposantId || requirement?.typeComposantId || null) as string | null
if (!typeId) return ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini'
return componentTypeLabelMap.value.get(typeId) || ((requirement?.typeComposant as AnyRecord)?.name as string) || 'Type non défini'
}
const resolvePieceRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => {
const typeId = (entry?.typePieceId || requirement?.typePieceId || null) as string | null
if (!typeId) return ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini'
return pieceTypeLabelMap.value.get(typeId) || ((requirement?.typePiece as AnyRecord)?.name as string) || 'Type non défini'
}
const resolveProductRequirementTypeLabel = (requirement: AnyRecord, entry: AnyRecord): string => {
const typeId =
(entry?.typeProductId as string) ||
(requirement?.typeProductId as string) ||
((requirement?.typeProduct as AnyRecord)?.id as string) ||
null
if (typeId) {
const typeMatch = productRequirements.value.find(
(req: AnyRecord) =>
req.typeProductId === typeId || (req.typeProduct as AnyRecord)?.id === typeId,
)
if (typeMatch && (typeMatch.typeProduct as AnyRecord)?.name) {
return (typeMatch.typeProduct as AnyRecord).name as string
}
}
return ((requirement?.typeProduct as AnyRecord)?.name as string) || 'Catégorie non définie'
}
const getProductOptionsForRequirement = (requirement: AnyRecord): AnyRecord[] => {
const requirementTypeId =
(requirement?.typeProductId as string) ||
((requirement?.typeProduct as AnyRecord)?.id as string) ||
null
return (productInventory.value as AnyRecord[]).filter((product) => {
if (!product?.id) return false
if (!requirementTypeId) return true
const productTypeId =
(product.typeProductId as string) ||
((product.typeProduct as AnyRecord)?.id as string) ||
null
return productTypeId === requirementTypeId
})
}
// ---------------------------------------------------------------------------
// Selection entry factories
// ---------------------------------------------------------------------------
const createComponentSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => {
const link = (source?.machineComponentLink as AnyRecord) || null
const entry: AnyRecord = {
linkId: resolveIdentifier(link?.id, source?.machineComponentLinkId, source?.linkId),
composantId: resolveIdentifier(source?.composantId, source?.componentId, source?.id),
parentLinkId: resolveIdentifier(link?.parentLinkId, link?.parentComponentLinkId, source?.parentComponentLinkId, source?.parentLinkId),
parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId),
parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId),
parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId),
parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId),
parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId),
typeComposantId:
(source?.typeMachineComponentRequirement as AnyRecord)?.typeComposantId ||
source?.typeComposantId ||
(source?.typeComposant as AnyRecord)?.id ||
requirement?.typeComposantId ||
null,
definition: {
name: source?.name || source?.nom || (requirement?.typeComposant as AnyRecord)?.name || '',
reference: source?.reference || '',
constructeurIds: [] as string[],
constructeurId: null as string | null,
prix: source?.prix ?? source?.price ?? null,
},
}
const defConstructeurIds = uniqueConstructeurIds(
(link?.overrides as AnyRecord)?.constructeurIds,
(link?.overrides as AnyRecord)?.constructeurId,
source?.constructeurIds,
source?.constructeurId,
source?.constructeur,
)
;(entry.definition as AnyRecord).constructeurIds = defConstructeurIds
;(entry.definition as AnyRecord).constructeurId = defConstructeurIds[0] || null
if (link?.overrides && isPlainObject(link.overrides)) {
entry.definition = { ...(entry.definition as AnyRecord), ...(link.overrides as AnyRecord) }
}
const finalConstructeurIds = uniqueConstructeurIds(
(entry.definition as AnyRecord).constructeurIds,
(entry.definition as AnyRecord).constructeurId,
(entry.definition as AnyRecord).constructeur,
)
;(entry.definition as AnyRecord).constructeurIds = finalConstructeurIds
;(entry.definition as AnyRecord).constructeurId = finalConstructeurIds[0] || null
return entry
}
const createPieceSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => {
const link = (source?.machinePieceLink as AnyRecord) || null
const entry: AnyRecord = {
linkId: resolveIdentifier(link?.id, source?.machinePieceLinkId, source?.linkId),
pieceId: resolveIdentifier(source?.pieceId, source?.id),
parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId),
parentComponentLinkId: resolveIdentifier(link?.parentComponentLinkId, source?.parentComponentLinkId, source?.machineComponentLinkId),
parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId),
parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId),
parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId),
parentComponentId: resolveIdentifier(link?.parentComponentId, source?.parentComponentId, source?.composantId),
parentPieceId: resolveIdentifier(link?.parentPieceId, source?.parentPieceId),
composantId: resolveIdentifier(source?.composantId, link?.composantId, link?.componentId),
typePieceId:
(source?.typeMachinePieceRequirement as AnyRecord)?.typePieceId ||
source?.typePieceId ||
(source?.typePiece as AnyRecord)?.id ||
requirement?.typePieceId ||
null,
definition: {
name: source?.name || source?.nom || (requirement?.typePiece as AnyRecord)?.name || '',
reference: source?.reference || '',
constructeurIds: [] as string[],
constructeurId: null as string | null,
prix: source?.prix ?? source?.price ?? null,
},
}
const defConstructeurIds = uniqueConstructeurIds(
(link?.overrides as AnyRecord)?.constructeurIds,
(link?.overrides as AnyRecord)?.constructeurId,
source?.constructeurIds,
source?.constructeurId,
source?.constructeur,
)
;(entry.definition as AnyRecord).constructeurIds = defConstructeurIds
;(entry.definition as AnyRecord).constructeurId = defConstructeurIds[0] || null
if (link?.overrides && isPlainObject(link.overrides)) {
entry.definition = { ...(entry.definition as AnyRecord), ...(link.overrides as AnyRecord) }
}
const finalConstructeurIds = uniqueConstructeurIds(
(entry.definition as AnyRecord).constructeurIds,
(entry.definition as AnyRecord).constructeurId,
(entry.definition as AnyRecord).constructeur,
)
;(entry.definition as AnyRecord).constructeurIds = finalConstructeurIds
;(entry.definition as AnyRecord).constructeurId = finalConstructeurIds[0] || null
return entry
}
const createProductSelectionEntry = (requirement: AnyRecord, source: AnyRecord | null = null): AnyRecord => {
const link = (source?.machineProductLink as AnyRecord) || source || null
return {
linkId: resolveIdentifier(link?.id, source?.machineProductLinkId, source?.linkId),
productId: resolveIdentifier(source?.productId, link?.productId),
parentLinkId: resolveIdentifier(link?.parentLinkId, source?.parentLinkId),
parentComponentLinkId: resolveIdentifier(link?.parentComponentLinkId, source?.parentComponentLinkId),
parentPieceLinkId: resolveIdentifier(link?.parentPieceLinkId, source?.parentPieceLinkId),
parentRequirementId: resolveIdentifier(link?.parentRequirementId, source?.parentRequirementId, requirement?.parentRequirementId),
parentComponentRequirementId: resolveIdentifier(link?.parentComponentRequirementId, source?.parentComponentRequirementId, requirement?.parentComponentRequirementId),
parentPieceRequirementId: resolveIdentifier(link?.parentPieceRequirementId, source?.parentPieceRequirementId, requirement?.parentPieceRequirementId),
parentMachineComponentRequirementId: resolveIdentifier(link?.parentMachineComponentRequirementId, source?.parentMachineComponentRequirementId, requirement?.parentMachineComponentRequirementId),
parentMachinePieceRequirementId: resolveIdentifier(link?.parentMachinePieceRequirementId, source?.parentMachinePieceRequirementId, requirement?.parentMachinePieceRequirementId),
typeProductId: resolveIdentifier(link?.typeProductId, source?.typeProductId, requirement?.typeProductId, (requirement?.typeProduct as AnyRecord)?.id),
}
}
// ---------------------------------------------------------------------------
// Selection CRUD
// ---------------------------------------------------------------------------
const resetSkeletonRequirementSelections = () => {
Object.keys(componentRequirementSelections).forEach((k) => delete componentRequirementSelections[k])
Object.keys(pieceRequirementSelections).forEach((k) => delete pieceRequirementSelections[k])
Object.keys(productRequirementSelections).forEach((k) => delete productRequirementSelections[k])
}
const addComponentSelectionEntry = (requirement: AnyRecord) => {
const entries = getComponentRequirementEntries(requirement.id as string)
const max = (requirement.maxCount as number | null) ?? 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) => {
const entries = getComponentRequirementEntries(requirementId)
componentRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setComponentRequirementType = (requirementId: string, index: number, value: string | null) => {
const entry = getComponentRequirementEntries(requirementId)[index]
if (!entry) return
entry.typeComposantId = value || null
}
const setComponentRequirementConstructeur = (requirementId: string, index: number, value: unknown) => {
const entry = getComponentRequirementEntries(requirementId)[index]
if (!entry) return
const ids = uniqueConstructeurIds(value)
;(entry.definition as AnyRecord).constructeurIds = ids
;(entry.definition as AnyRecord).constructeurId = ids[0] || null
}
const addPieceSelectionEntry = (requirement: AnyRecord) => {
const entries = getPieceRequirementEntries(requirement.id as string)
const max = (requirement.maxCount as number | null) ?? 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),
]
}
const removePieceSelectionEntry = (requirementId: string, index: number) => {
const entries = getPieceRequirementEntries(requirementId)
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setPieceRequirementType = (requirementId: string, index: number, value: string | null) => {
const entry = getPieceRequirementEntries(requirementId)[index]
if (!entry) return
entry.typePieceId = value || null
}
const setPieceRequirementConstructeur = (requirementId: string, index: number, value: unknown) => {
const entry = getPieceRequirementEntries(requirementId)[index]
if (!entry) return
const ids = uniqueConstructeurIds(value)
;(entry.definition as AnyRecord).constructeurIds = ids
;(entry.definition as AnyRecord).constructeurId = ids[0] || null
}
const addProductSelectionEntry = (requirement: AnyRecord) => {
const entries = getProductRequirementEntries(requirement.id as string)
const max = (requirement.maxCount as number | null) ?? 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) => {
const entries = getProductRequirementEntries(requirementId)
productRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const setProductRequirementProduct = (requirementId: string, index: number, productId: string | null) => {
const entry = getProductRequirementEntries(requirementId)[index]
if (!entry) return
const normalizedProductId = productId || null
entry.productId = normalizedProductId
if (normalizedProductId) {
const product = findProductById(normalizedProductId)
entry.typeProductId =
(product?.typeProductId as string) ||
((product?.typeProduct as AnyRecord)?.id as string) ||
(entry.typeProductId as string) ||
null
}
}
const setProductRequirementType = (requirementId: string, index: number, value: string | null) => {
const entry = getProductRequirementEntries(requirementId)[index]
if (!entry) return
entry.typeProductId = value || entry.typeProductId || null
}
// ---------------------------------------------------------------------------
// Skeleton initialization
// ---------------------------------------------------------------------------
const initializeSkeletonRequirementSelections = async () => {
skeletonEditor.loading = true
try {
resetSkeletonRequirementSelections()
const type = machineType.value as AnyRecord
if (!type) return
try {
await loadProducts()
} catch (error) {
console.error('Erreur lors du chargement des produits pour le squelette:', error)
}
;((type.componentRequirements as AnyRecord[]) || []).forEach((requirement) => {
const existing = flattenedComponents.value.filter(
(c) => c.typeMachineComponentRequirementId === requirement.id,
)
const entries = existing.map((c) => createComponentSelectionEntry(requirement, c))
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
while (entries.length < min) entries.push(createComponentSelectionEntry(requirement))
if (entries.length) componentRequirementSelections[requirement.id as string] = entries
})
const allPieces = collectPiecesForSkeleton()
;((type.pieceRequirements as AnyRecord[]) || []).forEach((requirement) => {
const existing = allPieces.filter(
(p) => p.typeMachinePieceRequirementId === requirement.id,
)
const entries = existing.map((p) => createPieceSelectionEntry(requirement, p))
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
while (entries.length < min) entries.push(createPieceSelectionEntry(requirement))
if (entries.length) pieceRequirementSelections[requirement.id as string] = entries
})
const existingProductLinks = Array.isArray(machineProductLinks.value)
? machineProductLinks.value
: Array.isArray(machine.value?.productLinks)
? (machine.value.productLinks as AnyRecord[])
: []
;((type.productRequirements as AnyRecord[]) || []).forEach((requirement) => {
const matches = existingProductLinks.filter((link) => {
const reqId = resolveIdentifier(link?.typeMachineProductRequirementId, link?.requirementId)
return reqId === requirement.id
})
const entries = matches.map((link) => createProductSelectionEntry(requirement, link))
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
while (entries.length < min) entries.push(createProductSelectionEntry(requirement))
if (entries.length) productRequirementSelections[requirement.id as string] = entries
})
} finally {
skeletonEditor.loading = false
}
}
// ---------------------------------------------------------------------------
// Editor open/close
// ---------------------------------------------------------------------------
const openSkeletonEditor = async () => {
if (skeletonEditor.open) return
skeletonEditor.open = true
await initializeSkeletonRequirementSelections()
}
const closeSkeletonEditor = () => {
if (!skeletonEditor.open) return
if (skeletonEditor.submitting) return
skeletonEditor.open = false
skeletonEditor.loading = false
skeletonEditor.submitting = false
resetSkeletonRequirementSelections()
}
const changeMachineView = async (view: 'details' | 'skeleton') => {
if (view === activeMachineView.value) return
if (view === 'skeleton') {
if (!machineHasSkeletonRequirements.value) {
toast.showInfo('Aucun squelette configuré pour cette machine.')
return
}
activeMachineView.value = 'skeleton'
if (!skeletonEditor.open) {
try {
await openSkeletonEditor()
} catch (error) {
console.error("Impossible d'ouvrir l'éditeur de squelette:", error)
toast.showError('Impossible de charger les éléments du squelette.')
activeMachineView.value = 'details'
}
}
return
}
closeSkeletonEditor()
activeMachineView.value = 'details'
}
// ---------------------------------------------------------------------------
// Validation & save
// ---------------------------------------------------------------------------
const computeSkeletonProductUsage = (type: AnyRecord): Map<string, number> => {
const usage = new Map<string, number>()
const increment = (typeProductId: string | null) => {
if (!typeProductId) return
usage.set(typeProductId, (usage.get(typeProductId) ?? 0) + 1)
}
for (const requirement of (type.componentRequirements as AnyRecord[]) || []) {
getComponentRequirementEntries(requirement.id as string).forEach((entry) => {
if (!entry?.composantId) return
const component = findComponentById(components.value, entry.composantId as string)
const typeProductId =
((component?.product as AnyRecord)?.typeProductId as string) ||
(((component?.product as AnyRecord)?.typeProduct as AnyRecord)?.id as string) ||
null
increment(typeProductId)
})
}
for (const requirement of (type.pieceRequirements as AnyRecord[]) || []) {
getPieceRequirementEntries(requirement.id as string).forEach((entry) => {
if (!entry?.pieceId) return
const piece = findPieceById(entry.pieceId as string)
const typeProductId =
((piece?.product as AnyRecord)?.typeProductId as string) ||
(((piece?.product as AnyRecord)?.typeProduct as AnyRecord)?.id as string) ||
null
increment(typeProductId)
})
}
for (const requirement of (type.productRequirements as AnyRecord[]) || []) {
getProductRequirementEntries(requirement.id as string).forEach((entry) => {
if (!entry?.productId) return
const product = findProductById(entry.productId as string)
const typeProductId =
((product?.typeProductId as string) ||
((product?.typeProduct as AnyRecord)?.id as string) ||
(entry?.typeProductId as string) ||
(requirement?.typeProductId as string) ||
((requirement?.typeProduct as AnyRecord)?.id as string) ||
null)
increment(typeProductId)
})
}
return usage
}
const validateSkeletonSelections = (type: AnyRecord) => {
const errors: string[] = []
const componentLinksPayload: AnyRecord[] = []
const pieceLinksPayload: AnyRecord[] = []
const productLinksPayload: AnyRecord[] = []
for (const requirement of (type.componentRequirements as AnyRecord[]) || []) {
const entries = getComponentRequirementEntries(requirement.id as string)
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
const max = (requirement.maxCount as number | null) ?? 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) => {
const resolvedTypeId = (entry.typeComposantId || requirement.typeComposantId || null) as string | null
if (!resolvedTypeId) {
errors.push(`Le groupe "${requirement.label || (requirement.typeComposant as AnyRecord)?.name || 'Composants'}" nécessite un type de composant.`)
return
}
const payload: AnyRecord = { requirementId: requirement.id, typeComposantId: resolvedTypeId }
if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId }
if (entry.composantId) payload.composantId = entry.composantId
const overrides = sanitizeDefinitionOverrides(entry.definition)
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 = getPieceRequirementEntries(requirement.id as string)
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
const max = (requirement.maxCount as number | null) ?? 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) => {
const resolvedTypeId = (entry.typePieceId || requirement.typePieceId || null) as string | null
if (!resolvedTypeId) {
errors.push(`Le groupe "${requirement.label || (requirement.typePiece as AnyRecord)?.name || 'Pièces'}" nécessite un type de pièce.`)
return
}
const payload: AnyRecord = { requirementId: requirement.id, typePieceId: resolvedTypeId }
if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId }
if (entry.pieceId) payload.pieceId = entry.pieceId
if (entry.composantId) payload.composantId = entry.composantId
const overrides = sanitizeDefinitionOverrides(entry.definition)
if (overrides) payload.overrides = overrides
Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
pieceLinksPayload.push(payload)
})
}
const productUsage = computeSkeletonProductUsage(type)
for (const requirement of (type.productRequirements as AnyRecord[]) || []) {
const entries = getProductRequirementEntries(requirement.id as string)
const max = (requirement.maxCount as number | null) ?? null
if (max !== null && entries.length > max) {
errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} sélection(s) directe(s).`)
}
const typeProductId = (requirement.typeProductId as string) || ((requirement.typeProduct as AnyRecord)?.id as string) || null
const count = typeProductId ? productUsage.get(typeProductId) ?? 0 : 0
const min = (requirement.minCount as number) ?? (requirement.required ? 1 : 0)
if (count < min) {
errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" nécessite au moins ${min} sélection(s).`)
}
if (max !== null && count > max) {
errors.push(`Le groupe "${requirement.label || (requirement.typeProduct as AnyRecord)?.name || 'Produits'}" ne peut dépasser ${max} sélection(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 = findProductById(entry.productId as string)
if (!product) {
errors.push(`Le produit sélectionné est introuvable (ID: ${entry.productId}).`)
return
}
const productTypeId =
(product.typeProductId as string) ||
((product.typeProduct as AnyRecord)?.id as string) ||
(entry.typeProductId as string) ||
null
if (typeProductId && productTypeId && productTypeId !== typeProductId) {
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 }
if (entry.linkId) { payload.id = entry.linkId; payload.linkId = entry.linkId }
if (entry.typeProductId) payload.typeProductId = entry.typeProductId
Object.assign(payload, extractParentLinkIdentifiers(requirement), extractParentLinkIdentifiers(entry))
productLinksPayload.push(payload)
})
}
if (errors.length > 0) return { valid: false as const, error: errors[0] }
return {
valid: true as const,
componentLinks: componentLinksPayload,
pieceLinks: pieceLinksPayload,
productLinks: productLinksPayload,
}
}
// ---------------------------------------------------------------------------
// Apply reconfiguration result
// ---------------------------------------------------------------------------
const applySkeletonReconfigurationResult = async (data: AnyRecord) => {
if (!data) return
const updatedMachine = (data.machine as AnyRecord) || data
if (updatedMachine) {
machine.value = {
...machine.value,
...updatedMachine,
documents: (updatedMachine.documents as AnyRecord[]) || (machine.value?.documents as AnyRecord[]) || [],
}
initMachineFields()
machineDocumentsLoaded.value = !!((machine.value!.documents as AnyRecord[])?.length)
}
const linksApplied = applyMachineLinks(data) || applyMachineLinks(updatedMachine)
if (linksApplied) {
if (machine.value) {
machine.value.componentLinks = machineComponentLinks.value
machine.value.pieceLinks = machinePieceLinks.value
machine.value.productLinks = machineProductLinks.value
}
collapseAllComponents()
return
}
const newComponents = (data.components ?? updatedMachine?.components ?? null) as AnyRecord[] | null
if (Array.isArray(newComponents)) {
components.value = transformComponentCustomFields(newComponents)
collapseAllComponents()
}
const newPieces = (data.pieces ?? updatedMachine?.pieces ?? null) as AnyRecord[] | null
if (Array.isArray(newPieces)) {
pieces.value = transformCustomFields(newPieces)
}
const prodLinks =
resolveLinkArray(data, ['productLinks', 'machineProductLinks']) ??
resolveLinkArray(updatedMachine, ['productLinks', 'machineProductLinks'])
if (Array.isArray(prodLinks)) {
machineProductLinks.value = prodLinks as AnyRecord[]
if (machine.value) machine.value.productLinks = prodLinks
}
}
// ---------------------------------------------------------------------------
// Save
// ---------------------------------------------------------------------------
const saveSkeletonConfiguration = async () => {
if (!machine.value?.id) return
const type = machineType.value as AnyRecord
let payload: AnyRecord = { componentLinks: [], pieceLinks: [], productLinks: [] }
if (type && machineHasSkeletonRequirements.value) {
const validation = validateSkeletonSelections(type)
if (!validation.valid) {
toast.showError((validation as AnyRecord).error as string)
return
}
payload = {
componentLinks: (validation as AnyRecord).componentLinks,
pieceLinks: (validation as AnyRecord).pieceLinks,
productLinks: (validation as AnyRecord).productLinks,
}
}
skeletonEditor.submitting = true
try {
const result = await reconfigureMachineSkeleton(machine.value.id as string, payload)
if ((result as AnyRecord).success) {
await applySkeletonReconfigurationResult((result as AnyRecord).data as AnyRecord)
await changeMachineView('details')
} else if ((result as AnyRecord).error) {
toast.showError((result as AnyRecord).error as string)
}
} catch (error) {
console.error('Erreur lors de la reconfiguration du squelette de la machine:', error)
toast.showError('Erreur lors de la mise à jour des éléments du squelette')
} finally {
skeletonEditor.submitting = false
skeletonEditor.loading = false
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
return {
// View state
activeMachineView,
isDetailsView,
isSkeletonView,
// Editor state
skeletonEditor,
componentRequirementSelections,
pieceRequirementSelections,
productRequirementSelections,
// Entry getters
getComponentRequirementEntries,
getPieceRequirementEntries,
getProductRequirementEntries,
// Label resolvers
resolveComponentRequirementTypeLabel,
resolvePieceRequirementTypeLabel,
resolveProductRequirementTypeLabel,
getProductOptionsForRequirement,
// Selection CRUD
addComponentSelectionEntry,
removeComponentSelectionEntry,
setComponentRequirementType,
setComponentRequirementConstructeur,
addPieceSelectionEntry,
removePieceSelectionEntry,
setPieceRequirementType,
setPieceRequirementConstructeur,
addProductSelectionEntry,
removeProductSelectionEntry,
setProductRequirementProduct,
setProductRequirementType,
// Editor lifecycle
openSkeletonEditor,
closeSkeletonEditor,
changeMachineView,
initializeSkeletonRequirementSelections,
// Validation & save
validateSkeletonSelections,
saveSkeletonConfiguration,
}
}

View File

@@ -1,186 +0,0 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi, type ApiResponse } from './useApi'
import { extractRelationId } from '~/shared/apiRelations'
import { extractCollection } from '~/shared/utils/apiHelpers'
export interface MachineTypeRequirement {
id?: string
label?: string
minCount?: number
maxCount?: number
required?: boolean
[key: string]: unknown
}
export interface MachineType {
id: string
name: string
componentRequirements: MachineTypeRequirement[]
pieceRequirements: MachineTypeRequirement[]
productRequirements: MachineTypeRequirement[]
[key: string]: unknown
}
const machineTypes = ref<MachineType[]>([])
const loading = ref(false)
const loaded = ref(false)
const normalizeRequirementList = (value: unknown, relationKey: string): MachineTypeRequirement[] => {
if (!Array.isArray(value)) {
return []
}
return value.map((entry: Record<string, unknown>, _index: number) => {
if (!entry || typeof entry !== 'object') {
return entry
}
const normalized = { ...entry }
const relationField = relationKey.replace('Id', '')
const relationValue = normalized[relationField]
if (relationKey && !normalized[relationKey]) {
const relationId = extractRelationId(relationValue)
if (relationId) {
normalized[relationKey] = relationId
}
}
return normalized as MachineTypeRequirement
})
}
const normalizeMachineType = (type: Record<string, unknown>): MachineType | null => {
if (!type || typeof type !== 'object') {
return null
}
return {
...type,
componentRequirements: normalizeRequirementList(type.componentRequirements, 'typeComposantId'),
pieceRequirements: normalizeRequirementList(type.pieceRequirements, 'typePieceId'),
productRequirements: normalizeRequirementList(type.productRequirements, 'typeProductId'),
} as MachineType
}
export function useMachineTypesApi() {
const { showSuccess } = useToast()
const { get, post, put, delete: del } = useApi()
const loadMachineTypes = async (options: { force?: boolean } = {}): Promise<void> => {
if (!options.force && loaded.value) return
loading.value = true
try {
const result = await get('/type_machines')
if (result.success) {
const items = extractCollection(result.data)
machineTypes.value = items
.map((item) => normalizeMachineType(item as Record<string, unknown>))
.filter((item): item is MachineType => item !== null)
loaded.value = true
}
} catch (error) {
console.error('Erreur lors du chargement des types de machines:', error)
} finally {
loading.value = false
}
}
const createMachineType = async (typeData: Partial<MachineType>): Promise<ApiResponse> => {
loading.value = true
try {
const result = await post('/type_machines', typeData)
if (result.success) {
const normalized = normalizeMachineType(result.data as Record<string, unknown>)
if (normalized) machineTypes.value.push(normalized)
showSuccess(`Type de machine "${typeData.name}" créé avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la création du type de machine:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const updateMachineType = async (id: string, typeData: Partial<MachineType>): Promise<ApiResponse> => {
loading.value = true
try {
const result = await put(`/type_machines/${id}`, typeData)
if (result.success) {
const normalized = normalizeMachineType(result.data as Record<string, unknown>)
const index = machineTypes.value.findIndex((type) => type.id === id)
if (index !== -1 && normalized) {
machineTypes.value[index] = normalized
}
showSuccess(`Type de machine "${typeData.name}" mis à jour avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la mise à jour du type de machine:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const deleteMachineType = async (id: string): Promise<ApiResponse> => {
loading.value = true
try {
const result = await del(`/type_machines/${id}`)
if (result.success) {
const deletedType = machineTypes.value.find((type) => type.id === id)
machineTypes.value = machineTypes.value.filter((type) => type.id !== id)
showSuccess(`Type de machine "${deletedType?.name || 'inconnu'}" supprimé avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors de la suppression du type de machine:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const getMachineTypeById = async (id: string, forceRefresh = false): Promise<ApiResponse> => {
// D'abord chercher dans le cache local (sauf si forceRefresh)
if (!forceRefresh) {
const localType = machineTypes.value.find((type) => type.id === id)
if (localType) {
return { success: true, data: localType }
}
}
// Récupérer depuis l'API
try {
const result = await get(`/type_machines/${id}`)
if (result.success) {
const normalized = normalizeMachineType(result.data as Record<string, unknown>)
// Mettre à jour le cache local
const index = machineTypes.value.findIndex((type) => type.id === id)
if (index !== -1 && normalized) {
machineTypes.value[index] = normalized
} else if (normalized) {
machineTypes.value.push(normalized)
}
return { success: true, data: normalized }
}
return result
} catch (error) {
console.error('Erreur lors de la récupération du type de machine:', error)
return { success: false, error: (error as Error).message }
}
}
const getMachineTypes = (): MachineType[] => machineTypes.value
const isLoading = (): boolean => loading.value
return {
machineTypes,
loading,
loadMachineTypes,
createMachineType,
updateMachineType,
deleteMachineType,
getMachineTypeById,
getMachineTypes,
isLoading,
}
}

View File

@@ -9,7 +9,6 @@ export interface Machine {
id: string
name?: string
siteId?: string | null
typeMachineId?: string | null
componentLinks?: unknown[]
pieceLinks?: unknown[]
[key: string]: unknown
@@ -53,13 +52,6 @@ const normalizeMachineResponse = (payload: unknown): Machine | null => {
}
}
if (!normalized.typeMachineId) {
const typeMachineId = extractRelationId(container.typeMachine)
if (typeMachineId) {
normalized.typeMachineId = typeMachineId
}
}
const componentLinks = resolveLinkCollection(raw, ['componentLinks', 'machineComponentLinks']) ??
resolveLinkCollection(container, ['componentLinks', 'machineComponentLinks']) ??
[]
@@ -121,15 +113,6 @@ export function useMachines() {
}
}
const createMachineFromType = async (machineData: Partial<Machine>, typeMachine: { id: string }): Promise<ApiResponse> => {
const machineWithStructure = {
...machineData,
typeMachineId: typeMachine.id,
}
return await createMachine(machineWithStructure)
}
const updateMachineData = async (id: string, machineData: Partial<Machine>): Promise<ApiResponse> => {
loading.value = true
try {
@@ -157,14 +140,14 @@ export function useMachines() {
}
}
const reconfigureSkeleton = async (machineId: string, payload: unknown): Promise<ApiResponse> => {
const updateStructure = async (machineId: string, payload: unknown): Promise<ApiResponse> => {
if (!machineId) {
return { success: false, error: 'Identifiant de machine manquant' }
}
loading.value = true
try {
const result = await patch(`/machines/${machineId}/skeleton`, payload)
const result = await patch(`/machines/${machineId}/structure`, payload)
if (result.success) {
const index = machines.value.findIndex((machine) => machine.id === machineId)
if (index !== -1) {
@@ -180,7 +163,29 @@ export function useMachines() {
}
return result
} catch (error) {
console.error('Erreur lors de la reconfiguration du squelette de la machine:', error)
console.error('Erreur lors de la mise à jour de la structure de la machine:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
}
}
const cloneMachine = async (sourceId: string, data: { name: string; siteId: string; reference?: string }): Promise<ApiResponse> => {
loading.value = true
try {
const result = await post(`/machines/${sourceId}/clone`, data)
if (result.success) {
const clonedMachine = normalizeMachineResponse(result.data) ||
normalizeMachineResponse((result.data as Record<string, unknown>)?.machine) ||
null
if (clonedMachine) {
machines.value.push(clonedMachine)
}
showSuccess(`Machine "${clonedMachine?.name || data.name}" clonée avec succès`)
}
return result
} catch (error) {
console.error('Erreur lors du clonage de la machine:', error)
return { success: false, error: (error as Error).message }
} finally {
loading.value = false
@@ -241,10 +246,6 @@ export function useMachines() {
return machines.value.filter((machine) => machine.siteId === siteId)
}
const getMachinesByType = (typeMachineId: string): Machine[] => {
return machines.value.filter((machine) => machine.typeMachineId === typeMachineId)
}
const getMachines = (): Machine[] => machines.value
const isLoading = (): boolean => loading.value
@@ -253,13 +254,12 @@ export function useMachines() {
loading,
loadMachines,
createMachine,
createMachineFromType,
updateMachine: updateMachineData,
reconfigureSkeleton,
updateStructure,
cloneMachine,
deleteMachine,
getMachineById,
getMachinesBySite,
getMachinesByType,
getMachines,
isLoading,
addMissingCustomFields,