/** * Component creation page – orchestration composable. * * Pure structure-assignment helpers live in * `~/shared/utils/structureAssignmentHelpers.ts`. */ import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from '#imports' import type { StructureAssignmentNode } from '~/components/ComponentStructureAssignmentNode.vue' import { useComponentTypes } from '~/composables/useComponentTypes' import { useComposants } from '~/composables/useComposants' import { usePieces } from '~/composables/usePieces' import { usePieceTypes } from '~/composables/usePieceTypes' import { useProducts } from '~/composables/useProducts' import { useProductTypes } from '~/composables/useProductTypes' import { useApi } from '~/composables/useApi' import { useToast } from '~/composables/useToast' import { humanizeError } from '~/shared/utils/errorMessages' import { useCustomFields } from '~/composables/useCustomFields' import { useDocuments } from '~/composables/useDocuments' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils' import { type CustomFieldInput, normalizeCustomFieldInputs, requiredCustomFieldsFilled as _requiredCustomFieldsFilled, saveCustomFieldValues as _saveCustomFieldValues, } from '~/shared/utils/customFieldFormUtils' import { uniqueConstructeurIds } from '~/shared/constructeurUtils' import { getStructurePieces, resolvePieceLabel as _resolvePieceLabel, resolveProductLabel as _resolveProductLabel, resolveSubcomponentLabel, fetchModelTypeNames, buildTypeLabelMap, } from '~/shared/utils/structureDisplayUtils' import { hasAssignments, initializeStructureAssignments, isAssignmentNodeComplete, serializeStructureAssignments, } from '~/shared/utils/structureAssignmentHelpers' import type { ComponentModelStructure } from '~/shared/types/inventory' import type { ModelType } from '~/services/modelTypes' interface ComponentCatalogType extends ModelType { structure: ComponentModelStructure | null customFields?: Array> } // --------------------------------------------------------------------------- // Main composable // --------------------------------------------------------------------------- export function useComponentCreate() { const route = useRoute() const router = useRouter() const { get } = useApi() const { componentTypes, loadComponentTypes, loadingComponentTypes: loadingTypes } = useComponentTypes() const { pieceTypes, loadPieceTypes } = usePieceTypes() const { productTypes, loadProductTypes } = useProductTypes() const { createComposant, composants: componentCatalogRef, loading: componentsLoading, } = useComposants() const { pieces: pieceCatalogRef, loading: piecesLoading, } = usePieces() const { products: productCatalogRef, loading: productsLoading, } = useProducts() const toast = useToast() const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const { uploadDocuments } = useDocuments() const { canEdit } = usePermissions() // ------------------------------------------------------------------------- // Local state // ------------------------------------------------------------------------- const selectedTypeId = ref(typeof route.query.typeId === 'string' ? route.query.typeId : '') const submitting = ref(false) const creationForm = reactive({ name: '' as string, description: '' as string, reference: '' as string, constructeurIds: [] as string[], prix: '' as string, }) const lastSuggestedName = ref('') const customFieldInputs = ref([]) const structureAssignments = ref(null) const selectedDocuments = ref([]) const uploadingDocuments = ref(false) // ------------------------------------------------------------------------- // Computed // ------------------------------------------------------------------------- const availablePieces = computed(() => pieceCatalogRef.value ?? []) const availableProducts = computed(() => productCatalogRef.value ?? []) const availableComponents = computed(() => componentCatalogRef.value ?? []) const structureDataLoading = computed( () => !submitting.value && (piecesLoading.value || componentsLoading.value || productsLoading.value), ) const fetchedPieceTypeMap = ref>({}) const pieceTypeLabelMap = computed(() => buildTypeLabelMap(pieceTypes.value, fetchedPieceTypeMap.value), ) const productTypeLabelMap = computed(() => buildTypeLabelMap(productTypes.value), ) const componentTypeLabelMap = computed(() => buildTypeLabelMap(componentTypes.value), ) const componentTypeList = computed(() => (componentTypes.value || []) .filter((item: any) => item?.category === 'COMPONENT') as ComponentCatalogType[], ) const typeOptionLabel = (type?: ComponentCatalogType) => type?.name || 'Catégorie' const typeOptionDescription = (type?: ComponentCatalogType) => type?.description ? String(type.description) : '' const selectedType = computed(() => { if (!selectedTypeId.value) { return null } return componentTypeList.value.find((type) => type.id === selectedTypeId.value) ?? null }) const selectedTypeStructure = computed(() => { const structure = selectedType.value?.structure ?? null return structure ? normalizeStructureForEditor(structure) : null }) const structureHasRequirements = computed(() => hasAssignments(structureAssignments.value), ) const structureSelectionsComplete = computed(() => { if (!structureHasRequirements.value) { return true } if (structureDataLoading.value) { return false } if (!structureAssignments.value) { return false } return isAssignmentNodeComplete(structureAssignments.value, true) }) const requiredCustomFieldsFilled = computed(() => _requiredCustomFieldsFilled(customFieldInputs.value), ) const canSubmit = computed(() => Boolean( canEdit.value && selectedType.value && creationForm.name && requiredCustomFieldsFilled.value && structureSelectionsComplete.value && !submitting.value, )) const resolvePieceLabel = (piece: Record) => _resolvePieceLabel(piece, pieceTypeLabelMap.value) const resolveProductLabel = (product: Record) => _resolveProductLabel(product, productTypeLabelMap.value) // ------------------------------------------------------------------------- // Watchers // ------------------------------------------------------------------------- watch( () => route.query.typeId, (value) => { if (typeof value === 'string') { selectedTypeId.value = value } }, ) watch(selectedTypeId, (id) => { const current = typeof route.query.typeId === 'string' ? route.query.typeId : '' if ((id || '') === current) { return } const nextQuery = { ...route.query } if (id) { nextQuery.typeId = id } else { delete nextQuery.typeId } router.replace({ path: route.path, query: nextQuery }).catch(() => {}) }) const clearCreationForm = () => { creationForm.name = '' creationForm.description = '' creationForm.reference = '' creationForm.constructeurIds = [] creationForm.prix = '' lastSuggestedName.value = '' structureAssignments.value = null } watch(selectedType, (type) => { if (!type) { clearCreationForm() customFieldInputs.value = [] structureAssignments.value = null return } if (!creationForm.name || creationForm.name === lastSuggestedName.value) { creationForm.name = type.name } lastSuggestedName.value = creationForm.name customFieldInputs.value = normalizeCustomFieldInputs(selectedTypeStructure.value) structureAssignments.value = initializeStructureAssignments(selectedTypeStructure.value) }) watch( selectedTypeStructure, (structure) => { const ids = getStructurePieces(structure) .map((piece: any) => piece?.typePieceId) .filter((id: any): id is string => typeof id === 'string' && id.trim().length > 0) if (!ids.length) { return } fetchModelTypeNames(Array.from(new Set(ids)), pieceTypeLabelMap.value, get) .then((additions) => { if (Object.keys(additions).length) { fetchedPieceTypeMap.value = { ...fetchedPieceTypeMap.value, ...additions } } }) .catch(() => {}) }, { immediate: true }, ) // ------------------------------------------------------------------------- // Submission // ------------------------------------------------------------------------- const submitCreation = async () => { if (!selectedType.value) { toast.showError('Sélectionnez une catégorie de composant.') return } const payload: Record = { name: creationForm.name.trim(), typeComposantId: selectedType.value.id, } const description = creationForm.description.trim() if (description) { payload.description = description } const reference = creationForm.reference.trim() if (reference) { payload.reference = reference } if (creationForm.constructeurIds.length) { payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds) } const rawPrice = typeof creationForm.prix === 'string' ? creationForm.prix.trim() : creationForm.prix === null || creationForm.prix === undefined ? '' : String(creationForm.prix).trim() if (rawPrice) { const parsed = Number(rawPrice) if (!Number.isNaN(parsed)) { payload.prix = String(parsed) } } const rootProductSelection = structureAssignments.value?.products?.find( (product) => typeof product.selectedProductId === 'string' && product.selectedProductId.trim().length > 0, ) ?? null if (rootProductSelection?.selectedProductId) { payload.productId = rootProductSelection.selectedProductId.trim() } if (structureHasRequirements.value && !structureSelectionsComplete.value) { toast.showError('Complétez la sélection des pièces, produits et sous-composants.') return } const serializedStructure = structureHasRequirements.value ? serializeStructureAssignments(structureAssignments.value) : null if (serializedStructure) { payload.structure = serializedStructure } submitting.value = true try { const result = await createComposant(payload) if (result.success) { const createdComponent = result.data as Record await _saveCustomFieldValues( 'composant', createdComponent.id, [createdComponent?.typeComposant?.structure?.customFields], { customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast }, ) if (selectedDocuments.value.length && result.data?.id) { uploadingDocuments.value = true const uploadResult = await uploadDocuments( { files: selectedDocuments.value, context: { composantId: result.data.id }, }, { updateStore: false }, ) if (!uploadResult.success) { const message = uploadResult.error ? `Documents non ajoutés : ${uploadResult.error}` : 'Documents non ajoutés : une erreur est survenue.' toast.showError(message) } selectedDocuments.value = [] } toast.showSuccess('Composant créé avec succès') await router.replace(`/component/${createdComponent.id}?edit=true`) } else if (result.error) { toast.showError(result.error) } } catch (error: any) { toast.showError(humanizeError(error?.message) || 'Impossible de créer le composant') } finally { submitting.value = false uploadingDocuments.value = false } } // ------------------------------------------------------------------------- // Initialization // ------------------------------------------------------------------------- onMounted(async () => { await Promise.allSettled([ loadComponentTypes(), loadPieceTypes(), loadProductTypes(), ]) }) // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- return { // State selectedTypeId, submitting, creationForm, customFieldInputs, structureAssignments, selectedDocuments, uploadingDocuments, // Computed loadingTypes, componentTypeList, selectedType, selectedTypeStructure, availablePieces, availableProducts, availableComponents, piecesLoading, productsLoading, componentsLoading, structureDataLoading, pieceTypeLabelMap, productTypeLabelMap, componentTypeLabelMap, structureHasRequirements, structureSelectionsComplete, canEdit, canSubmit, // Functions typeOptionLabel, typeOptionDescription, formatStructurePreview, resolvePieceLabel, resolveProductLabel, resolveSubcomponentLabel, submitCreation, } }