WIP: corrections multiples formulaires et sérialisation

- Fix constructeurUtils: réordonner delete/add pour sauvegarder les fournisseurs
- Fix prix/supplierPrice: envoyer en string pour DECIMAL Doctrine
- Fix useMachineTypesApi: normaliser les requirements et forceRefresh
- Fix SearchSelect: watch deep sur baseOptions
- Debug logs temporaires pour pieceRequirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 12:28:40 +01:00
parent 2f3d4c5260
commit 9cc7ac10f0
14 changed files with 276 additions and 89 deletions

View File

@@ -594,6 +594,15 @@ const selectedTypeStructure = computed<ComponentModelStructure | null>(() => {
return structure ? normalizeStructureForEditor(structure) : null
})
const refreshCustomFieldInputs = (
structureOverride?: ComponentModelStructure | null,
valuesOverride?: any[] | null,
) => {
const structure = structureOverride ?? selectedTypeStructure.value ?? null
const values = valuesOverride ?? component.value?.customFieldValues ?? null
customFieldInputs.value = buildCustomFieldInputs(structure, values)
}
const requiredCustomFieldsFilled = computed(() =>
customFieldInputs.value.every((field) => {
if (!field.required) {
@@ -641,6 +650,7 @@ const fetchComponent = async () => {
const customValues = await getCustomFieldValuesByEntity('composant', result.data.id)
if (customValues.success && Array.isArray(customValues.data)) {
component.value.customFieldValues = customValues.data
refreshCustomFieldInputs(undefined, customValues.data)
}
} else {
component.value = null
@@ -677,10 +687,7 @@ watch(
void ensureConstructeurs(editionForm.constructeurIds)
}
customFieldInputs.value = buildCustomFieldInputs(
currentStructure,
currentComponent.customFieldValues,
)
refreshCustomFieldInputs(currentStructure, currentComponent.customFieldValues)
initialized = true
},
@@ -691,10 +698,7 @@ watch(selectedTypeStructure, (currentStructure) => {
if (!component.value) {
return
}
customFieldInputs.value = buildCustomFieldInputs(
currentStructure,
component.value.customFieldValues,
)
refreshCustomFieldInputs(currentStructure, component.value.customFieldValues)
})
const submitEdition = async () => {
@@ -719,7 +723,7 @@ const submitEdition = async () => {
if (rawPrice) {
const parsed = Number(rawPrice)
if (!Number.isNaN(parsed)) {
payload.prix = parsed
payload.prix = String(parsed)
}
} else {
payload.prix = null

View File

@@ -870,7 +870,7 @@ const submitCreation = async () => {
if (rawPrice) {
const parsed = Number(rawPrice)
if (!Number.isNaN(parsed)) {
payload.prix = parsed
payload.prix = String(parsed)
}
}

View File

@@ -86,6 +86,7 @@
import { ref, computed, onMounted } from 'vue'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
import { extractRelationId } from '~/shared/apiRelations'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideClipboardList from '~icons/lucide/clipboard-list'
import IconLucideList from '~icons/lucide/list'
@@ -142,6 +143,20 @@ const parseOptions = (field = {}) => {
return []
}
const toModelTypeIri = (value) => {
if (!value) {
return undefined
}
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
return value
}
const relationId = extractRelationId(value)
if (relationId) {
return `/api/model_types/${relationId}`
}
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
}
const normalizeCustomFields = (fields = []) =>
fields
.filter(field => field?.name && field.name.trim() !== '')
@@ -165,9 +180,9 @@ const toIntegerOrNull = (value, fallback = null) => {
const normalizeComponentRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeComposantId)
.filter(req => req?.typeComposantId || req?.typeComposant)
.map((req, index) => ({
typeComposantId: req.typeComposantId,
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 1),
maxCount: toIntegerOrNull(req.maxCount, null),
@@ -180,9 +195,9 @@ const normalizeComponentRequirements = (requirements = []) =>
const normalizePieceRequirements = (requirements = []) =>
requirements
.filter(req => req?.typePieceId)
.filter(req => req?.typePieceId || req?.typePiece)
.map((req, index) => ({
typePieceId: req.typePieceId,
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),
@@ -195,9 +210,9 @@ const normalizePieceRequirements = (requirements = []) =>
const normalizeProductRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeProductId)
.filter(req => req?.typeProductId || req?.typeProduct)
.map((req, index) => ({
typeProductId: req.typeProductId,
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),

View File

@@ -154,26 +154,26 @@
/>
</div>
<div v-if="selectedType" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
<div v-if="selectedType || resolvedStructure" class="space-y-3 rounded-lg border border-base-200 bg-base-200/40 p-4">
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="font-semibold text-base-content">Squelette sélectionné</h2>
<p class="text-xs text-base-content/70">
{{ selectedType.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
{{ selectedType?.description || 'Ce squelette définit la structure et les champs personnalisés de la pièce.' }}
</p>
</div>
<span class="badge badge-outline">{{ formatPieceStructurePreview(selectedType.structure) }}</span>
<span class="badge badge-outline">{{ formatPieceStructurePreview(resolvedStructure) }}</span>
</div>
<details v-if="selectedType.structure" class="collapse collapse-arrow bg-base-100">
<details v-if="resolvedStructure" class="collapse collapse-arrow bg-base-100">
<summary class="collapse-title text-sm font-medium">
Consulter le détail du squelette
</summary>
<div class="collapse-content space-y-2 text-sm text-base-content/80">
<div v-if="getStructureCustomFields(selectedType.structure).length" class="space-y-1">
<div v-if="getStructureCustomFields(resolvedStructure).length" class="space-y-1">
<h3 class="font-semibold text-sm text-base-content">Champs personnalisés</h3>
<ul class="list-disc list-inside space-y-1">
<li v-for="field in getStructureCustomFields(selectedType.structure)" :key="field.name">
<li v-for="field in getStructureCustomFields(resolvedStructure)" :key="field.name">
<span class="font-medium">{{ field.name }}</span>
<span v-if="field.value !== undefined && field.value !== null"> : {{ field.value }}</span>
</li>
@@ -395,12 +395,14 @@ import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
import { extractRelationId } from '~/shared/apiRelations'
import { getFileIcon } from '~/utils/fileIcons'
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
import { formatPieceStructurePreview } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import type { PieceModelProduct, PieceModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
import { getModelType } from '~/services/modelTypes'
interface PieceCatalogType extends ModelType {
structure: PieceModelStructure | null
@@ -424,7 +426,7 @@ const router = useRouter()
const { get } = useApi()
const { pieceTypes, loadPieceTypes } = usePieceTypes()
const { updatePiece } = usePieces()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
const toast = useToast()
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
const { ensureConstructeurs } = useConstructeurs()
@@ -440,6 +442,7 @@ const previewDocument = ref<any | null>(null)
const previewVisible = ref(false)
const selectedTypeId = ref<string>('')
const pieceTypeDetails = ref<any | null>(null)
const editionForm = reactive({
name: '' as string,
reference: '' as string,
@@ -451,6 +454,18 @@ const editionForm = reactive({
const customFieldInputs = ref<CustomFieldInput[]>([])
const documentIcon = (doc: any) =>
getFileIcon({ name: doc?.filename || doc?.name, mime: doc?.mimeType })
const resolvedStructure = computed<PieceModelStructure | null>(() =>
pieceTypeDetails.value?.structure ?? selectedType.value?.structure ?? null,
)
const refreshCustomFieldInputs = (
structureOverride?: PieceModelStructure | null,
valuesOverride?: any[] | null,
) => {
const structure = structureOverride ?? resolvedStructure.value ?? null
const values = valuesOverride ?? piece.value?.customFieldValues ?? null
customFieldInputs.value = buildCustomFieldInputs(structure, values)
}
const formatSize = (size: number | null | undefined) => {
if (size === null || size === undefined) {
return '—'
@@ -578,7 +593,7 @@ const selectedType = computed(() => {
})
const structureProducts = computed(() =>
getStructureProducts(selectedType.value?.structure ?? null),
getStructureProducts(resolvedStructure.value),
)
const requiresProductSelection = computed(() => structureProducts.value.length > 0)
@@ -659,12 +674,37 @@ const fetchPiece = async () => {
if (result.success) {
piece.value = result.data
pieceDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
const customValues = await getCustomFieldValuesByEntity('piece', result.data.id)
if (customValues.success && Array.isArray(customValues.data)) {
piece.value.customFieldValues = customValues.data
refreshCustomFieldInputs(undefined, customValues.data)
}
await loadPieceTypeDetails(result.data)
} else {
piece.value = null
pieceDocuments.value = []
}
}
const loadPieceTypeDetails = async (currentPiece: any) => {
const typeId = currentPiece?.typePieceId
|| extractRelationId(currentPiece?.typePiece)
|| ''
if (!typeId) {
pieceTypeDetails.value = null
return
}
try {
const type = await getModelType(typeId)
if (type && typeof type === 'object') {
pieceTypeDetails.value = type
refreshCustomFieldInputs(type.structure ?? null, currentPiece?.customFieldValues ?? null)
}
} catch (error) {
pieceTypeDetails.value = null
}
}
let initialized = false
watch(
@@ -674,7 +714,13 @@ watch(
return
}
selectedTypeId.value = currentPiece.typePieceId || ''
const resolvedTypeId = currentPiece.typePieceId
|| extractRelationId(currentPiece.typePiece)
|| ''
if (resolvedTypeId && !currentPiece.typePieceId) {
currentPiece.typePieceId = resolvedTypeId
}
selectedTypeId.value = resolvedTypeId
editionForm.name = currentPiece.name || ''
editionForm.reference = currentPiece.reference || ''
@@ -689,10 +735,7 @@ watch(
void ensureConstructeurs(editionForm.constructeurIds)
}
customFieldInputs.value = buildCustomFieldInputs(
currentType?.structure ?? null,
currentPiece.customFieldValues,
)
refreshCustomFieldInputs(currentType?.structure ?? null, currentPiece.customFieldValues)
initialized = true
},
@@ -703,10 +746,16 @@ watch(selectedType, (currentType) => {
if (!piece.value || !currentType) {
return
}
customFieldInputs.value = buildCustomFieldInputs(
currentType.structure,
piece.value.customFieldValues,
)
if (!pieceTypeDetails.value) {
refreshCustomFieldInputs(currentType.structure, piece.value.customFieldValues)
}
})
watch(resolvedStructure, (currentStructure) => {
if (!piece.value) {
return
}
refreshCustomFieldInputs(currentStructure, piece.value.customFieldValues)
})
const submitEdition = async () => {
@@ -744,7 +793,7 @@ const submitEdition = async () => {
if (rawPrice) {
const parsed = Number(rawPrice)
if (!Number.isNaN(parsed)) {
payload.prix = parsed
payload.prix = String(parsed)
}
} else {
payload.prix = null

View File

@@ -504,7 +504,7 @@ const submitCreation = async () => {
if (rawPrice) {
const parsed = Number(rawPrice)
if (!Number.isNaN(parsed)) {
payload.prix = parsed
payload.prix = String(parsed)
}
}

View File

@@ -352,7 +352,7 @@ const route = useRoute()
const router = useRouter()
const toast = useToast()
const { getProduct, updateProduct } = useProducts()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const { upsertCustomFieldValue, updateCustomFieldValue, getCustomFieldValuesByEntity } = useCustomFields()
const {
loadDocumentsByProduct,
uploadDocuments: uploadProductDocuments,
@@ -373,6 +373,15 @@ const productDocuments = ref<any[]>([])
const previewDocument = ref<any | null>(null)
const previewVisible = ref(false)
const refreshCustomFieldInputs = (
structureOverride?: ProductModelStructure | null,
valuesOverride?: any[] | null,
) => {
const nextStructure = structureOverride ?? structure.value ?? null
const nextValues = valuesOverride ?? product.value?.customFieldValues ?? null
customFieldInputs.value = buildCustomFieldInputs(nextStructure, nextValues)
}
const editionForm = reactive({
name: '' as string,
reference: '' as string,
@@ -493,6 +502,11 @@ const loadProduct = async () => {
product.value = result.data
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
await loadProductType()
const customValues = await getCustomFieldValuesByEntity('product', result.data.id)
if (customValues.success && Array.isArray(customValues.data)) {
product.value.customFieldValues = customValues.data
refreshCustomFieldInputs(undefined, customValues.data)
}
await hydrateForm()
await refreshDocuments()
} else {
@@ -582,7 +596,7 @@ const hydrateForm = async () => {
editionForm.supplierPrice = product.value.supplierPrice !== null && product.value.supplierPrice !== undefined
? String(product.value.supplierPrice)
: ''
customFieldInputs.value = buildCustomFieldInputs(structure.value, product.value.customFieldValues)
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
if (editionForm.constructeurIds.length) {
await ensureConstructeurs(editionForm.constructeurIds)
}
@@ -691,11 +705,13 @@ const submitEdition = async () => {
constructeurIds,
}
const rawPrice = editionForm.supplierPrice.trim()
payload.supplierPrice = rawPrice
const rawPrice = typeof editionForm.supplierPrice === 'string'
? editionForm.supplierPrice.trim()
: editionForm.supplierPrice
payload.supplierPrice = rawPrice !== '' && rawPrice !== null && rawPrice !== undefined
? Number.isNaN(Number(rawPrice))
? null
: Number(rawPrice)
: String(Number(rawPrice))
: null
saving.value = true
@@ -730,20 +746,29 @@ const saveCustomFieldValues = async (productId: string) => {
continue
}
if (!field.customFieldId) {
continue
}
const metadata = field.customFieldId
? undefined
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
const result = await upsertCustomFieldValue(
field.customFieldId,
'product',
productId,
String(value ?? ''),
{ customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required },
metadata,
)
if (!result.success) {
failed.push(field.name)
} else {
const createdValue = result.data
if (createdValue?.id) {
field.customFieldValueId = createdValue.id
}
const resolvedId = createdValue?.customField?.id || field.customFieldId
if (resolvedId) {
field.customFieldId = resolvedId
}
}
}
return failed

View File

@@ -425,11 +425,13 @@ const buildPayload = () => {
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
const rawPrice = creationForm.supplierPrice.trim()
if (rawPrice) {
const rawPrice = typeof creationForm.supplierPrice === 'string'
? creationForm.supplierPrice.trim()
: creationForm.supplierPrice
if (rawPrice !== '' && rawPrice !== null && rawPrice !== undefined) {
const parsed = Number(rawPrice)
if (!Number.isNaN(parsed)) {
payload.supplierPrice = parsed
payload.supplierPrice = String(parsed)
}
}
@@ -486,19 +488,31 @@ const submitCreation = async () => {
const saveCustomFieldValues = async (productId: string) => {
const failed: string[] = []
for (const field of customFieldInputs.value) {
if (!field.customFieldId || !field.name) {
if (!field.name) {
continue
}
const value = field.value ?? ''
const metadata = field.customFieldId
? undefined
: { customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required }
const result = await upsertCustomFieldValue(
field.customFieldId,
'product',
productId,
String(value ?? ''),
{ customFieldName: field.name, customFieldType: field.type, customFieldRequired: field.required },
metadata,
)
if (!result.success) {
failed.push(field.name)
} else {
const createdValue = result.data
if (createdValue?.id) {
field.customFieldValueId = createdValue.id
}
const resolvedId = createdValue?.customField?.id || field.customFieldId
if (resolvedId) {
field.customFieldId = resolvedId
}
}
}
return failed

View File

@@ -52,6 +52,7 @@ import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
import { useToast } from '~/composables/useToast'
import { extractRelationId } from '~/shared/apiRelations'
const route = useRoute()
const router = useRouter()
@@ -90,6 +91,20 @@ const parseOptions = (field = {}) => {
return []
}
const toModelTypeIri = (value) => {
if (!value) {
return undefined
}
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
return value
}
const relationId = extractRelationId(value)
if (relationId) {
return `/api/model_types/${relationId}`
}
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
}
const normalizeCustomFields = (fields = []) =>
fields
.filter(field => field?.name && field.name.trim() !== '')
@@ -113,9 +128,9 @@ const toIntegerOrNull = (value, fallback = null) => {
const normalizeComponentRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeComposantId)
.filter(req => req?.typeComposantId || req?.typeComposant)
.map((req, index) => ({
typeComposantId: req.typeComposantId,
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 1),
maxCount: toIntegerOrNull(req.maxCount, null),
@@ -128,9 +143,9 @@ const normalizeComponentRequirements = (requirements = []) =>
const normalizePieceRequirements = (requirements = []) =>
requirements
.filter(req => req?.typePieceId)
.filter(req => req?.typePieceId || req?.typePiece)
.map((req, index) => ({
typePieceId: req.typePieceId,
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),
@@ -143,9 +158,9 @@ const normalizePieceRequirements = (requirements = []) =>
const normalizeProductRequirements = (requirements = []) =>
requirements
.filter(req => req?.typeProductId)
.filter(req => req?.typeProductId || req?.typeProduct)
.map((req, index) => ({
typeProductId: req.typeProductId,
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
label: req.label?.trim() ? req.label.trim() : undefined,
minCount: toIntegerOrNull(req.minCount, 0),
maxCount: toIntegerOrNull(req.maxCount, null),
@@ -194,7 +209,7 @@ onMounted(async () => {
console.log('=== EDIT TYPE PAGE LOADING ===')
console.log('Loading type with ID:', typeId)
const result = await getMachineTypeById(typeId)
const result = await getMachineTypeById(typeId, true)
console.log('API Result:', result)
if (result.success) {