Normalize machine link responses

This commit is contained in:
MatthieuTD
2025-10-09 09:21:40 +02:00
parent f89364d04e
commit 95c2a82689
3 changed files with 1028 additions and 216 deletions

View File

@@ -203,16 +203,18 @@
Constructeur :
{{ findComponentById(entry.composantId)?.constructeur?.name || findComponentById(entry.composantId)?.constructeurName || "—" }}
</div>
<div>
Machine actuelle :
{{ findComponentById(entry.composantId)?.machine?.name || findComponentById(entry.composantId)?.machineId || "Non affecté" }}
</div>
<div
v-if="findComponentById(entry.composantId)?.machine?.name"
class="text-warning mt-1"
>
La réaffectation détachera ce composant de sa machine actuelle lors de la création.
</div>
<div>
Machines liées :
{{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) || 'Aucune' }}
</div>
<div
v-if="formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId)))"
class="text-warning mt-1"
>
Ce composant est déjà lié à
{{ formatAssignmentList(getComponentMachineAssignments(findComponentById(entry.composantId))) }}.
La création ajoutera un nouveau lien.
</div>
</div>
</div>
</div>
@@ -319,20 +321,20 @@
Constructeur :
{{ findPieceById(entry.pieceId)?.constructeur?.name || findPieceById(entry.pieceId)?.constructeurName || "—" }}
</div>
<div>
Machine actuelle :
{{ findPieceById(entry.pieceId)?.machine?.name || findPieceById(entry.pieceId)?.machineId || "Non affecté" }}
</div>
<div>
Composant actuel :
{{ findPieceById(entry.pieceId)?.composant?.name || findPieceById(entry.pieceId)?.composantId || "Non affecté" }}
</div>
<div
v-if="findPieceById(entry.pieceId)?.machine?.name || findPieceById(entry.pieceId)?.composant?.name"
class="text-warning mt-1"
>
Cette pièce sera détachée de son affectation actuelle pendant la création.
</div>
<div>
Machines liées :
{{ formatAssignmentList(getPieceMachineAssignments(findPieceById(entry.pieceId))) || 'Aucune' }}
</div>
<div>
Composants liés :
{{ formatAssignmentList(getPieceComponentAssignments(findPieceById(entry.pieceId))) || 'Aucun' }}
</div>
<div
v-if="formatAssignmentList(getPieceMachineAssignments(findPieceById(entry.pieceId))) || formatAssignmentList(getPieceComponentAssignments(findPieceById(entry.pieceId)))"
class="text-warning mt-1"
>
Cette pièce dispose déjà de liaisons existantes. La création ajoutera un nouveau lien.
</div>
</div>
</div>
</div>
@@ -583,6 +585,7 @@ import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useComposants } from '~/composables/useComposants'
import { usePieces } from '~/composables/usePieces'
import { useToast } from '~/composables/useToast'
import { sanitizeDefinitionOverrides } from '~/shared/modelUtils'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideX from '~icons/lucide/x'
import IconLucideEye from '~icons/lucide/eye'
@@ -639,6 +642,215 @@ const pieceById = computed(() => {
const componentInventory = computed(() => composants.value || [])
const pieceInventory = computed(() => pieces.value || [])
const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value)
const toTrimmedString = (value) => {
if (typeof value === 'string') {
const trimmed = value.trim()
return trimmed.length > 0 ? trimmed : null
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
return null
}
const dedupeAssignments = (assignments) => {
const seen = new Set()
return assignments.filter((assignment) => {
if (!assignment) {
return false
}
const id = assignment.id != null ? String(assignment.id) : ''
const name = assignment.name != null ? String(assignment.name) : ''
const key = `${id}::${name}`
if (!id && !name) {
return false
}
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
const normalizeMachineAssignment = (input) => {
if (!input) {
return null
}
if (typeof input === 'string') {
const name = toTrimmedString(input)
return name ? { id: name, name } : null
}
if (typeof input === 'number' && Number.isFinite(input)) {
const value = String(input)
return { id: value, name: value }
}
const container = input.machine || input.machineData || input
if (!isPlainObject(container)) {
return null
}
const id = container.id ?? input.machineId ?? input.id ?? null
const name =
container.name
|| input.machineName
|| container.label
|| container.title
|| (typeof id === 'string' ? id : null)
|| (typeof id === 'number' ? String(id) : null)
if (id == null && name == null) {
return null
}
return {
id: id != null ? id : null,
name: name != null ? name : null,
}
}
const collectMachineAssignments = (source) => {
if (!isPlainObject(source)) {
return []
}
const candidates = [
source.machines,
source.machineLinks,
source.machineAssignments,
source.machinesAssignments,
source.linkedMachines,
]
const assignments = []
candidates.forEach((list) => {
if (Array.isArray(list)) {
list.forEach((item) => {
const normalized = normalizeMachineAssignment(item)
if (normalized) {
assignments.push(normalized)
}
})
}
})
if (!assignments.length) {
const direct = normalizeMachineAssignment(source.machine)
if (direct) {
assignments.push(direct)
}
}
if (!assignments.length) {
const idCandidate = source.machineId ?? source.machineID ?? null
const nameCandidate = source.machineName ?? null
const normalized = normalizeMachineAssignment(nameCandidate || idCandidate)
if (normalized) {
assignments.push(normalized)
}
}
return dedupeAssignments(assignments)
}
const normalizeComponentAssignment = (input) => {
if (!input) {
return null
}
if (typeof input === 'string') {
const value = toTrimmedString(input)
return value ? { id: value, name: value } : null
}
if (typeof input === 'number' && Number.isFinite(input)) {
const value = String(input)
return { id: value, name: value }
}
const container = input.component || input.composant || input
if (!isPlainObject(container)) {
return null
}
const id = container.id ?? input.componentId ?? input.composantId ?? input.id ?? null
const name =
container.name
|| input.componentName
|| input.composantName
|| container.label
|| (typeof id === 'string' ? id : null)
|| (typeof id === 'number' ? String(id) : null)
if (id == null && name == null) {
return null
}
return {
id: id != null ? id : null,
name: name != null ? name : null,
}
}
const collectComponentAssignments = (source) => {
if (!isPlainObject(source)) {
return []
}
const candidates = [
source.components,
source.composants,
source.componentLinks,
source.linkedComponents,
]
const assignments = []
candidates.forEach((list) => {
if (Array.isArray(list)) {
list.forEach((item) => {
const normalized = normalizeComponentAssignment(item)
if (normalized) {
assignments.push(normalized)
}
})
}
})
if (!assignments.length) {
const direct = normalizeComponentAssignment(source.component || source.composant)
if (direct) {
assignments.push(direct)
}
}
if (!assignments.length) {
const idCandidate = source.componentId ?? source.composantId ?? null
const normalized = normalizeComponentAssignment(idCandidate)
if (normalized) {
assignments.push(normalized)
}
}
return dedupeAssignments(assignments)
}
const getComponentMachineAssignments = component => collectMachineAssignments(component || {})
const getPieceMachineAssignments = piece => collectMachineAssignments(piece || {})
const getPieceComponentAssignments = piece => collectComponentAssignments(piece || {})
const formatAssignmentList = (assignments) => {
if (!Array.isArray(assignments) || assignments.length === 0) {
return ''
}
return assignments
.map((assignment) => assignment?.name || assignment?.id)
.filter(Boolean)
.join(', ')
}
const selectedComponentIds = computed(() => {
const ids = []
Object.values(componentRequirementSelections).forEach((entries) => {
@@ -715,10 +927,9 @@ const formatComponentOption = (component) => {
if (constructeurName) {
parts.push(constructeurName)
}
if (component.machine?.name) {
parts.push(`Machine: ${component.machine.name}`)
} else if (component.machineId) {
parts.push(`Machine: ${component.machineId}`)
const machineAssignments = getComponentMachineAssignments(component)
if (machineAssignments.length) {
parts.push(`Machines: ${formatAssignmentList(machineAssignments)}`)
}
return parts.join(' • ')
}
@@ -735,15 +946,13 @@ const formatPieceOption = (piece) => {
if (constructeurName) {
parts.push(constructeurName)
}
if (piece.machine?.name) {
parts.push(`Machine: ${piece.machine.name}`)
} else if (piece.machineId) {
parts.push(`Machine: ${piece.machineId}`)
const machineAssignments = getPieceMachineAssignments(piece)
if (machineAssignments.length) {
parts.push(`Machines: ${formatAssignmentList(machineAssignments)}`)
}
if (piece.composant?.name) {
parts.push(`Composant: ${piece.composant.name}`)
} else if (piece.composantId) {
parts.push(`Composant: ${piece.composantId}`)
const componentAssignments = getPieceComponentAssignments(piece)
if (componentAssignments.length) {
parts.push(`Composants: ${formatAssignmentList(componentAssignments)}`)
}
return parts.join(' • ')
}
@@ -877,10 +1086,58 @@ const removePieceSelectionEntry = (requirementId, index) => {
pieceRequirementSelections[requirementId] = entries.filter((_, i) => i !== index)
}
const extractParentIdentifiers = (source) => {
if (!isPlainObject(source)) {
return {}
}
const identifiers = {}
const idKeys = [
'parentRequirementId',
'parentComponentRequirementId',
'parentPieceRequirementId',
'parentMachineComponentRequirementId',
'parentMachinePieceRequirementId',
'parentLinkId',
'parentComponentId',
'parentPieceId',
]
idKeys.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const value = source[key]
if (value !== undefined && value !== null && value !== '') {
identifiers[key] = value
}
}
})
const objectKeys = [
'parentRequirement',
'parentComponentRequirement',
'parentPieceRequirement',
'parentMachineComponentRequirement',
'parentMachinePieceRequirement',
]
objectKeys.forEach((key) => {
const value = source[key]
if (isPlainObject(value) && value.id !== undefined && value.id !== null && value.id !== '') {
const idKey = `${key}Id`
if (!Object.prototype.hasOwnProperty.call(identifiers, idKey)) {
identifiers[idKey] = value.id
}
}
})
return identifiers
}
const validateRequirementSelections = (type) => {
const errors = []
const componentSelectionsPayload = []
const pieceSelectionsPayload = []
const componentLinksPayload = []
const pieceLinksPayload = []
for (const requirement of type.componentRequirements || []) {
const entries = getComponentRequirementEntries(requirement.id)
@@ -907,16 +1164,6 @@ const validateRequirementSelections = (type) => {
return
}
if (component.machineId) {
errors.push(`Le composant "${component.name || component.id}" est déjà affecté à une machine.`)
return
}
if (component.parentComposantId) {
errors.push(`Le composant "${component.name || component.id}" est déjà rattaché à un autre composant.`)
return
}
const requiredTypeId = requirement.typeComposantId || requirement.typeComposant?.id || null
if (
requiredTypeId &&
@@ -927,10 +1174,19 @@ const validateRequirementSelections = (type) => {
return
}
componentSelectionsPayload.push({
const payload = {
requirementId: requirement.id,
composantId: entry.composantId,
})
}
const overrides = sanitizeDefinitionOverrides(entry.definition)
if (overrides) {
payload.overrides = overrides
}
Object.assign(payload, extractParentIdentifiers(requirement), extractParentIdentifiers(entry))
componentLinksPayload.push(payload)
})
}
@@ -959,11 +1215,6 @@ const validateRequirementSelections = (type) => {
return
}
if (piece.machineId || piece.composantId) {
errors.push(`La pièce "${piece.name || piece.id}" est déjà affectée.`)
return
}
const requiredTypeId = requirement.typePieceId || requirement.typePiece?.id || null
if (
requiredTypeId &&
@@ -974,10 +1225,19 @@ const validateRequirementSelections = (type) => {
return
}
pieceSelectionsPayload.push({
const payload = {
requirementId: requirement.id,
pieceId: entry.pieceId,
})
}
const overrides = sanitizeDefinitionOverrides(entry.definition)
if (overrides) {
payload.overrides = overrides
}
Object.assign(payload, extractParentIdentifiers(requirement), extractParentIdentifiers(entry))
pieceLinksPayload.push(payload)
})
}
@@ -987,8 +1247,8 @@ const validateRequirementSelections = (type) => {
return {
valid: true,
componentSelections: componentSelectionsPayload,
pieceSelections: pieceSelectionsPayload,
componentLinks: componentLinksPayload,
pieceLinks: pieceLinksPayload,
}
}
@@ -1059,6 +1319,11 @@ const machinePreview = computed(() => {
if (constructeurName) {
subtitleParts.push(constructeurName)
}
const machineAssignments = selectedComponent ? getComponentMachineAssignments(selectedComponent) : []
const assignmentLabel = formatAssignmentList(machineAssignments)
if (assignmentLabel) {
subtitleParts.push(`Liée à ${assignmentLabel}`)
}
const status = entry.composantId ? 'complete' : 'pending'
return {
@@ -1066,6 +1331,8 @@ const machinePreview = computed(() => {
status,
title: displayName,
subtitle: subtitleParts.join(' • ') || null,
assignmentLabel,
assignments: machineAssignments,
}
})
@@ -1086,9 +1353,22 @@ const machinePreview = computed(() => {
issues.push({ message: 'Sélectionner un composant pour chaque entrée.', kind: 'error', anchor: `component-group-${requirement.id}` })
}
const status = issues.some(issue => issue.kind === 'error')
normalizedEntries.forEach((entrySummary) => {
if (entrySummary.assignmentLabel) {
issues.push({
message: `Le composant "${entrySummary.title}" est déjà lié à ${entrySummary.assignmentLabel}.`,
kind: 'warning',
anchor: `component-group-${requirement.id}`,
})
}
})
const hasErrors = issues.some(issue => issue.kind === 'error')
const hasWarnings = issues.some(issue => issue.kind === 'warning') || completed < entries.length
const status = hasErrors
? 'error'
: completed < entries.length
: hasWarnings
? 'warning'
: 'ready'
@@ -1120,6 +1400,16 @@ const machinePreview = computed(() => {
if (constructeurName) {
subtitleParts.push(constructeurName)
}
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}`)
}
const status = entry.pieceId ? 'complete' : 'pending'
return {
@@ -1127,6 +1417,10 @@ const machinePreview = computed(() => {
status,
title: displayName,
subtitle: subtitleParts.join(' • ') || null,
machineAssignmentLabel,
componentAssignmentLabel,
machineAssignments,
componentAssignments,
}
})
@@ -1147,9 +1441,29 @@ const machinePreview = computed(() => {
issues.push({ message: 'Sélectionner une pièce pour chaque entrée.', kind: 'error', anchor: `piece-group-${requirement.id}` })
}
const status = issues.some(issue => issue.kind === 'error')
normalizedEntries.forEach((entrySummary) => {
if (entrySummary.machineAssignmentLabel) {
issues.push({
message: `La pièce "${entrySummary.title}" est déjà liée aux machines ${entrySummary.machineAssignmentLabel}.`,
kind: 'warning',
anchor: `piece-group-${requirement.id}`,
})
}
if (entrySummary.componentAssignmentLabel) {
issues.push({
message: `La pièce "${entrySummary.title}" est déjà rattachée aux composants ${entrySummary.componentAssignmentLabel}.`,
kind: 'warning',
anchor: `piece-group-${requirement.id}`,
})
}
})
const hasErrors = issues.some(issue => issue.kind === 'error')
const hasWarnings = issues.some(issue => issue.kind === 'warning') || completed < entries.length
const status = hasErrors
? 'error'
: completed < entries.length
: hasWarnings
? 'warning'
: 'ready'
@@ -1293,8 +1607,8 @@ const finalizeMachineCreation = async () => {
const hasRequirements = (type.componentRequirements?.length || 0) > 0 || (type.pieceRequirements?.length || 0) > 0
let componentSelections = []
let pieceSelections = []
let componentLinks = []
let pieceLinks = []
if (hasRequirements) {
const validationResult = validateRequirementSelections(type)
@@ -1302,16 +1616,16 @@ const finalizeMachineCreation = async () => {
toast.showError(validationResult.error)
return
}
componentSelections = validationResult.componentSelections
pieceSelections = validationResult.pieceSelections
componentLinks = validationResult.componentLinks
pieceLinks = validationResult.pieceLinks
}
const payload = {
...baseMachineData,
...(hasRequirements
? {
componentSelections,
pieceSelections
componentLinks,
pieceLinks
}
: {})
}