Files
Inventory/app/composables/useComponentCreate.ts
Matthieu 1d6c520945 fix(navigation) : use router.replace after entity creation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:10:22 +01:00

418 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<Record<string, any>>
}
// ---------------------------------------------------------------------------
// 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<string>(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<CustomFieldInput[]>([])
const structureAssignments = ref<StructureAssignmentNode | null>(null)
const selectedDocuments = ref<File[]>([])
const uploadingDocuments = ref(false)
// -------------------------------------------------------------------------
// Computed
// -------------------------------------------------------------------------
const availablePieces = computed(() => pieceCatalogRef.value ?? [])
const availableProducts = computed(() => productCatalogRef.value ?? [])
const availableComponents = computed(() => componentCatalogRef.value ?? [])
const structureDataLoading = computed(
() => piecesLoading.value || componentsLoading.value || productsLoading.value,
)
const fetchedPieceTypeMap = ref<Record<string, string>>({})
const pieceTypeLabelMap = computed(() =>
buildTypeLabelMap(pieceTypes.value, fetchedPieceTypeMap.value),
)
const productTypeLabelMap = computed(() =>
buildTypeLabelMap(productTypes.value),
)
const componentTypeLabelMap = computed(() =>
buildTypeLabelMap(componentTypes.value),
)
const componentTypeList = computed<ComponentCatalogType[]>(() =>
(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<ComponentModelStructure | null>(() => {
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<string, any>) =>
_resolvePieceLabel(piece, pieceTypeLabelMap.value)
const resolveProductLabel = (product: Record<string, any>) =>
_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<string, any> = {
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<string, any>
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`)
}
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,
}
}