diff --git a/app/components/common/EntityVersionList.vue b/app/components/common/EntityVersionList.vue new file mode 100644 index 0000000..c37d370 --- /dev/null +++ b/app/components/common/EntityVersionList.vue @@ -0,0 +1,151 @@ + + + diff --git a/app/components/common/VersionRestoreModal.vue b/app/components/common/VersionRestoreModal.vue new file mode 100644 index 0000000..57b44fc --- /dev/null +++ b/app/components/common/VersionRestoreModal.vue @@ -0,0 +1,197 @@ + + + diff --git a/app/composables/useEntityVersions.ts b/app/composables/useEntityVersions.ts new file mode 100644 index 0000000..43572e3 --- /dev/null +++ b/app/composables/useEntityVersions.ts @@ -0,0 +1,98 @@ +import { ref, toValue } from 'vue' +import { useApi } from '~/composables/useApi' +import type { MaybeRef } from 'vue' + +export interface VersionEntry { + version: number + action: 'create' | 'update' | 'restore' | string + createdAt: string + actor: { id: string; label: string } | null + diff: Record | null +} + +export interface RestorePreview { + version: number + restoreMode: 'full' | 'partial' + diff: Record + warnings: Array<{ + field: string + message: string + missingEntityId: string | null + missingEntityName: string | null + }> + snapshot: Record +} + +export interface RestoreResult { + success: boolean + newVersion: number + restoredFromVersion: number + restoreMode: 'full' | 'partial' + warnings: RestorePreview['warnings'] +} + +const ENTITY_ENDPOINTS: Record = { + machine: '/machines', + composant: '/composants', + piece: '/pieces', + product: '/products', +} + +interface Deps { + entityType: MaybeRef + entityId: MaybeRef +} + +export function useEntityVersions(deps: Deps) { + const { get, post } = useApi() + + const versions = ref([]) + const loading = ref(false) + const error = ref(null) + + const getPath = () => { + const type = toValue(deps.entityType) + const id = toValue(deps.entityId) + const base = ENTITY_ENDPOINTS[type] + return `${base}/${id}` + } + + const fetchVersions = async () => { + loading.value = true + error.value = null + try { + const result = await get(`${getPath()}/versions`) + if (!result.success) { + error.value = result.error ?? 'Impossible de charger les versions.' + versions.value = [] + return + } + versions.value = result.data?.items ?? [] + } + catch (err: any) { + error.value = err?.message ?? 'Erreur inconnue' + versions.value = [] + } + finally { + loading.value = false + } + } + + const fetchPreview = async (version: number): Promise => { + const result = await get(`${getPath()}/versions/${version}/preview`) + if (!result.success || !result.data) { + return null + } + return result.data + } + + const restore = async (version: number): Promise => { + const result = await post(`${getPath()}/versions/${version}/restore`, {}) + if (!result.success || !result.data) { + return null + } + return result.data + } + + return { versions, loading, error, fetchVersions, fetchPreview, restore } +} diff --git a/app/pages/component/[id]/edit.vue b/app/pages/component/[id]/edit.vue index dad8e9c..73506ae 100644 --- a/app/pages/component/[id]/edit.vue +++ b/app/pages/component/[id]/edit.vue @@ -316,11 +316,19 @@ :field-labels="historyFieldLabels" /> + +
Annuler - @@ -349,6 +357,7 @@ import { useDocuments } from '~/composables/useDocuments' const route = useRoute() const { updateDocument } = useDocuments() +const versionRefreshKey = ref(0) const { component, @@ -389,6 +398,7 @@ const { resolveProductLabel, resolveSubcomponentLabel, formatStructurePreview, + fetchComponent, } = useComponentEdit(String(route.params.id)) const editingDocument = ref(null) diff --git a/app/pages/component/[id]/index.vue b/app/pages/component/[id]/index.vue index 819f4b3..d941cc6 100644 --- a/app/pages/component/[id]/index.vue +++ b/app/pages/component/[id]/index.vue @@ -207,15 +207,15 @@ :resolve-subcomponent-label="resolveSubcomponentLabel" /> - +
-

Sélections du squelette

+

{{ isEditMode ? 'Sélections du squelette' : 'Structure du composant' }}

- Choisissez les pièces, produits et sous-composants pour chaque emplacement requis par la catégorie. + {{ isEditMode ? 'Choisissez les pièces, produits et sous-composants pour chaque emplacement requis par la catégorie.' : 'Pièces, produits et sous-composants associés à ce composant.' }}

@@ -230,26 +230,32 @@ -
-
- -
-
- + +
+ {{ resolvePieceLabel(slot.selectedPieceId) || '— Non sélectionné' }} + x{{ slot.quantity }}
@@ -266,12 +272,17 @@ - + +
+ {{ resolveProductLabel(slot.selectedProductId) || '— Non sélectionné' }} +
@@ -287,12 +298,17 @@ - + +
+ {{ resolveSubcomponentLabel(slot.selectedComponentId) || '— Non sélectionné' }} +
diff --git a/app/pages/machine/[id].vue b/app/pages/machine/[id].vue index d8ed188..291f548 100644 --- a/app/pages/machine/[id].vue +++ b/app/pages/machine/[id].vue @@ -71,10 +71,10 @@ @update:machine-reference="d.machineReference.value = $event" @update:machine-site-id="d.machineSiteId.value = $event" @update:constructeur-ids="d.handleMachineConstructeurChange" - @blur-field="d.updateMachineInfo" + @blur-field="() => { d.updateMachineInfo(); refreshVersions() }" @set-custom-field-value="d.setMachineCustomFieldValue" @update-custom-field="d.updateMachineCustomField" - @custom-fields-saved="d.loadMachineData()" + @custom-fields-saved="() => { d.loadMachineData(); refreshVersions() }" /> @@ -146,6 +146,17 @@ :field-labels="historyFieldLabels" /> + + +
{ versionRefreshKey.value++ } const { history, diff --git a/app/pages/piece/[id].vue b/app/pages/piece/[id].vue index 59e9db5..857fbca 100644 --- a/app/pages/piece/[id].vue +++ b/app/pages/piece/[id].vue @@ -181,17 +181,17 @@
- +

- Produit requis par le squelette + Produits liés

- Cette pièce doit rester liée à un produit catalogue répondant aux critères suivants. + {{ isEditMode ? 'Cette pièce doit rester liée à un produit catalogue répondant aux critères suivants.' : 'Produits associés à cette pièce via le squelette.' }}

    @@ -204,7 +204,7 @@ {{ description }}
-
+
+
+
+ +
+ {{ productSelectionLabels[index] || '— Non sélectionné' }} +
+
+
@@ -329,6 +343,14 @@ :field-labels="historyFieldLabels" /> + +