From ef440d29702f9d27250dfc7fc721f8063971f5e2 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sun, 9 Nov 2025 08:17:10 +0100 Subject: [PATCH] feat(#4): Add comparison for versions, refactor version restoring --- .../application_form/ApplicationForm.kt | 3 + .../ApplicationFormService.kt | 5 +- .../ApplicationFormVersionService.kt | 9 + legalconsenthub/app/components/FormEngine.vue | 1 + .../app/components/RestoreVersionModal.vue | 32 +++ .../app/components/VersionComparisonModal.vue | 192 ++++++++++++++++++ .../app/components/VersionHistory.vue | 80 ++++---- .../pages/application-forms/[id]/versions.vue | 2 +- legalconsenthub/app/utils/formDiff.ts | 163 +++++++++++++++ legalconsenthub/types/formDiff.ts | 31 +++ 10 files changed, 473 insertions(+), 45 deletions(-) create mode 100644 legalconsenthub/app/components/RestoreVersionModal.vue create mode 100644 legalconsenthub/app/components/VersionComparisonModal.vue create mode 100644 legalconsenthub/app/utils/formDiff.ts create mode 100644 legalconsenthub/types/formDiff.ts diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt index c46e61f..998b11b 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationForm.kt @@ -1,5 +1,6 @@ package com.betriebsratkanzlei.legalconsenthub.application_form +import com.betriebsratkanzlei.legalconsenthub.application_form_version.ApplicationFormVersion import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus @@ -30,6 +31,8 @@ class ApplicationForm( var name: String = "", @OneToMany(mappedBy = "applicationForm", cascade = [CascadeType.ALL], orphanRemoval = true) var formElementSections: MutableList = mutableListOf(), + @OneToMany(mappedBy = "applicationForm", cascade = [CascadeType.ALL], orphanRemoval = true) + var versions: MutableList = mutableListOf(), @Column(nullable = false) var isTemplate: Boolean, var organizationId: String = "", diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt index 50662c6..5860f9c 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt @@ -56,6 +56,7 @@ class ApplicationFormService( } fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm { + val existingApplicationForm = getApplicationFormById(applicationFormDto.id) val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto) val updatedApplicationForm: ApplicationForm @@ -66,7 +67,9 @@ class ApplicationFormService( } val currentUser = userService.getCurrentUser() - versionService.createVersion(updatedApplicationForm, currentUser) + if (versionService.hasChanges(existingApplicationForm, updatedApplicationForm)) { + versionService.createVersion(updatedApplicationForm, currentUser) + } return updatedApplicationForm } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form_version/ApplicationFormVersionService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form_version/ApplicationFormVersionService.kt index b959a1d..43cb91c 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form_version/ApplicationFormVersionService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form_version/ApplicationFormVersionService.kt @@ -47,6 +47,15 @@ class ApplicationFormVersionService( return versionRepository.save(version) } + fun hasChanges( + existingForm: ApplicationForm, + newForm: ApplicationForm, + ): Boolean { + val existingSnapshot = createSnapshot(existingForm) + val newSnapshot = createSnapshot(newForm) + return existingSnapshot != newSnapshot + } + fun getVersionsByApplicationFormId(applicationFormId: UUID): List = versionRepository.findByApplicationFormIdOrderByVersionNumberDesc(applicationFormId) diff --git a/legalconsenthub/app/components/FormEngine.vue b/legalconsenthub/app/components/FormEngine.vue index 17b2a1a..d61dc8f 100644 --- a/legalconsenthub/app/components/FormEngine.vue +++ b/legalconsenthub/app/components/FormEngine.vue @@ -94,6 +94,7 @@ function getDropdownItems(formElementId: string, formElementPosition: number): D } function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) { + console.log('Updating form options for element index:', formElementIndex, formOptions) const updatedModelValue = [...props.modelValue] updatedModelValue[formElementIndex] = { ...updatedModelValue[formElementIndex], options: formOptions } emit('update:modelValue', updatedModelValue) diff --git a/legalconsenthub/app/components/RestoreVersionModal.vue b/legalconsenthub/app/components/RestoreVersionModal.vue new file mode 100644 index 0000000..ac3427f --- /dev/null +++ b/legalconsenthub/app/components/RestoreVersionModal.vue @@ -0,0 +1,32 @@ + + + diff --git a/legalconsenthub/app/components/VersionComparisonModal.vue b/legalconsenthub/app/components/VersionComparisonModal.vue new file mode 100644 index 0000000..95557e7 --- /dev/null +++ b/legalconsenthub/app/components/VersionComparisonModal.vue @@ -0,0 +1,192 @@ + + + diff --git a/legalconsenthub/app/components/VersionHistory.vue b/legalconsenthub/app/components/VersionHistory.vue index 00e5b26..3604ed6 100644 --- a/legalconsenthub/app/components/VersionHistory.vue +++ b/legalconsenthub/app/components/VersionHistory.vue @@ -4,21 +4,15 @@ -
- Fehler beim Laden der Versionen: {{ error }} -
+
Fehler beim Laden der Versionen: {{ error }}
-
- Keine Versionen verfügbar -
+
Keine Versionen verfügbar
- - v{{ version.versionNumber }} - + v{{ version.versionNumber }}
{{ version.name }}
@@ -30,6 +24,14 @@ {{ getStatusLabel(version.status) }} +
- - - - + + +
- diff --git a/legalconsenthub/app/pages/application-forms/[id]/versions.vue b/legalconsenthub/app/pages/application-forms/[id]/versions.vue index 9237eb1..795951c 100644 --- a/legalconsenthub/app/pages/application-forms/[id]/versions.vue +++ b/legalconsenthub/app/pages/application-forms/[id]/versions.vue @@ -15,7 +15,7 @@ diff --git a/legalconsenthub/app/utils/formDiff.ts b/legalconsenthub/app/utils/formDiff.ts new file mode 100644 index 0000000..2bb51ef --- /dev/null +++ b/legalconsenthub/app/utils/formDiff.ts @@ -0,0 +1,163 @@ +import type { + ApplicationFormDto, + ApplicationFormSnapshotDto, + FormElementDto, + FormElementSnapshotDto, + FormOptionDto +} from '~~/.api-client' +import type { FormDiff, ElementModification, OptionChange, OptionModification } from '~~/types/formDiff' + +export function compareApplicationForms( + current: ApplicationFormDto, + versionSnapshot: ApplicationFormSnapshotDto +): FormDiff { + const diff: FormDiff = { + elementsAdded: [], + elementsRemoved: [], + elementsModified: [] + } + + const currentElements = flattenFormElements(current) + const versionElements = flattenSnapshotElements(versionSnapshot) + + const currentElementsMap = new Map( + currentElements.map((el, idx) => [ + createElementKey(el.element, idx), + { element: el.element, index: idx, sectionTitle: el.sectionTitle } + ]) + ) + const versionElementsMap = new Map( + versionElements.map((el, idx) => [ + createSnapshotElementKey(el.element, idx), + { element: el.element, index: idx, sectionTitle: el.sectionTitle } + ]) + ) + + for (const [key, currentData] of currentElementsMap) { + if (!versionElementsMap.has(key)) { + diff.elementsAdded.push({ + sectionTitle: currentData.sectionTitle, + title: currentData.element.title, + type: currentData.element.type, + position: currentData.index + }) + } else { + const versionData = versionElementsMap.get(key)! + const modifications = compareElements( + currentData.element, + versionData.element, + currentData.sectionTitle, + currentData.index + ) + if (modifications) { + diff.elementsModified.push(modifications) + } + } + } + + for (const [key, versionData] of versionElementsMap) { + if (!currentElementsMap.has(key)) { + diff.elementsRemoved.push({ + sectionTitle: versionData.sectionTitle, + title: versionData.element.title, + type: versionData.element.type, + position: versionData.index + }) + } + } + + return diff +} + +function flattenFormElements(form: ApplicationFormDto): Array<{ element: FormElementDto; sectionTitle: string }> { + const elements: Array<{ element: FormElementDto; sectionTitle: string }> = [] + for (const section of form.formElementSections) { + for (const element of section.formElements) { + elements.push({ element, sectionTitle: section.title }) + } + } + return elements +} + +function flattenSnapshotElements( + snapshot: ApplicationFormSnapshotDto +): Array<{ element: FormElementSnapshotDto; sectionTitle: string }> { + const elements: Array<{ element: FormElementSnapshotDto; sectionTitle: string }> = [] + for (const section of snapshot.sections) { + for (const element of section.elements) { + elements.push({ element, sectionTitle: section.title }) + } + } + return elements +} + +function createElementKey(element: FormElementDto, index: number): string { + return `${index}-${element.type}-${element.title || 'untitled'}` +} + +function createSnapshotElementKey(element: FormElementSnapshotDto, index: number): string { + return `${index}-${element.type}-${element.title || 'untitled'}` +} + +function compareElements( + current: FormElementDto, + version: FormElementSnapshotDto, + sectionTitle: string, + position: number +): ElementModification | null { + const optionsDiff = compareOptions(current.options, version.options) + + if (optionsDiff.added.length > 0 || optionsDiff.removed.length > 0 || optionsDiff.modified.length > 0) { + return { + sectionTitle, + position, + optionsAdded: optionsDiff.added, + optionsRemoved: optionsDiff.removed, + optionsModified: optionsDiff.modified + } + } + + return null +} + +function compareOptions( + currentOptions: FormOptionDto[], + versionOptions: FormOptionDto[] +): { added: OptionChange[]; removed: OptionChange[]; modified: OptionModification[] } { + const result = { + added: [] as OptionChange[], + removed: [] as OptionChange[], + modified: [] as OptionModification[] + } + + const currentMap = new Map(currentOptions.map((opt) => [opt.value, opt])) + const versionMap = new Map(versionOptions.map((opt) => [opt.value, opt])) + + for (const [value, currentOpt] of currentMap) { + if (!versionMap.has(value)) { + result.added.push({ + value: currentOpt.value, + label: currentOpt.label + }) + } else { + const versionOpt = versionMap.get(value)! + if (currentOpt.label !== versionOpt.label) { + result.modified.push({ + value, + labelChanged: { from: versionOpt.label, to: currentOpt.label } + }) + } + } + } + + for (const [value, versionOpt] of versionMap) { + if (!currentMap.has(value)) { + result.removed.push({ + value: versionOpt.value, + label: versionOpt.label + }) + } + } + + return result +} diff --git a/legalconsenthub/types/formDiff.ts b/legalconsenthub/types/formDiff.ts new file mode 100644 index 0000000..49f3b81 --- /dev/null +++ b/legalconsenthub/types/formDiff.ts @@ -0,0 +1,31 @@ +export interface FormDiff { + elementsAdded: ElementChange[] + elementsRemoved: ElementChange[] + elementsModified: ElementModification[] +} + +export interface ElementChange { + sectionTitle: string + title: string | undefined + type: string + position: number +} + +export interface ElementModification { + sectionTitle: string + position: number + optionsAdded: OptionChange[] + optionsRemoved: OptionChange[] + optionsModified: OptionModification[] +} + +export interface OptionChange { + value: string + label: string +} + +export interface OptionModification { + value: string + labelChanged: { from: string; to: string } +} +