2 Commits

Author SHA1 Message Date
Matthieu
9e1504ddb7 feat(machine) : add entity history section to machine detail page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:31:25 +01:00
Matthieu
a72279f978 refactor(component-edit) : replace slot auto-save with deferred save on submit
Slot selections (piece, product, subcomponent, quantity) are no longer
saved immediately on change. Instead, edits are stored locally and
persisted together with base fields and custom fields when the user
clicks "Enregistrer les modifications".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:45:18 +01:00
4 changed files with 137 additions and 79 deletions

View File

@@ -269,95 +269,78 @@ export function useComponentEdit(componentId: string) {
}
})
// --- Slot selection entries (for selectors) ---
// --- Slot local edits (saved on submit, not auto-saved) ---
const slotEdits = reactive<{
pieces: Record<string, { selectedPieceId?: string | null, quantity?: number }>
products: Record<string, { selectedProductId?: string | null }>
subcomponents: Record<string, { selectedComposantId?: string | null }>
}>({ pieces: {}, products: {}, subcomponents: {} })
const pieceSlotEntries = computed(() => {
const structure = component.value?.structure
if (!structure?.pieces) return []
return (structure.pieces as any[]).map((slot: any, i: number) => ({
slotId: slot.slotId,
typePieceId: slot.typePieceId,
selectedPieceId: slot.selectedPieceId ?? null,
quantity: slot.quantity ?? 1,
position: slot.position ?? i,
label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`,
}))
return (structure.pieces as any[]).map((slot: any, i: number) => {
const edits = slotEdits.pieces[slot.slotId]
return {
slotId: slot.slotId,
typePieceId: slot.typePieceId,
selectedPieceId: edits && 'selectedPieceId' in edits ? edits.selectedPieceId : (slot.selectedPieceId ?? null),
quantity: edits && 'quantity' in edits ? edits.quantity! : (slot.quantity ?? 1),
position: slot.position ?? i,
label: pieceTypeLabelMap.value[slot.typePieceId] || `Pièce #${i + 1}`,
}
})
})
const productSlotEntries = computed(() => {
const structure = component.value?.structure
if (!structure?.products) return []
return (structure.products as any[]).map((slot: any, i: number) => ({
slotId: slot.slotId,
typeProductId: slot.typeProductId,
selectedProductId: slot.selectedProductId ?? null,
familyCode: slot.familyCode,
position: slot.position ?? i,
label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`,
}))
return (structure.products as any[]).map((slot: any, i: number) => {
const edits = slotEdits.products[slot.slotId]
return {
slotId: slot.slotId,
typeProductId: slot.typeProductId,
selectedProductId: edits && 'selectedProductId' in edits ? edits.selectedProductId : (slot.selectedProductId ?? null),
familyCode: slot.familyCode,
position: slot.position ?? i,
label: productTypeLabelMap.value[slot.typeProductId] || `Produit #${i + 1}`,
}
})
})
const subcomponentSlotEntries = computed(() => {
const structure = component.value?.structure
if (!structure?.subcomponents) return []
return (structure.subcomponents as any[]).map((slot: any, i: number) => ({
slotId: slot.slotId,
typeComposantId: slot.typeComposantId,
selectedComponentId: slot.selectedComponentId ?? null,
alias: slot.alias,
familyCode: slot.familyCode,
position: slot.position ?? i,
label: slot.alias || `Sous-composant #${i + 1}`,
}))
return (structure.subcomponents as any[]).map((slot: any, i: number) => {
const edits = slotEdits.subcomponents[slot.slotId]
return {
slotId: slot.slotId,
typeComposantId: slot.typeComposantId,
selectedComponentId: edits && 'selectedComposantId' in edits ? edits.selectedComposantId : (slot.selectedComponentId ?? null),
alias: slot.alias,
familyCode: slot.familyCode,
position: slot.position ?? i,
label: slot.alias || `Sous-composant #${i + 1}`,
}
})
})
const savePieceSlotSelection = async (slotId: string, selectedPieceId: string | null) => {
const result = await patch(`/composant-piece-slots/${slotId}`, { selectedPieceId })
if (result.success) {
const structure = component.value?.structure
if (structure?.pieces) {
const slot = (structure.pieces as any[]).find((s: any) => s.slotId === slotId)
if (slot) slot.selectedPieceId = selectedPieceId
}
toast.showSuccess('Pièce mise à jour')
}
const setPieceSlotSelection = (slotId: string, selectedPieceId: string | null) => {
slotEdits.pieces[slotId] = { ...slotEdits.pieces[slotId], selectedPieceId }
}
const saveProductSlotSelection = async (slotId: string, selectedProductId: string | null) => {
const result = await patch(`/composant-product-slots/${slotId}`, { selectedProductId })
if (result.success) {
const structure = component.value?.structure
if (structure?.products) {
const slot = (structure.products as any[]).find((s: any) => s.slotId === slotId)
if (slot) slot.selectedProductId = selectedProductId
}
toast.showSuccess('Produit mis à jour')
}
const setProductSlotSelection = (slotId: string, selectedProductId: string | null) => {
slotEdits.products[slotId] = { ...slotEdits.products[slotId], selectedProductId }
}
const saveSubcomponentSlotSelection = async (slotId: string, selectedComposantId: string | null) => {
const result = await patch(`/composant-subcomponent-slots/${slotId}`, { selectedComposantId })
if (result.success) {
const structure = component.value?.structure
if (structure?.subcomponents) {
const slot = (structure.subcomponents as any[]).find((s: any) => s.slotId === slotId)
if (slot) slot.selectedComponentId = selectedComposantId
}
toast.showSuccess('Sous-composant mis à jour')
}
const setSubcomponentSlotSelection = (slotId: string, selectedComposantId: string | null) => {
slotEdits.subcomponents[slotId] = { ...slotEdits.subcomponents[slotId], selectedComposantId }
}
const saveSlotQuantity = async (slotId: string, quantity: number) => {
const setSlotQuantity = (slotId: string, quantity: number) => {
if (!slotId || quantity < 1) return
const result = await patch(`/composant-piece-slots/${slotId}`, { quantity: Math.max(1, quantity) })
if (result.success) {
const structure = component.value?.structure
if (structure?.pieces) {
const slot = (structure.pieces as any[]).find((s: any) => s.slotId === slotId)
if (slot) slot.quantity = quantity
}
toast.showSuccess('Quantité mise à jour')
}
slotEdits.pieces[slotId] = { ...slotEdits.pieces[slotId], quantity: Math.max(1, quantity) }
}
const submitEdition = async () => {
@@ -403,6 +386,54 @@ export function useComponentEdit(componentId: string) {
],
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
)
// Save slot edits
const slotPromises: Promise<any>[] = []
for (const [slotId, edits] of Object.entries(slotEdits.pieces)) {
if (Object.keys(edits).length) {
slotPromises.push(patch(`/composant-piece-slots/${slotId}`, {
...'selectedPieceId' in edits ? { selectedPieceId: edits.selectedPieceId } : {},
...'quantity' in edits ? { quantity: edits.quantity } : {},
}))
}
}
for (const [slotId, edits] of Object.entries(slotEdits.products)) {
if ('selectedProductId' in edits) {
slotPromises.push(patch(`/composant-product-slots/${slotId}`, { selectedProductId: edits.selectedProductId }))
}
}
for (const [slotId, edits] of Object.entries(slotEdits.subcomponents)) {
if ('selectedComposantId' in edits) {
slotPromises.push(patch(`/composant-subcomponent-slots/${slotId}`, { selectedComposantId: edits.selectedComposantId }))
}
}
await Promise.all(slotPromises)
// Apply slot edits to local structure so UI reflects saved values
const structure = component.value?.structure
if (structure) {
for (const [slotId, edits] of Object.entries(slotEdits.pieces)) {
const slot = (structure.pieces as any[])?.find((s: any) => s.slotId === slotId)
if (slot) {
if ('selectedPieceId' in edits) slot.selectedPieceId = edits.selectedPieceId
if ('quantity' in edits) slot.quantity = edits.quantity
}
}
for (const [slotId, edits] of Object.entries(slotEdits.products)) {
const slot = (structure.products as any[])?.find((s: any) => s.slotId === slotId)
if (slot && 'selectedProductId' in edits) slot.selectedProductId = edits.selectedProductId
}
for (const [slotId, edits] of Object.entries(slotEdits.subcomponents)) {
const slot = (structure.subcomponents as any[])?.find((s: any) => s.slotId === slotId)
if (slot && 'selectedComposantId' in edits) slot.selectedComponentId = edits.selectedComposantId
}
}
// Reset local slot edits
slotEdits.pieces = {}
slotEdits.products = {}
slotEdits.subcomponents = {}
toast.showSuccess('Composant mis à jour avec succès.')
}
}
@@ -538,10 +569,10 @@ export function useComponentEdit(componentId: string) {
handleFilesAdded,
refreshDocuments,
submitEdition,
saveSlotQuantity,
savePieceSlotSelection,
saveProductSlotSelection,
saveSubcomponentSlotSelection,
setSlotQuantity,
setPieceSlotSelection,
setProductSlotSelection,
setSubcomponentSlotSelection,
resolvePieceLabel,
resolveProductLabel,
resolveSubcomponentLabel,

View File

@@ -23,6 +23,7 @@ export type EntityHistoryEntry = {
}
const ENTITY_ENDPOINTS: Record<string, string> = {
machine: '/machines',
composant: '/composants',
piece: '/pieces',
product: '/products',
@@ -35,7 +36,7 @@ const extractItems = (payload: any): EntityHistoryEntry[] => {
return []
}
export function useEntityHistory(entityType: 'composant' | 'piece' | 'product') {
export function useEntityHistory(entityType: 'machine' | 'composant' | 'piece' | 'product') {
const { get } = useApi()
const basePath = ENTITY_ENDPOINTS[entityType]

View File

@@ -198,7 +198,7 @@
:model-value="slot.selectedPieceId"
:disabled="!canEdit || saving"
:type-piece-id="slot.typePieceId"
@update:model-value="(value) => savePieceSlotSelection(slot.slotId, value)"
@update:model-value="(value) => setPieceSlotSelection(slot.slotId, value)"
/>
</div>
<div class="w-20 shrink-0">
@@ -209,7 +209,7 @@
class="input input-bordered input-sm w-full text-center"
:disabled="!canEdit || saving"
title="Quantité"
@change="(e) => saveSlotQuantity(slot.slotId, Number((e.target as HTMLInputElement).value))"
@change="(e) => setSlotQuantity(slot.slotId, Number((e.target as HTMLInputElement).value))"
>
</div>
</div>
@@ -232,7 +232,7 @@
:model-value="slot.selectedProductId"
:disabled="!canEdit || saving"
:type-product-id="slot.typeProductId"
@update:model-value="(value) => saveProductSlotSelection(slot.slotId, value)"
@update:model-value="(value) => setProductSlotSelection(slot.slotId, value)"
/>
</div>
</div>
@@ -253,7 +253,7 @@
:model-value="slot.selectedComponentId"
:disabled="!canEdit || saving"
:type-composant-id="slot.typeComposantId"
@update:model-value="(value) => saveSubcomponentSlotSelection(slot.slotId, value)"
@update:model-value="(value) => setSubcomponentSlotSelection(slot.slotId, value)"
/>
</div>
</div>
@@ -381,10 +381,10 @@ const {
removeDocument,
handleFilesAdded,
submitEdition,
saveSlotQuantity,
savePieceSlotSelection,
saveProductSlotSelection,
saveSubcomponentSlotSelection,
setSlotQuantity,
setPieceSlotSelection,
setProductSlotSelection,
setSubcomponentSlotSelection,
resolvePieceLabel,
resolveProductLabel,
resolveSubcomponentLabel,

View File

@@ -138,6 +138,14 @@
@confirm="handleAddEntity"
/>
<!-- Historique -->
<EntityHistorySection
:entries="history"
:loading="historyLoading"
:error="historyError"
:field-labels="historyFieldLabels"
/>
<!-- Comments -->
<div class="mt-4">
<CommentSection
@@ -181,6 +189,7 @@
import { computed, ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useMachineDetailData } from '~/composables/useMachineDetailData'
import { useEntityHistory } from '~/composables/useEntityHistory'
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
import PageHero from '~/components/PageHero.vue'
import MachinePrintSelectionModal from '~/components/MachinePrintSelectionModal.vue'
@@ -191,6 +200,7 @@ import MachineProductsCard from '~/components/machine/MachineProductsCard.vue'
import MachineComponentsCard from '~/components/machine/MachineComponentsCard.vue'
import MachinePiecesCard from '~/components/machine/MachinePiecesCard.vue'
import AddEntityToMachineModal from '~/components/machine/AddEntityToMachineModal.vue'
import EntityHistorySection from '~/components/common/EntityHistorySection.vue'
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
const route = useRoute()
@@ -203,6 +213,21 @@ if (!machineId) {
const d = useMachineDetailData(machineId)
const {
history,
loading: historyLoading,
error: historyError,
loadHistory,
} = useEntityHistory('machine')
const historyFieldLabels = {
name: 'Nom',
reference: 'Référence',
prix: 'Prix',
site: 'Site',
constructeurIds: 'Fournisseurs',
}
const addModalOpen = ref(false)
const addModalKind = ref('component')
@@ -228,6 +253,7 @@ const machineViewTitle = computed(() => {
onMounted(() => {
d.loadMachineData()
d.loadInitialData()
loadHistory(String(machineId)).catch(() => {})
if (route.query.edit === 'true' && canEdit.value) {
d.isEditMode.value = true