feat(machine) : single save button + link versioning display
- Replace auto-save-on-blur with single "Enregistrer" button - Add Cancel button that resets local state - Expose saveFieldDefinitions via defineExpose on MachineInfoCard - Remove standalone save button from MachineCustomFieldDefEditor - Add saveAllMachineCustomFields batch method - Add submitEdition/cancelEdition/saving/canSubmit to orchestrator - Show diff summary badges in version list entries - Show link changes in restore modal description Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,15 @@
|
||||
<span>{{ formatDate(entry.createdAt) }}</span>
|
||||
<span v-if="entry.actor">· {{ entry.actor.label }}</span>
|
||||
</div>
|
||||
<div v-if="entry.diff && Object.keys(entry.diff).length" class="mt-1 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="(change, field) in entry.diff"
|
||||
:key="field"
|
||||
class="badge badge-ghost badge-xs"
|
||||
>
|
||||
{{ formatDiffEntry(String(field), change) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="canRestore && entry.version !== currentVersion"
|
||||
@@ -117,6 +126,16 @@ const targetVersion = ref<number | null>(null)
|
||||
const actionLabel = (action: string) => historyActionLabel(action)
|
||||
const formatDate = (date: string) => formatHistoryDate(date)
|
||||
|
||||
const formatDiffEntry = (field: string, change: { from: unknown; to: unknown }): string => {
|
||||
const label = props.fieldLabels[field] || field
|
||||
// Link changes (addedComponent, removedPiece, etc.) have {id, name} as value
|
||||
const val = change.to ?? change.from
|
||||
if (val && typeof val === 'object' && 'name' in (val as Record<string, unknown>)) {
|
||||
return `${label}: ${(val as Record<string, unknown>).name}`
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
const handleRestore = async (version: number) => {
|
||||
targetVersion.value = version
|
||||
previewData.value = null
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<li>Nom, reference, prix</li>
|
||||
<li>Site</li>
|
||||
<li>Fournisseurs</li>
|
||||
<li>Composants, pieces et produits lies</li>
|
||||
<li>Champs personnalises</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
<template>
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">
|
||||
Définitions des champs personnalisés
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
:disabled="saving"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
<span v-if="saving" class="loading loading-spinner loading-xs" />
|
||||
Enregistrer les champs
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold">
|
||||
Définitions des champs personnalisés
|
||||
</h3>
|
||||
|
||||
<p v-if="!fields.length" class="text-xs text-gray-500">
|
||||
Aucun champ personnalisé défini. Cliquez sur « Ajouter » pour en créer un.
|
||||
@@ -117,7 +106,6 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
save: []
|
||||
'add-field': []
|
||||
'remove-field': [index: number]
|
||||
}>()
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
@input="$emit('update:machine-name', ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('blur-field')"
|
||||
/>
|
||||
<div v-else class="input input-bordered bg-base-200">
|
||||
{{ machineName }}
|
||||
@@ -28,7 +27,7 @@
|
||||
v-if="isEditMode"
|
||||
:value="machineSiteId"
|
||||
class="select select-bordered"
|
||||
@change="$emit('update:machine-site-id', ($event.target as HTMLSelectElement).value); $emit('blur-field')"
|
||||
@change="$emit('update:machine-site-id', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="">Sélectionner un site</option>
|
||||
<option
|
||||
@@ -54,7 +53,6 @@
|
||||
type="text"
|
||||
class="input input-bordered"
|
||||
@input="$emit('update:machine-reference', ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('blur-field')"
|
||||
/>
|
||||
<div v-else class="input input-bordered bg-base-200">
|
||||
{{ machineReference }}
|
||||
@@ -115,7 +113,6 @@
|
||||
class="input input-bordered input-sm"
|
||||
:required="field.required"
|
||||
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('update-custom-field', field)"
|
||||
/>
|
||||
<input
|
||||
v-else-if="field.type === 'number'"
|
||||
@@ -124,7 +121,6 @@
|
||||
class="input input-bordered input-sm"
|
||||
:required="field.required"
|
||||
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('update-custom-field', field)"
|
||||
/>
|
||||
<select
|
||||
v-else-if="field.type === 'select'"
|
||||
@@ -132,7 +128,6 @@
|
||||
class="select select-bordered select-sm"
|
||||
:required="field.required"
|
||||
@change="$emit('set-custom-field-value', field, ($event.target as HTMLSelectElement).value)"
|
||||
@blur="$emit('update-custom-field', field)"
|
||||
>
|
||||
<option value="">Sélectionner...</option>
|
||||
<option
|
||||
@@ -149,7 +144,6 @@
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
:checked="String(field.value).toLowerCase() === 'true'"
|
||||
@change="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).checked ? 'true' : 'false')"
|
||||
@blur="$emit('update-custom-field', field)"
|
||||
>
|
||||
<span class="text-sm" :class="String(field.value).toLowerCase() === 'true' ? 'text-success font-medium' : 'text-base-content/60'">{{ String(field.value).toLowerCase() === 'true' ? 'Oui' : 'Non' }}</span>
|
||||
</label>
|
||||
@@ -160,7 +154,6 @@
|
||||
class="input input-bordered input-sm"
|
||||
:required="field.required"
|
||||
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||
@blur="$emit('update-custom-field', field)"
|
||||
/>
|
||||
<div v-else class="text-xs text-error">
|
||||
Type de champ non pris en charge
|
||||
@@ -184,7 +177,6 @@
|
||||
:on-drag-enter="fieldDefs.onDragEnter"
|
||||
:on-drop="fieldDefs.onDrop"
|
||||
:on-drag-end="fieldDefs.onDragEnd"
|
||||
@save="fieldDefs.saveDefinitions()"
|
||||
@add-field="fieldDefs.addField()"
|
||||
@remove-field="fieldDefs.removeField($event)"
|
||||
/>
|
||||
@@ -224,9 +216,7 @@ const emit = defineEmits<{
|
||||
'update:machine-reference': [value: string]
|
||||
'update:machine-site-id': [value: string]
|
||||
'update:constructeur-ids': [ids: unknown]
|
||||
'blur-field': []
|
||||
'set-custom-field-value': [field: any, value: unknown]
|
||||
'update-custom-field': [field: any]
|
||||
'custom-fields-saved': []
|
||||
}>()
|
||||
|
||||
@@ -239,4 +229,8 @@ const fieldDefs = useMachineCustomFieldDefs({
|
||||
watch(() => props.machineCustomFieldDefs, (newDefs) => {
|
||||
fieldDefs.reinit(newDefs)
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({
|
||||
saveFieldDefinitions: () => fieldDefs.saveDefinitions(),
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -376,6 +376,58 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
|
||||
}
|
||||
}
|
||||
|
||||
const saveAllMachineCustomFields = async () => {
|
||||
if (!machine.value) return
|
||||
|
||||
const fields = Array.isArray(machineCustomFields.value) ? machineCustomFields.value : []
|
||||
const fieldsToSave = fields.filter(
|
||||
(field) => field.value !== undefined && field.value !== null && String(field.value).trim() !== '',
|
||||
)
|
||||
|
||||
for (const field of fieldsToSave) {
|
||||
const { id: customFieldId, customFieldValueId } = field
|
||||
|
||||
try {
|
||||
if (customFieldValueId) {
|
||||
await updateCustomFieldValueApi(customFieldValueId as string, {
|
||||
value: field.value ?? '',
|
||||
} as any)
|
||||
} else if (customFieldId) {
|
||||
const result: any = await upsertCustomFieldValue(
|
||||
customFieldId as string,
|
||||
'machine',
|
||||
machine.value.id as string,
|
||||
field.value ?? '',
|
||||
)
|
||||
if (result.success) {
|
||||
const createdValue = result.data as AnyRecord
|
||||
if (createdValue?.id) {
|
||||
field.customFieldValueId = createdValue.id
|
||||
if (!createdValue.customField) {
|
||||
createdValue.customField = {
|
||||
id: customFieldId,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required,
|
||||
options: field.options,
|
||||
}
|
||||
}
|
||||
const existingValues = Array.isArray(machine.value.customFieldValues)
|
||||
? (machine.value.customFieldValues as AnyRecord[]).filter(
|
||||
(item) => item.id !== createdValue.id,
|
||||
)
|
||||
: []
|
||||
machine.value.customFieldValues = [...existingValues, createdValue]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde du champ personnalisé:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
machineCustomFields,
|
||||
@@ -392,5 +444,6 @@ export function useMachineDetailCustomFields(deps: MachineDetailCustomFieldsDeps
|
||||
setMachineCustomFieldValue,
|
||||
updateMachineCustomField,
|
||||
updatePieceCustomField,
|
||||
saveAllMachineCustomFields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export function useMachineDetailData(machineId: string) {
|
||||
const machine = ref<AnyRecord | null>(null)
|
||||
const productDocumentsMap = ref<Map<string, AnyRecord[]>>(new Map())
|
||||
const printAreaRef = ref<HTMLElement | null>(null)
|
||||
const saving = ref(false)
|
||||
|
||||
// Machine fields
|
||||
const machineName = ref('')
|
||||
@@ -108,6 +109,12 @@ export function useMachineDetailData(machineId: string) {
|
||||
|
||||
// UI state
|
||||
const isEditMode = ref(false)
|
||||
const canSubmit = computed(() => {
|
||||
if (!machine.value) return false
|
||||
if (saving.value) return false
|
||||
if (!machineName.value.trim()) return false
|
||||
return true
|
||||
})
|
||||
const debug = ref(false)
|
||||
|
||||
const componentsCollapsed = ref(true)
|
||||
@@ -146,6 +153,7 @@ export function useMachineDetailData(machineId: string) {
|
||||
setMachineCustomFieldValue,
|
||||
updateMachineCustomField,
|
||||
updatePieceCustomField,
|
||||
saveAllMachineCustomFields,
|
||||
} = useMachineDetailCustomFields({
|
||||
machine,
|
||||
isEditMode,
|
||||
@@ -302,6 +310,37 @@ export function useMachineDetailData(machineId: string) {
|
||||
pieceCollapseToggleToken.value += 1
|
||||
}
|
||||
|
||||
const submitEdition = async () => {
|
||||
if (!machine.value || saving.value) return
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
// 1. Save machine info (name, reference, site, constructeurs)
|
||||
await updateMachineInfo()
|
||||
|
||||
// 2. Save all custom field values
|
||||
await saveAllMachineCustomFields()
|
||||
|
||||
// 3. Reload machine data to get fresh state
|
||||
await loadMachineData()
|
||||
|
||||
// 4. Exit edit mode
|
||||
isEditMode.value = false
|
||||
toast.showSuccess('Machine mise à jour avec succès')
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
toast.showError('Erreur lors de la sauvegarde de la machine')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEdition = () => {
|
||||
initMachineFields()
|
||||
syncMachineCustomFields()
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
// Print wrappers
|
||||
const ensurePrintSelectionEntries = () =>
|
||||
_ensurePrintEntries(components.value, machinePieces.value)
|
||||
@@ -451,6 +490,7 @@ export function useMachineDetailData(machineId: string) {
|
||||
updateMachineInfo, updateComponent, updatePieceFromComponent,
|
||||
updatePieceInfo, handleMachineConstructeurChange, editComponent, editPiece,
|
||||
toggleEditMode, toggleAllComponents, collapseAllComponents, toggleAllPieces,
|
||||
saving, canSubmit, submitEdition, cancelEdition,
|
||||
|
||||
// Print
|
||||
printModalOpen, printSelection, ensurePrintSelectionEntries,
|
||||
|
||||
@@ -208,9 +208,8 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleMachineConstructeurChange = async (value: unknown) => {
|
||||
const handleMachineConstructeurChange = (value: unknown) => {
|
||||
machineConstructeurIds.value = uniqueConstructeurIds(value)
|
||||
await updateMachineInfo()
|
||||
}
|
||||
|
||||
const editComponent = () => {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
<!-- Machine Info Card -->
|
||||
<MachineInfoCard
|
||||
ref="machineInfoCardRef"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:machine-name="d.machineName.value"
|
||||
:machine-reference="d.machineReference.value"
|
||||
@@ -71,9 +72,7 @@
|
||||
@update:machine-reference="d.machineReference.value = $event"
|
||||
@update:machine-site-id="d.machineSiteId.value = $event"
|
||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||
@blur-field="() => { d.updateMachineInfo(); refreshVersions() }"
|
||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||
@update-custom-field="d.updateMachineCustomField"
|
||||
@custom-fields-saved="() => { d.loadMachineData(); refreshVersions() }"
|
||||
/>
|
||||
|
||||
@@ -97,7 +96,7 @@
|
||||
:products="d.machineDirectProducts.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
@add-product="openAddModal('product')"
|
||||
@remove-product="d.removeProductLink"
|
||||
@remove-product="async (id) => { await d.removeProductLink(id); refreshVersions() }"
|
||||
/>
|
||||
|
||||
<!-- Components Section -->
|
||||
@@ -112,7 +111,7 @@
|
||||
@edit-piece="d.updatePieceFromComponent"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
@add-component="openAddModal('component')"
|
||||
@remove-component="d.removeComponentLink"
|
||||
@remove-component="async (id) => { await d.removeComponentLink(id); refreshVersions() }"
|
||||
/>
|
||||
|
||||
<!-- Machine Pieces Section -->
|
||||
@@ -126,7 +125,7 @@
|
||||
@edit-piece="d.editPiece"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
@add-piece="openAddModal('piece')"
|
||||
@remove-piece="d.removePieceLink"
|
||||
@remove-piece="async (id) => { await d.removePieceLink(id); refreshVersions() }"
|
||||
@toggle-collapse="d.toggleAllPieces"
|
||||
/>
|
||||
|
||||
@@ -138,6 +137,27 @@
|
||||
@confirm="handleAddEntity"
|
||||
/>
|
||||
|
||||
<!-- Save / Cancel buttons -->
|
||||
<div v-if="d.isEditMode.value" class="flex flex-col gap-3 md:flex-row md:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
:class="{ 'btn-disabled': d.saving.value }"
|
||||
@click="d.cancelEdition()"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:disabled="!d.canSubmit.value"
|
||||
@click="submitMachineEdition"
|
||||
>
|
||||
<span v-if="d.saving.value" class="loading loading-spinner loading-sm mr-2" />
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Historique -->
|
||||
<EntityHistorySection
|
||||
:entries="history"
|
||||
@@ -224,6 +244,7 @@ if (!machineId) {
|
||||
}
|
||||
|
||||
const d = useMachineDetailData(machineId)
|
||||
const machineInfoCardRef = ref(null)
|
||||
const versionRefreshKey = ref(0)
|
||||
const refreshVersions = () => { versionRefreshKey.value++ }
|
||||
|
||||
@@ -240,6 +261,15 @@ const historyFieldLabels = {
|
||||
prix: 'Prix',
|
||||
site: 'Site',
|
||||
constructeurIds: 'Fournisseurs',
|
||||
addedComponent: 'Composant ajouté',
|
||||
removedComponent: 'Composant supprimé',
|
||||
addedPiece: 'Pièce ajoutée',
|
||||
removedPiece: 'Pièce supprimée',
|
||||
addedProduct: 'Produit ajouté',
|
||||
removedProduct: 'Produit supprimé',
|
||||
componentLinks: 'Composants liés',
|
||||
pieceLinks: 'Pièces liées',
|
||||
productLinks: 'Produits liés',
|
||||
}
|
||||
|
||||
const addModalOpen = ref(false)
|
||||
@@ -258,12 +288,21 @@ const handleAddEntity = async (entityId) => {
|
||||
} else {
|
||||
await d.addProductLink(entityId)
|
||||
}
|
||||
refreshVersions()
|
||||
}
|
||||
|
||||
const machineViewTitle = computed(() => {
|
||||
return d.isEditMode.value ? 'Modification de la machine' : 'Détails de la machine'
|
||||
})
|
||||
|
||||
const submitMachineEdition = async () => {
|
||||
if (machineInfoCardRef.value?.saveFieldDefinitions) {
|
||||
await machineInfoCardRef.value.saveFieldDefinitions()
|
||||
}
|
||||
await d.submitEdition()
|
||||
refreshVersions()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
d.loadMachineData()
|
||||
d.loadInitialData()
|
||||
|
||||
Reference in New Issue
Block a user