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:
@@ -62,19 +62,30 @@ const deepClone = value => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const createFieldKey = () => `cf-${Math.random().toString(16).slice(2)}-${Date.now()}`
|
||||
|
||||
const normalizeCustomField = (field = {}, index = 0) => {
|
||||
const clone = deepClone(field)
|
||||
if (clone.type === 'select') {
|
||||
if (typeof clone.optionsText !== 'string' || !clone.optionsText.length) {
|
||||
if (Array.isArray(clone.options)) {
|
||||
clone.optionsText = clone.options.map(option => String(option).trim()).filter(Boolean).join('\n')
|
||||
} else {
|
||||
clone.optionsText = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
const currentOrder =
|
||||
typeof clone?.orderIndex === 'number' ? clone.orderIndex : index
|
||||
clone.orderIndex = currentOrder
|
||||
if (typeof clone?.__key !== 'string' || !clone.__key) {
|
||||
clone.__key = createFieldKey()
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
const withNormalizedOrder = (items = []) => {
|
||||
if (!Array.isArray(items)) { return [] }
|
||||
return items
|
||||
.map((item, index) => {
|
||||
const clone = deepClone(item)
|
||||
const currentOrder =
|
||||
typeof clone?.orderIndex === 'number' ? clone.orderIndex : index
|
||||
clone.orderIndex = currentOrder
|
||||
if (typeof clone?.__key !== 'string' || !clone.__key) {
|
||||
clone.__key = createFieldKey()
|
||||
}
|
||||
return clone
|
||||
})
|
||||
.map((item, index) => normalizeCustomField(item, index))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((item, index) => ({ ...item, orderIndex: index }))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import RequirementListEditor from '~/components/common/RequirementListEditor.vue'
|
||||
import { usePieceTypes } from '~/composables/usePieceTypes'
|
||||
|
||||
@@ -91,5 +91,11 @@ onMounted(async () => {
|
||||
if (!pieceTypes.value.length) {
|
||||
await loadPieceTypes()
|
||||
}
|
||||
console.log('[PieceRequirementsSection] pieceTypes loaded:', pieceTypes.value.map(t => ({ id: t.id, name: t.name })))
|
||||
console.log('[PieceRequirementsSection] requirements on mount:', props.modelValue.map(r => ({ id: r.id, typePieceId: r.typePieceId })))
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
console.log('[PieceRequirementsSection] requirements updated:', newVal.map(r => ({ id: r.id, typePieceId: r.typePieceId })))
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
@@ -184,11 +184,13 @@ watch(
|
||||
|
||||
watch(
|
||||
baseOptions,
|
||||
() => {
|
||||
if (!openDropdown.value) {
|
||||
searchTerm.value = selectedOption.value ? resolveLabel(selectedOption.value) : searchTerm.value
|
||||
(newOptions) => {
|
||||
console.log('[SearchSelect] baseOptions changed, count:', newOptions.length, 'modelValue:', props.modelValue, 'selectedOption:', selectedOption.value?.id)
|
||||
if (!openDropdown.value && selectedOption.value) {
|
||||
searchTerm.value = resolveLabel(selectedOption.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(openDropdown, (isOpen) => {
|
||||
|
||||
@@ -10,18 +10,28 @@ const normalizeRequirementList = (value, relationKey) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
return value.map((entry) => {
|
||||
return value.map((entry, index) => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return entry
|
||||
}
|
||||
const normalized = { ...entry }
|
||||
const relationField = relationKey.replace('Id', '')
|
||||
const relationValue = normalized[relationField]
|
||||
console.log(`[normalizeRequirementList] Entry ${index}:`, {
|
||||
relationKey,
|
||||
relationField,
|
||||
hasRelationKey: !!normalized[relationKey],
|
||||
relationValue,
|
||||
relationValueType: typeof relationValue
|
||||
})
|
||||
if (relationKey && !normalized[relationKey]) {
|
||||
const relationValue = normalized[relationKey.replace('Id', '')]
|
||||
const relationId = extractRelationId(relationValue)
|
||||
console.log(`[normalizeRequirementList] Extracted ID:`, relationId)
|
||||
if (relationId) {
|
||||
normalized[relationKey] = relationId
|
||||
}
|
||||
}
|
||||
console.log(`[normalizeRequirementList] Normalized entry:`, normalized)
|
||||
return normalized
|
||||
})
|
||||
}
|
||||
@@ -56,7 +66,7 @@ const extractCollection = (payload) => {
|
||||
|
||||
export function useMachineTypesApi () {
|
||||
const { showSuccess, showError, showInfo } = useToast()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
const { get, post, put, delete: del } = useApi()
|
||||
|
||||
const loadMachineTypes = async () => {
|
||||
loading.value = true
|
||||
@@ -94,7 +104,7 @@ export function useMachineTypesApi () {
|
||||
const updateMachineType = async (id, typeData) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await patch(`/type_machines/${id}`, typeData)
|
||||
const result = await put(`/type_machines/${id}`, typeData)
|
||||
if (result.success) {
|
||||
const normalized = normalizeMachineType(result.data)
|
||||
const index = machineTypes.value.findIndex(type => type.id === id)
|
||||
@@ -130,19 +140,28 @@ export function useMachineTypesApi () {
|
||||
}
|
||||
}
|
||||
|
||||
const getMachineTypeById = async (id) => {
|
||||
// D'abord chercher dans le cache local
|
||||
const localType = machineTypes.value.find(type => type.id === id)
|
||||
if (localType) {
|
||||
return { success: true, data: localType }
|
||||
const getMachineTypeById = async (id, forceRefresh = false) => {
|
||||
// D'abord chercher dans le cache local (sauf si forceRefresh)
|
||||
if (!forceRefresh) {
|
||||
const localType = machineTypes.value.find(type => type.id === id)
|
||||
if (localType) {
|
||||
return { success: true, data: localType }
|
||||
}
|
||||
}
|
||||
|
||||
// Si pas trouvé localement, récupérer depuis l'API
|
||||
// Récupérer depuis l'API
|
||||
try {
|
||||
const result = await get(`/type_machines/${id}`)
|
||||
if (result.success) {
|
||||
// Ajouter au cache local
|
||||
machineTypes.value.push(normalizeMachineType(result.data))
|
||||
const normalized = normalizeMachineType(result.data)
|
||||
// Mettre à jour le cache local
|
||||
const index = machineTypes.value.findIndex(type => type.id === id)
|
||||
if (index !== -1) {
|
||||
machineTypes.value[index] = normalized
|
||||
} else {
|
||||
machineTypes.value.push(normalized)
|
||||
}
|
||||
return { success: true, data: normalized }
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -870,7 +870,7 @@ const submitCreation = async () => {
|
||||
if (rawPrice) {
|
||||
const parsed = Number(rawPrice)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
payload.prix = parsed
|
||||
payload.prix = String(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -504,7 +504,7 @@ const submitCreation = async () => {
|
||||
if (rawPrice) {
|
||||
const parsed = Number(rawPrice)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
payload.prix = parsed
|
||||
payload.prix = String(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -117,21 +117,48 @@ export const formatConstructeurContact = (
|
||||
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
||||
payload: T,
|
||||
): T & { constructeurs?: string[] } => {
|
||||
const ids = uniqueConstructeurIds(
|
||||
const collected = new Set(uniqueConstructeurIds(
|
||||
payload?.constructeurIds,
|
||||
payload?.constructeurId,
|
||||
payload?.constructeur,
|
||||
payload?.constructeurs,
|
||||
);
|
||||
));
|
||||
|
||||
if (!collected.size) {
|
||||
const fallbackLists = [
|
||||
payload?.constructeurIds,
|
||||
payload?.constructeurs,
|
||||
];
|
||||
fallbackLists.forEach((list) => {
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
const id = toStringId(item);
|
||||
if (id) {
|
||||
collected.add(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isObject(item) && typeof item.id === 'string') {
|
||||
collected.add(item.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const ids = Array.from(collected);
|
||||
|
||||
const next = { ...payload } as Record<string, any>;
|
||||
if (ids.length) {
|
||||
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
|
||||
}
|
||||
delete next.constructeurId;
|
||||
delete next.constructeur;
|
||||
delete next.constructeurs;
|
||||
delete next.constructeurIds;
|
||||
|
||||
if (ids.length) {
|
||||
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
|
||||
}
|
||||
|
||||
return next as T & { constructeurs?: string[] };
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 3001
|
||||
port: 3000
|
||||
},
|
||||
modules: [
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user