diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8db37b0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 2025-11-02 - Form Element Management API + +- New input field with title and text needed to be added to application forms +- Adding new form elements to existing application forms caused type conflicts: frontend needed to add `CreateFormElementDto` (without ID) to arrays of `FormElementDto` (with ID) +- Implemented separate endpoint for adding form elements: `POST /application-forms/{applicationFormId}/sections/{sectionId}/form-elements` + diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index f34bc54..43a4b0f 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -224,6 +224,57 @@ paths: "503": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + /application-forms/{applicationFormId}/sections/{sectionId}/form-elements: + post: + summary: Add a new form element to a specific section + operationId: addFormElementToSection + tags: + - application-form + parameters: + - name: applicationFormId + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the application form + - name: sectionId + in: path + required: true + schema: + type: string + format: uuid + description: The ID of the form element section + - name: position + in: query + required: true + schema: + type: integer + description: The position to insert the form element + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateFormElementDto" + responses: + "201": + description: Form element successfully added + content: + application/json: + schema: + $ref: "#/components/schemas/ApplicationFormDto" + "400": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest" + "401": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "404": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound" + "500": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + "503": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + ####### Application Form Templates ####### /application-form-templates: get: @@ -1080,6 +1131,7 @@ components: - RADIOBUTTON - TEXTFIELD - SWITCH + - TITLE_BODY_TEXTFIELDS ####### UserDto ####### UserDto: diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt index dd835ae..641ae1a 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt @@ -3,6 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.application_form import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto +import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource @@ -108,4 +109,24 @@ class ApplicationFormController( applicationFormService.submitApplicationForm(id), ), ) + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')", + ) + override fun addFormElementToSection( + applicationFormId: UUID, + sectionId: UUID, + position: Int, + createFormElementDto: CreateFormElementDto, + ): ResponseEntity = + ResponseEntity.status(201).body( + applicationFormMapper.toApplicationFormDto( + applicationFormService.addFormElementToSection( + applicationFormId, + sectionId, + createFormElementDto, + position, + ), + ), + ) } 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 30a0c42..c4ddf1e 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 @@ -5,10 +5,13 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedExc import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException +import com.betriebsratkanzlei.legalconsenthub.error.FormElementSectionNotFoundException +import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementMapper import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto +import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType import org.springframework.data.domain.Page @@ -20,6 +23,7 @@ import java.util.UUID class ApplicationFormService( private val applicationFormRepository: ApplicationFormRepository, private val applicationFormMapper: ApplicationFormMapper, + private val formElementMapper: FormElementMapper, private val notificationService: NotificationService, ) { fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { @@ -45,7 +49,6 @@ class ApplicationFormService( } fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm { - // TODO find statt mappen? val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto) val updatedApplicationForm: ApplicationForm @@ -112,4 +115,35 @@ class ApplicationFormService( notificationService.createNotificationForOrganization(createNotificationDto) } + + fun addFormElementToSection( + applicationFormId: UUID, + sectionId: UUID, + createFormElementDto: CreateFormElementDto, + position: Int, + ): ApplicationForm { + val applicationForm = getApplicationFormById(applicationFormId) + + val section = + applicationForm.formElementSections + .find { it.id == sectionId } + ?: throw FormElementSectionNotFoundException(sectionId) + + val newFormElement = formElementMapper.toFormElement(createFormElementDto, section) + + if (position >= 0 && position < section.formElements.size) { + section.formElements.add(position, newFormElement) + } else { + section.formElements.add(newFormElement) + } + + val updatedApplicationForm = + try { + applicationFormRepository.save(applicationForm) + } catch (e: Exception) { + throw ApplicationFormNotUpdatedException(e, applicationFormId) + } + + return updatedApplicationForm + } } diff --git a/legalconsenthub/components/FormEngine.vue b/legalconsenthub/components/FormEngine.vue index 08a5515..3dd8592 100644 --- a/legalconsenthub/components/FormEngine.vue +++ b/legalconsenthub/components/FormEngine.vue @@ -1,28 +1,28 @@ @@ -31,6 +31,7 @@ import type { FormElementDto, FormOptionDto } from '~/.api-client' import { resolveComponent } from 'vue' import TheComment from '~/components/TheComment.vue' +import type { DropdownMenuItem } from '@nuxt/ui' const props = defineProps<{ modelValue: FormElementDto[] @@ -41,6 +42,7 @@ const props = defineProps<{ const emit = defineEmits<{ (e: 'update:modelValue', formElementDto: FormElementDto[]): void (e: 'click:comments', formElementId: string): void + (e: 'add:input-form', position: number): void }>() const commentStore = useCommentStore() @@ -66,11 +68,30 @@ function getResolvedComponent(formElement: FormElementDto) { return resolveComponent('TheSwitch') case 'TEXTFIELD': return resolveComponent('TheInput') + case 'TITLE_BODY_TEXTFIELDS': + return resolveComponent('TheTitleBodyInput') default: return resolveComponent('Unimplemented') } } +function getDropdownItems(formElementId: string, formElementPosition: number): DropdownMenuItem[] { + return [ + [ + { + label: 'Comments', + icon: 'i-lucide-message-square-more', + onClick: () => toggleComments(formElementId) + }, + { + label: 'Add input field below', + icon: 'i-lucide-list-plus', + onClick: () => emit('add:input-form', formElementPosition) + } + ] + ] +} + function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) { const updatedModelValue = [...props.modelValue] updatedModelValue[formElementIndex] = { ...updatedModelValue[formElementIndex], options: formOptions } diff --git a/legalconsenthub/components/formelements/TheTitleBodyInput.vue b/legalconsenthub/components/formelements/TheTitleBodyInput.vue new file mode 100644 index 0000000..360586b --- /dev/null +++ b/legalconsenthub/components/formelements/TheTitleBodyInput.vue @@ -0,0 +1,66 @@ + + + diff --git a/legalconsenthub/composables/applicationForm/useApplicationForm.ts b/legalconsenthub/composables/applicationForm/useApplicationForm.ts index 0f40c3a..70cb796 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationForm.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationForm.ts @@ -1,4 +1,9 @@ -import { type CreateApplicationFormDto, type ApplicationFormDto, type PagedApplicationFormDto } from '~/.api-client' +import type { + CreateApplicationFormDto, + CreateFormElementDto, + ApplicationFormDto, + PagedApplicationFormDto +} from '~/.api-client' import { useApplicationFormApi } from './useApplicationFormApi' export function useApplicationForm() { @@ -71,12 +76,36 @@ export function useApplicationForm() { } } + async function addFormElementToSection( + applicationFormId: string, + sectionId: string, + createFormElementDto: CreateFormElementDto, + position: number + ): Promise { + if (!applicationFormId || !sectionId) { + return Promise.reject(new Error('Application form ID or section ID missing')) + } + + try { + return await applicationFormApi.addFormElementToSection( + applicationFormId, + sectionId, + createFormElementDto, + position + ) + } catch (e: unknown) { + console.error(`Failed adding form element to section ${sectionId}:`, e) + return Promise.reject(e) + } + } + return { createApplicationForm, getAllApplicationForms, getApplicationFormById, updateApplicationForm, deleteApplicationFormById, - submitApplicationForm + submitApplicationForm, + addFormElementToSection } } diff --git a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts index c43725c..b4d3eb7 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts @@ -2,6 +2,7 @@ import { ApplicationFormApi, Configuration, type CreateApplicationFormDto, + type CreateFormElementDto, type ApplicationFormDto, type PagedApplicationFormDto } from '~/.api-client' @@ -53,12 +54,27 @@ export function useApplicationFormApi() { return applicationFormApiClient.submitApplicationForm({ id }) } + async function addFormElementToSection( + applicationFormId: string, + sectionId: string, + createFormElementDto: CreateFormElementDto, + position: number + ): Promise { + return applicationFormApiClient.addFormElementToSection({ + applicationFormId, + sectionId, + createFormElementDto, + position + }) + } + return { createApplicationForm, getAllApplicationForms, getApplicationFormById, updateApplicationForm, deleteApplicationFormById, - submitApplicationForm + submitApplicationForm, + addFormElementToSection } } diff --git a/legalconsenthub/composables/useFormElementManagement.ts b/legalconsenthub/composables/useFormElementManagement.ts new file mode 100644 index 0000000..7181122 --- /dev/null +++ b/legalconsenthub/composables/useFormElementManagement.ts @@ -0,0 +1,48 @@ +import type { ApplicationFormDto, CreateFormElementDto, FormElementSectionDto } from '~/.api-client' +import type { MaybeRefOrGetter } from 'vue' + +export function useFormElementManagement( + currentFormElementSection: MaybeRefOrGetter, + applicationFormId?: string +) { + const { addFormElementToSection } = useApplicationForm() + + async function addInputFormToApplicationForm(position: number): Promise { + const section = toValue(currentFormElementSection) + if (!section) return + + const { formElements } = section + + const inputFormElement: CreateFormElementDto = { + title: 'Formular ergänzen', + description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.', + options: [ + { + value: '|||', + label: '', + processingPurpose: 'NONE', + employeeDataCategory: 'NONE' + } + ], + type: 'TITLE_BODY_TEXTFIELDS' + } + + if (applicationFormId) { + try { + return await addFormElementToSection(applicationFormId, section.id, inputFormElement, position + 1) + } catch (error) { + console.error('Failed to add form element:', error) + throw error + } + } else { + // @ts-expect-error Add CreateFormElementDto to formElements array. ID will be generated by the backend. + formElements.splice(position + 1, 0, inputFormElement) + return undefined + } + } + + return { + addInputFormToApplicationForm + } +} + diff --git a/legalconsenthub/composables/useFormStepper.ts b/legalconsenthub/composables/useFormStepper.ts new file mode 100644 index 0000000..8a393c0 --- /dev/null +++ b/legalconsenthub/composables/useFormStepper.ts @@ -0,0 +1,58 @@ +import type { FormElementSectionDto } from '~/.api-client' +import type { StepperItem } from '@nuxt/ui' +import type { MaybeRefOrGetter } from 'vue' + +interface Stepper { + hasPrev: boolean + hasNext: boolean + next: () => void + prev: () => void +} + +export function useFormStepper( + formElementSections: MaybeRefOrGetter, + options?: { + onNavigate?: (direction: 'forward' | 'backward', newIndex: number) => void | Promise + } +) { + const stepper = useTemplateRef('stepper') + const activeStepperItemIndex = ref(0) + + const sections = computed(() => toValue(formElementSections) ?? []) + + const stepperItems = computed(() => { + const items: StepperItem[] = [] + sections.value.forEach((section: FormElementSectionDto) => { + items.push({ + title: section.shortTitle, + description: section.description + }) + }) + return items + }) + + const currentFormElementSection = computed( + () => sections.value[activeStepperItemIndex.value] + ) + + async function navigateStepper(direction: 'forward' | 'backward') { + if (direction === 'forward') { + stepper.value?.next() + } else { + stepper.value?.prev() + } + + if (options?.onNavigate) { + await options.onNavigate(direction, activeStepperItemIndex.value) + } + } + + return { + stepper, + activeStepperItemIndex, + stepperItems, + currentFormElementSection, + navigateStepper + } +} + diff --git a/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue b/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue index 3ac3645..00615ea 100644 --- a/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue +++ b/legalconsenthub/pages/application-forms/[id]/[sectionIndex].vue @@ -31,15 +31,16 @@