Extract useMachineCreatePage composable and 5 preview/selector components from machines/new.vue, reducing it from 1231 to 196 LOC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
366 lines
14 KiB
TypeScript
366 lines
14 KiB
TypeScript
/**
|
||
* 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,
|
||
}
|
||
}
|