feat(constructeur) : update product edit flow with supplier references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-31 17:02:32 +02:00
parent 80739a4528
commit e0f761da2b
4 changed files with 131 additions and 30 deletions

View File

@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { useToast } from './useToast'
import { useApi } from './useApi'
import { humanizeError } from '~/shared/utils/errorMessages'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
import { extractCollection } from '~/shared/utils/apiHelpers'
@@ -196,7 +196,8 @@ export function useProducts() {
}
const createProduct = async (payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
loading.value = true
error.value = null
try {
@@ -225,7 +226,8 @@ export function useProducts() {
}
const updateProduct = async (id: string, payload: Partial<Product>): Promise<ProductSingleResult> => {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(payload))
const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = payload as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
loading.value = true
error.value = null
try {

View File

@@ -105,6 +105,11 @@
</div>
</div>
<ConstructeurLinksTable
v-if="constructeurLinks.length"
v-model="constructeurLinks"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label">
@@ -237,9 +242,11 @@ import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { useProductHistory } from '~/composables/useProductHistory'
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { getModelType } from '~/services/modelTypes'
import type { ProductModelStructure } from '~/shared/types/inventory'
import { canPreviewDocument } from '~/utils/documentPreview'
@@ -263,7 +270,8 @@ const {
deleteDocument: deleteProductDocument,
updateDocument,
} = useDocuments()
const { ensureConstructeurs } = useConstructeurs()
const { ensureConstructeurs, getConstructeurById } = useConstructeurs()
const { fetchLinks, syncLinks } = useConstructeurLinks()
const {
history,
loading: historyLoading,
@@ -286,6 +294,9 @@ const previewVisible = ref(false)
const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false)
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
const historyFieldLabels: Record<string, string> = {
name: 'Nom',
reference: 'Référence',
@@ -457,18 +468,19 @@ const hydrateForm = () => {
}
editionForm.name = product.value.name || ''
editionForm.reference = product.value.reference || ''
editionForm.constructeurIds = uniqueConstructeurIds(
product.value,
Array.isArray(product.value.constructeurs) ? product.value.constructeurs : [],
)
// Load constructeur links
fetchLinks('product', String(route.params.id)).then((links) => {
constructeurLinks.value = links
originalConstructeurLinks.value = links.map(l => ({ ...l }))
editionForm.constructeurIds = constructeurIdsFromLinks(links)
if (editionForm.constructeurIds.length) {
void ensureConstructeurs(editionForm.constructeurIds)
}
})
editionForm.supplierPrice = product.value.supplierPrice !== null && product.value.supplierPrice !== undefined
? String(product.value.supplierPrice)
: ''
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
if (editionForm.constructeurIds.length) {
// Smart-cached + deduped — fire-and-forget, ConstructeurSelect handles its own loading
ensureConstructeurs(editionForm.constructeurIds).catch(() => {})
}
}
watch(
@@ -486,12 +498,9 @@ const submitEdition = async () => {
return
}
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
const payload: Record<string, any> = {
name: editionForm.name.trim(),
reference: editionForm.reference.trim() || null,
constructeurIds,
}
const rawPrice = typeof editionForm.supplierPrice === 'string'
@@ -518,6 +527,8 @@ const submitEdition = async () => {
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
return
}
await syncLinks('product', product.value.id, originalConstructeurLinks.value, constructeurLinks.value)
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
toast.showSuccess('Produit mis à jour avec succès')
versionRefreshKey.value++
}
@@ -528,6 +539,25 @@ const submitEdition = async () => {
}
}
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
watch(
() => editionForm.constructeurIds,
(ids) => {
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
for (const id of ids) {
if (!currentIds.has(id)) {
const resolved = getConstructeurById(id)
constructeurLinks.value.push({
constructeurId: id,
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
supplierReference: null,
})
}
}
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
},
)
onMounted(async () => {
await loadProduct()
})

View File

@@ -133,6 +133,17 @@
</div>
</div>
<!-- Constructeur links table -->
<ConstructeurLinksTable
v-if="isEditMode && constructeurLinks.length"
v-model="constructeurLinks"
/>
<ConstructeurLinksTable
v-else-if="!isEditMode && constructeurLinks.length"
:model-value="constructeurLinks"
:readonly="true"
/>
<!-- Prix fournisseur (if value or edit mode) -->
<div v-if="isEditMode || product.supplierPrice" class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
@@ -303,10 +314,12 @@ import { useToast } from '~/composables/useToast'
import { humanizeError } from '~/shared/utils/errorMessages'
import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { useProductHistory } from '~/composables/useProductHistory'
import { usePermissions } from '~/composables/usePermissions'
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { getModelType } from '~/services/modelTypes'
import type { ProductModelStructure } from '~/shared/types/inventory'
import { canPreviewDocument } from '~/utils/documentPreview'
@@ -330,6 +343,7 @@ const {
updateDocument,
} = useDocuments()
const { ensureConstructeurs, getConstructeurById } = useConstructeurs()
const { fetchLinks, syncLinks } = useConstructeurLinks()
const {
history,
loading: historyLoading,
@@ -339,6 +353,9 @@ const {
const isEditMode = ref(false)
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
const product = ref<any | null>(null)
const productType = ref<any | null>(null)
const structure = ref<ProductModelStructure | null>(null)
@@ -529,17 +546,19 @@ const hydrateForm = () => {
}
editionForm.name = product.value.name || ''
editionForm.reference = product.value.reference || ''
editionForm.constructeurIds = uniqueConstructeurIds(
product.value,
Array.isArray(product.value.constructeurs) ? product.value.constructeurs : [],
)
// Load constructeur links
fetchLinks('product', String(route.params.id)).then((links) => {
constructeurLinks.value = links
originalConstructeurLinks.value = links.map(l => ({ ...l }))
editionForm.constructeurIds = constructeurIdsFromLinks(links)
if (editionForm.constructeurIds.length) {
void ensureConstructeurs(editionForm.constructeurIds)
}
})
editionForm.supplierPrice = product.value.supplierPrice !== null && product.value.supplierPrice !== undefined
? String(product.value.supplierPrice)
: ''
refreshCustomFieldInputs(structure.value, product.value.customFieldValues)
if (editionForm.constructeurIds.length) {
ensureConstructeurs(editionForm.constructeurIds).catch(() => {})
}
}
watch(
@@ -557,12 +576,9 @@ const submitEdition = async () => {
return
}
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
const payload: Record<string, any> = {
name: editionForm.name.trim(),
reference: editionForm.reference.trim() || null,
constructeurIds,
}
const rawPrice = typeof editionForm.supplierPrice === 'string'
@@ -589,6 +605,8 @@ const submitEdition = async () => {
toast.showError(`Impossible d'enregistrer ${failedFields.length} champ(s): ${failedFields.join(', ')}`)
return
}
await syncLinks('product', product.value.id, originalConstructeurLinks.value, constructeurLinks.value)
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
toast.showSuccess('Produit mis à jour avec succès')
await loadProduct()
isEditMode.value = false
@@ -600,6 +618,25 @@ const submitEdition = async () => {
}
}
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
watch(
() => editionForm.constructeurIds,
(ids) => {
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
for (const id of ids) {
if (!currentIds.has(id)) {
const resolved = getConstructeurById(id)
constructeurLinks.value.push({
constructeurId: id,
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
supplierReference: null,
})
}
}
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
},
)
onMounted(async () => {
await loadProduct()
if (route.query.edit === 'true' && canEdit.value) {

View File

@@ -79,6 +79,11 @@
</div>
</div>
<ConstructeurLinksTable
v-if="constructeurLinks.length"
v-model="constructeurLinks"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label">
@@ -175,7 +180,10 @@ import { useToast } from '~/composables/useToast'
import { useCustomFields } from '~/composables/useCustomFields'
import { useDocuments } from '~/composables/useDocuments'
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { useConstructeurs } from '~/composables/useConstructeurs'
import type { ProductModelStructure } from '~/shared/types/inventory'
import type { ModelType } from '~/services/modelTypes'
import {
@@ -197,6 +205,8 @@ const toast = useToast()
const { upsertCustomFieldValue } = useCustomFields()
const { uploadDocuments } = useDocuments()
const { canEdit } = usePermissions()
const { syncLinks } = useConstructeurLinks()
const { getConstructeurById } = useConstructeurs()
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
const selectedTypeId = ref<string>(initialTypeId.value)
@@ -207,6 +217,7 @@ const creationForm = reactive({
constructeurIds: [] as string[],
supplierPrice: '' as string,
})
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const selectedDocuments = ref<File[]>([])
const uploadingDocuments = ref(false)
@@ -300,8 +311,6 @@ const buildPayload = () => {
payload.reference = reference
}
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
const rawPrice = typeof creationForm.supplierPrice === 'string'
? creationForm.supplierPrice.trim()
: creationForm.supplierPrice
@@ -333,6 +342,10 @@ const submitCreation = async () => {
await router.replace(`/product/${result.data.id}?edit=true`)
return
}
// Sync constructeur links after creation
if (constructeurLinks.value.length) {
await syncLinks('product', productId, [], constructeurLinks.value)
}
if (selectedDocuments.value.length) {
uploadingDocuments.value = true
const uploadResult = await uploadDocuments(
@@ -395,6 +408,25 @@ const saveCustomFieldValues = async (productId: string) => {
return failed
}
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
watch(
() => creationForm.constructeurIds,
(ids) => {
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
for (const id of ids) {
if (!currentIds.has(id)) {
const resolved = getConstructeurById(id)
constructeurLinks.value.push({
constructeurId: id,
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
supplierReference: null,
})
}
}
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
},
)
onMounted(async () => {
await loadProductTypes()
if (selectedTypeId.value && !selectedType.value) {