feat(constructeur) : update composant 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 16:52:50 +02:00
parent c5988ec7a6
commit 80739a4528
6 changed files with 143 additions and 33 deletions

View File

@@ -26,7 +26,9 @@ import {
requiredCustomFieldsFilled as _requiredCustomFieldsFilled, requiredCustomFieldsFilled as _requiredCustomFieldsFilled,
saveCustomFieldValues as _saveCustomFieldValues, saveCustomFieldValues as _saveCustomFieldValues,
} from '~/shared/utils/customFieldFormUtils' } from '~/shared/utils/customFieldFormUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils' import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { import {
getStructurePieces, getStructurePieces,
resolvePieceLabel as _resolvePieceLabel, resolvePieceLabel as _resolvePieceLabel,
@@ -77,6 +79,7 @@ export function useComponentCreate() {
const toast = useToast() const toast = useToast()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const { uploadDocuments } = useDocuments() const { uploadDocuments } = useDocuments()
const { syncLinks } = useConstructeurLinks()
const { canEdit } = usePermissions() const { canEdit } = usePermissions()
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -92,6 +95,8 @@ export function useComponentCreate() {
constructeurIds: [] as string[], constructeurIds: [] as string[],
prix: '' as string, prix: '' as string,
}) })
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
const lastSuggestedName = ref('') const lastSuggestedName = ref('')
const customFieldInputs = ref<CustomFieldInput[]>([]) const customFieldInputs = ref<CustomFieldInput[]>([])
const structureAssignments = ref<StructureAssignmentNode | null>(null) const structureAssignments = ref<StructureAssignmentNode | null>(null)
@@ -276,9 +281,7 @@ export function useComponentCreate() {
payload.reference = reference payload.reference = reference
} }
if (creationForm.constructeurIds.length) { // constructeurIds are handled via link entities, not in the main payload
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
}
const rawPrice = typeof creationForm.prix === 'string' const rawPrice = typeof creationForm.prix === 'string'
? creationForm.prix.trim() ? creationForm.prix.trim()
@@ -343,6 +346,10 @@ export function useComponentCreate() {
} }
selectedDocuments.value = [] selectedDocuments.value = []
} }
// Sync constructeur links after creation
if (constructeurLinks.value.length) {
await syncLinks('composant', createdComponent.id, [], constructeurLinks.value)
}
toast.showSuccess('Composant créé avec succès') toast.showSuccess('Composant créé avec succès')
await router.replace(`/component/${createdComponent.id}?edit=true`) await router.replace(`/component/${createdComponent.id}?edit=true`)
} }
@@ -380,6 +387,8 @@ export function useComponentCreate() {
selectedTypeId, selectedTypeId,
submitting, submitting,
creationForm, creationForm,
constructeurLinks,
constructeurIdsFromForm,
customFieldInputs, customFieldInputs,
structureAssignments, structureAssignments,
selectedDocuments, selectedDocuments,

View File

@@ -12,9 +12,11 @@ import { useToast } from '~/composables/useToast'
import { extractRelationId } from '~/shared/apiRelations' import { extractRelationId } from '~/shared/apiRelations'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs' import { useConstructeurs } from '~/composables/useConstructeurs'
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
import { useComponentHistory } from '~/composables/useComponentHistory' import { useComponentHistory } from '~/composables/useComponentHistory'
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils' import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
import { uniqueConstructeurIds } from '~/shared/constructeurUtils' import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
import { import {
getStructurePieces, getStructurePieces,
getStructureProducts, getStructureProducts,
@@ -61,6 +63,7 @@ export function useComponentEdit(componentId: string) {
const { pieces } = usePieces() const { pieces } = usePieces()
const { products } = useProducts() const { products } = useProducts()
const { ensureConstructeurs } = useConstructeurs() const { ensureConstructeurs } = useConstructeurs()
const { fetchLinks, syncLinks } = useConstructeurLinks()
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields() const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
const toast = useToast() const toast = useToast()
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments() const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
@@ -89,6 +92,9 @@ export function useComponentEdit(componentId: string) {
constructeurIds: [] as string[], constructeurIds: [] as string[],
prix: '' as string, prix: '' as string,
}) })
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
const constructeurIdsFromForm = computed(() => constructeurIdsFromLinks(constructeurLinks.value))
const customFieldInputs = ref<CustomFieldInput[]>([]) const customFieldInputs = ref<CustomFieldInput[]>([])
const fetchedPieceTypeMap = ref<Record<string, string>>({}) const fetchedPieceTypeMap = ref<Record<string, string>>({})
@@ -286,6 +292,7 @@ export function useComponentEdit(componentId: string) {
slotId: slot.slotId, slotId: slot.slotId,
typePieceId: slot.typePieceId, typePieceId: slot.typePieceId,
selectedPieceId: edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null), selectedPieceId: edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null),
selectedPieceName: slot.selectedPieceName ?? null,
quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1), quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1),
position: slot.position ?? i, position: slot.position ?? i,
label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`, label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`,
@@ -302,6 +309,7 @@ export function useComponentEdit(componentId: string) {
slotId: slot.slotId, slotId: slot.slotId,
typeProductId: slot.typeProductId, typeProductId: slot.typeProductId,
selectedProductId: edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null), selectedProductId: edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null),
selectedProductName: slot.selectedProductName ?? null,
familyCode: slot.familyCode, familyCode: slot.familyCode,
position: slot.position ?? i, position: slot.position ?? i,
label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`, label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`,
@@ -318,6 +326,7 @@ export function useComponentEdit(componentId: string) {
slotId: slot.slotId, slotId: slot.slotId,
typeComposantId: slot.typeComposantId, typeComposantId: slot.typeComposantId,
selectedComponentId: edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null), selectedComponentId: edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null),
selectedComponentName: slot.selectedComponentName ?? null,
alias: slot.alias, alias: slot.alias,
familyCode: slot.familyCode, familyCode: slot.familyCode,
position: slot.position ?? i, position: slot.position ?? i,
@@ -361,7 +370,6 @@ export function useComponentEdit(componentId: string) {
const reference = editionForm.reference.trim() const reference = editionForm.reference.trim()
payload.reference = reference || null payload.reference = reference || null
payload.constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
if (rawPrice) { if (rawPrice) {
const parsed = Number(rawPrice) const parsed = Number(rawPrice)
@@ -434,6 +442,9 @@ export function useComponentEdit(componentId: string) {
slotEdits.products = {} slotEdits.products = {}
slotEdits.subcomponents = {} slotEdits.subcomponents = {}
await syncLinks('composant', component.value.id, originalConstructeurLinks.value, constructeurLinks.value)
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
toast.showSuccess('Composant mis à jour avec succès.') toast.showSuccess('Composant mis à jour avec succès.')
} }
} }
@@ -468,15 +479,16 @@ export function useComponentEdit(componentId: string) {
editionForm.name = currentComponent.name || '' editionForm.name = currentComponent.name || ''
editionForm.description = currentComponent.description || '' editionForm.description = currentComponent.description || ''
editionForm.reference = currentComponent.reference || '' editionForm.reference = currentComponent.reference || ''
editionForm.constructeurIds = uniqueConstructeurIds( // Load constructeur links
currentComponent, fetchLinks('composant', componentId).then((links) => {
Array.isArray(currentComponent.constructeurs) ? currentComponent.constructeurs : [], constructeurLinks.value = links
currentComponent.constructeur ? [currentComponent.constructeur] : [], originalConstructeurLinks.value = links.map(l => ({ ...l }))
) editionForm.constructeurIds = constructeurIdsFromLinks(links)
if (editionForm.constructeurIds.length) {
void ensureConstructeurs(editionForm.constructeurIds)
}
})
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : '' editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
if (editionForm.constructeurIds.length) {
void ensureConstructeurs(editionForm.constructeurIds)
}
initialized.value = true initialized.value = true
} }
@@ -543,6 +555,9 @@ export function useComponentEdit(componentId: string) {
previewVisible, previewVisible,
selectedTypeId, selectedTypeId,
editionForm, editionForm,
constructeurLinks,
originalConstructeurLinks,
constructeurIdsFromForm,
customFieldInputs, customFieldInputs,
historyFieldLabels, historyFieldLabels,

View File

@@ -1,7 +1,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useToast } from './useToast' import { useToast } from './useToast'
import { useApi } from './useApi' import { useApi } from './useApi'
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils' import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
import { useConstructeurs, type Constructeur } from './useConstructeurs' import { useConstructeurs, type Constructeur } from './useConstructeurs'
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations' import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
import { extractCollection } from '~/shared/utils/apiHelpers' import { extractCollection } from '~/shared/utils/apiHelpers'
@@ -182,7 +182,8 @@ export function useComposants() {
const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => { const createComposant = async (composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
loading.value = true loading.value = true
try { try {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData)) const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = composantData as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
const result = await post('/composants', normalizedPayload) const result = await post('/composants', normalizedPayload)
if (result.success && result.data) { if (result.success && result.data) {
const enriched = await withResolvedConstructeurs(result.data as Composant) const enriched = await withResolvedConstructeurs(result.data as Composant)
@@ -209,7 +210,8 @@ export function useComposants() {
const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => { const updateComposantData = async (id: string, composantData: Partial<Composant>): Promise<ComposantSingleResult> => {
loading.value = true loading.value = true
try { try {
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(composantData)) const { constructeurIds, constructeurs, constructeurId, constructeur, ...cleanPayload } = composantData as any
const normalizedPayload = normalizeRelationIds(cleanPayload)
const result = await patch(`/composants/${id}`, normalizedPayload) const result = await patch(`/composants/${id}`, normalizedPayload)
if (result.success && result.data) { if (result.success && result.data) {
const updated = await withResolvedConstructeurs(result.data as Composant) const updated = await withResolvedConstructeurs(result.data as Composant)

View File

@@ -141,6 +141,11 @@
</div> </div>
</div> </div>
<ConstructeurLinksTable
v-if="constructeurLinks.length"
v-model="constructeurLinks"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
@@ -350,13 +355,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRoute } from '#imports' import { useRoute } from '#imports'
import { useComponentEdit } from '~/composables/useComponentEdit' import { useComponentEdit } from '~/composables/useComponentEdit'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { useConstructeurs } from '~/composables/useConstructeurs'
const route = useRoute() const route = useRoute()
const { updateDocument } = useDocuments() const { updateDocument } = useDocuments()
const { getConstructeurById } = useConstructeurs()
const versionRefreshKey = ref(0) const versionRefreshKey = ref(0)
const { const {
@@ -371,6 +378,8 @@ const {
previewVisible, previewVisible,
selectedTypeId, selectedTypeId,
editionForm, editionForm,
constructeurLinks,
constructeurIdsFromForm,
customFieldInputs, customFieldInputs,
historyFieldLabels, historyFieldLabels,
canEdit, canEdit,
@@ -401,6 +410,26 @@ const {
fetchComponent, fetchComponent,
} = useComponentEdit(String(route.params.id)) } = useComponentEdit(String(route.params.id))
// 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,
})
}
}
// Remove links whose ID was removed from the select
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
},
)
const editingDocument = ref<any | null>(null) const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false) const editModalVisible = ref(false)

View File

@@ -128,7 +128,7 @@
<!-- Référence + Fournisseurs (if value or edit mode) --> <!-- Référence + Fournisseurs (if value or edit mode) -->
<div <div
v-if="isEditMode || component.reference || editionForm.constructeurIds.length" v-if="isEditMode || component.reference || constructeurLinks.length"
class="grid grid-cols-1 gap-4 md:grid-cols-2" class="grid grid-cols-1 gap-4 md:grid-cols-2"
> >
<div v-if="isEditMode || component.reference" class="form-control"> <div v-if="isEditMode || component.reference" class="form-control">
@@ -148,7 +148,7 @@
</div> </div>
</div> </div>
<div v-if="isEditMode || editionForm.constructeurIds.length" class="form-control"> <div v-if="isEditMode || constructeurLinks.length" class="form-control">
<label class="label"> <label class="label">
<span class="label-text">Fournisseur</span> <span class="label-text">Fournisseur</span>
</label> </label>
@@ -160,18 +160,20 @@
placeholder="Rechercher un ou plusieurs fournisseurs..." placeholder="Rechercher un ou plusieurs fournisseurs..."
:initial-options="component?.constructeurs || []" :initial-options="component?.constructeurs || []"
/> />
<div v-else class="flex flex-wrap gap-2">
<span
v-for="id in editionForm.constructeurIds"
:key="id"
class="badge badge-outline"
>
{{ getConstructeurById(id)?.name || id }}
</span>
</div>
</div> </div>
</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 (if value or edit mode) --> <!-- Prix (if value or edit mode) -->
<div v-if="isEditMode || component.prix" class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div v-if="isEditMode || component.prix" class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control"> <div class="form-control">
@@ -254,7 +256,7 @@
</div> </div>
</template> </template>
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2"> <div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2">
{{ resolvePieceLabel(slot.selectedPieceId) || '— Non sélectionné' }} {{ slot.selectedPieceName || '— Non sélectionné' }}
<span v-if="slot.quantity > 1" class="badge badge-sm">x{{ slot.quantity }}</span> <span v-if="slot.quantity > 1" class="badge badge-sm">x{{ slot.quantity }}</span>
</div> </div>
</div> </div>
@@ -281,7 +283,7 @@
/> />
</template> </template>
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center"> <div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
{{ resolveProductLabel(slot.selectedProductId) || '— Non sélectionné' }} {{ slot.selectedProductName || '— Non sélectionné' }}
</div> </div>
</div> </div>
</div> </div>
@@ -307,7 +309,7 @@
/> />
</template> </template>
<div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center"> <div v-else class="input input-bordered input-sm md:input-md bg-base-200 flex items-center">
{{ resolveSubcomponentLabel(slot.selectedComponentId) || '— Non sélectionné' }} {{ slot.selectedComponentName || '— Non sélectionné' }}
</div> </div>
</div> </div>
</div> </div>
@@ -435,12 +437,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from '#imports' import { useRoute } from '#imports'
import { useComponentEdit } from '~/composables/useComponentEdit' import { useComponentEdit } from '~/composables/useComponentEdit'
import { useDocuments } from '~/composables/useDocuments' import { useDocuments } from '~/composables/useDocuments'
import { usePermissions } from '~/composables/usePermissions' import { usePermissions } from '~/composables/usePermissions'
import { useConstructeurs } from '~/composables/useConstructeurs' import { useConstructeurs } from '~/composables/useConstructeurs'
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
const route = useRoute() const route = useRoute()
const { canEdit } = usePermissions() const { canEdit } = usePermissions()
@@ -461,6 +464,8 @@ const {
previewVisible, previewVisible,
selectedTypeId, selectedTypeId,
editionForm, editionForm,
constructeurLinks,
constructeurIdsFromForm,
customFieldInputs, customFieldInputs,
historyFieldLabels, historyFieldLabels,
canSubmit, canSubmit,
@@ -497,6 +502,26 @@ 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,
})
}
}
// Remove links whose ID was removed from the select
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
},
)
const editingDocument = ref<any | null>(null) const editingDocument = ref<any | null>(null)
const editModalVisible = ref(false) const editModalVisible = ref(false)

View File

@@ -92,6 +92,11 @@
</div> </div>
</div> </div>
<ConstructeurLinksTable
v-if="constructeurLinks.length"
v-model="constructeurLinks"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
@@ -215,10 +220,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch } from 'vue'
import { useConstructeurs } from '~/composables/useConstructeurs'
const { getConstructeurById } = useConstructeurs()
const { const {
selectedTypeId, selectedTypeId,
submitting, submitting,
creationForm, creationForm,
constructeurLinks,
customFieldInputs, customFieldInputs,
structureAssignments, structureAssignments,
selectedDocuments, selectedDocuments,
@@ -249,4 +260,23 @@ const {
resolveSubcomponentLabel, resolveSubcomponentLabel,
submitCreation, submitCreation,
} = useComponentCreate() } = useComponentCreate()
// 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))
},
)
</script> </script>