feat(fullstack): Add form element section and stepper

This commit is contained in:
2025-06-01 18:16:38 +02:00
parent 7a9809909b
commit cb9abeed7f
15 changed files with 497 additions and 196 deletions

View File

@@ -665,7 +665,7 @@ components:
required: required:
- id - id
- name - name
- formElements - formElementSections
- isTemplate - isTemplate
- organizationId - organizationId
- createdBy - createdBy
@@ -678,10 +678,10 @@ components:
format: uuid format: uuid
name: name:
type: string type: string
formElements: formElementSections:
type: array type: array
items: items:
$ref: "#/components/schemas/FormElementDto" $ref: "#/components/schemas/FormElementSectionDto"
isTemplate: isTemplate:
type: boolean type: boolean
organizationId: organizationId:
@@ -700,16 +700,16 @@ components:
CreateApplicationFormDto: CreateApplicationFormDto:
required: required:
- name - name
- formElements - formElementSections
- isTemplate - isTemplate
type: object type: object
properties: properties:
name: name:
type: string type: string
formElements: formElementSections:
type: array type: array
items: items:
$ref: "#/components/schemas/CreateFormElementDto" $ref: "#/components/schemas/CreateFormElementSectionDto"
isTemplate: isTemplate:
type: boolean type: boolean
default: false default: false
@@ -728,21 +728,64 @@ components:
items: items:
$ref: "#/components/schemas/ApplicationFormDto" $ref: "#/components/schemas/ApplicationFormDto"
####### Form ####### ####### Form #######
FormElementDto: FormElementSectionDto:
type: object type: object
required: required:
- id - id
- title
- formElements
- applicationFormId - applicationFormId
- options
- type
properties: properties:
id: id:
type: string type: string
format: uuid format: uuid
title:
type: string
shortTitle:
type: string
description:
type: string
formElements:
type: array
items:
$ref: "#/components/schemas/FormElementDto"
applicationFormId: applicationFormId:
type: string type: string
format: uuid format: uuid
CreateFormElementSectionDto:
type: object
required:
- title
- formElements
properties:
id:
type: string
format: uuid
title:
type: string
shortTitle:
type: string
description:
type: string
formElements:
type: array
items:
$ref: "#/components/schemas/CreateFormElementDto"
FormElementDto:
type: object
required:
- id
- options
- type
- formElementSectionId
properties:
id:
type: string
format: uuid
title: title:
type: string type: string
description: description:
@@ -753,6 +796,9 @@ components:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
type: type:
$ref: "#/components/schemas/FormElementType" $ref: "#/components/schemas/FormElementType"
formElementSectionId:
type: string
format: uuid
CreateFormElementDto: CreateFormElementDto:
type: object type: object

View File

@@ -1,6 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.application_form package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import jakarta.persistence.AttributeOverride import jakarta.persistence.AttributeOverride
import jakarta.persistence.AttributeOverrides import jakarta.persistence.AttributeOverrides
@@ -29,7 +29,7 @@ class ApplicationForm(
var name: String = "", var name: String = "",
@OneToMany(mappedBy = "applicationForm", cascade = [CascadeType.ALL], orphanRemoval = true) @OneToMany(mappedBy = "applicationForm", cascade = [CascadeType.ALL], orphanRemoval = true)
var formElements: MutableList<FormElement> = mutableListOf(), var formElementSections: MutableList<FormElementSection> = mutableListOf(),
@Column(nullable = false) @Column(nullable = false)
var isTemplate: Boolean, var isTemplate: Boolean,

View File

@@ -1,6 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.application_form package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementMapper import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSectionMapper
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
@@ -11,12 +11,12 @@ import org.springframework.stereotype.Component
import java.time.LocalDateTime import java.time.LocalDateTime
@Component @Component
class ApplicationFormMapper(private val formElementMapper: FormElementMapper, private val userMapper: UserMapper) { class ApplicationFormMapper(private val formElementSectionMapper: FormElementSectionMapper, private val userMapper: UserMapper) {
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto { fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto {
return ApplicationFormDto( return ApplicationFormDto(
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"), id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
name = applicationForm.name, name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElementDto(it) }, formElementSections = applicationForm.formElementSections.map { formElementSectionMapper.toFormElementSectionDto(it) },
isTemplate = applicationForm.isTemplate, isTemplate = applicationForm.isTemplate,
organizationId = applicationForm.organizationId, organizationId = applicationForm.organizationId,
createdBy = userMapper.toUserDto(applicationForm.createdBy), createdBy = userMapper.toUserDto(applicationForm.createdBy),
@@ -30,7 +30,7 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
return ApplicationForm( return ApplicationForm(
id = applicationForm.id, id = applicationForm.id,
name = applicationForm.name, name = applicationForm.name,
formElements = applicationForm.formElements.map { formElementMapper.toFormElement(it) }.toMutableList(), formElementSections = applicationForm.formElementSections.map { formElementSectionMapper.toFormElementSection(it) }.toMutableList(),
isTemplate = applicationForm.isTemplate, isTemplate = applicationForm.isTemplate,
organizationId = applicationForm.organizationId, organizationId = applicationForm.organizationId,
createdBy = userMapper.toUser(applicationForm.createdBy), createdBy = userMapper.toUser(applicationForm.createdBy),
@@ -53,8 +53,8 @@ class ApplicationFormMapper(private val formElementMapper: FormElementMapper, pr
createdBy = createdBy, createdBy = createdBy,
lastModifiedBy = lastModifiedBy, lastModifiedBy = lastModifiedBy,
) )
applicationForm.formElements = createApplicationFormDto.formElements applicationForm.formElementSections = createApplicationFormDto.formElementSections
.map { formElementMapper.toFormElement(it, applicationForm) } .map { formElementSectionMapper.toFormElementSection(it, applicationForm) }
.toMutableList() .toMutableList()
return applicationForm return applicationForm
} }

View File

@@ -0,0 +1,5 @@
package com.betriebsratkanzlei.legalconsenthub.error
import java.util.UUID
class FormElementSectionNotFoundException(id: UUID): RuntimeException("Couldn't find form element section with ID: $id")

View File

@@ -1,18 +1,16 @@
package com.betriebsratkanzlei.legalconsenthub.form_element; package com.betriebsratkanzlei.legalconsenthub.form_element;
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
import com.betriebsratkanzlei.legalconsenthub.comment.Comment
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
import jakarta.persistence.CascadeType
import jakarta.persistence.CollectionTable import jakarta.persistence.CollectionTable
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.ElementCollection import jakarta.persistence.ElementCollection
import jakarta.persistence.Embeddable
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import java.util.UUID; import java.util.UUID;
@@ -22,10 +20,6 @@ class FormElement(
@GeneratedValue @GeneratedValue
var id: UUID? = null, var id: UUID? = null,
@ManyToOne
@JoinColumn(name = "application_form_id", nullable = false)
var applicationForm: ApplicationForm? = null,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
@@ -35,5 +29,9 @@ class FormElement(
var options: MutableList<FormOption> = mutableListOf(), var options: MutableList<FormOption> = mutableListOf(),
@Column(nullable = false) @Column(nullable = false)
var type: FormElementType var type: FormElementType,
@ManyToOne
@JoinColumn(name = "form_element_section_id", nullable = false)
var formElementSection: FormElementSection? = null
) )

View File

@@ -1,8 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.form_element package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm import com.betriebsratkanzlei.legalconsenthub.error.FormElementSectionNotFoundException
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -10,7 +8,7 @@ import org.springframework.stereotype.Component
@Component @Component
class FormElementMapper( class FormElementMapper(
private val formOptionMapper: FormOptionMapper, private val formOptionMapper: FormOptionMapper,
private val applicationFormRepository: ApplicationFormRepository private val formElementSectionRepository: FormElementSectionRepository
) { ) {
fun toFormElementDto(formElement: FormElement): FormElementDto { fun toFormElementDto(formElement: FormElement): FormElementDto {
return FormElementDto( return FormElementDto(
@@ -19,14 +17,14 @@ class FormElementMapper(
description = formElement.description, description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOptionDto(it) }, options = formElement.options.map { formOptionMapper.toFormOptionDto(it) },
type = formElement.type, type = formElement.type,
applicationFormId = formElement.applicationForm?.id formElementSectionId = formElement.formElementSection?.id
?: throw IllegalStateException("ApplicationForm ID must not be null!") ?: throw IllegalStateException("FormElementSection ID must not be null!")
) )
} }
fun toFormElement(formElement: FormElementDto): FormElement { fun toFormElement(formElement: FormElementDto): FormElement {
val applicationForm = applicationFormRepository.findById(formElement.applicationFormId) val formElementSection = formElementSectionRepository.findById(formElement.formElementSectionId)
.orElseThrow { ApplicationFormNotFoundException(formElement.applicationFormId) } .orElseThrow { FormElementSectionNotFoundException(formElement.formElementSectionId) }
return FormElement( return FormElement(
id = formElement.id, id = formElement.id,
@@ -34,18 +32,18 @@ class FormElementMapper(
description = formElement.description, description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type, type = formElement.type,
applicationForm = applicationForm formElementSection = formElementSection
) )
} }
fun toFormElement(formElement: CreateFormElementDto, applicationForm: ApplicationForm): FormElement { fun toFormElement(formElement: CreateFormElementDto, formElementSection: FormElementSection): FormElement {
return FormElement( return FormElement(
id = null, id = null,
title = formElement.title, title = formElement.title,
description = formElement.description, description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type, type = formElement.type,
applicationForm = applicationForm formElementSection = formElementSection
) )
} }
} }

View File

@@ -0,0 +1,36 @@
package com.betriebsratkanzlei.legalconsenthub.form_element;
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
import jakarta.persistence.CascadeType
import jakarta.persistence.CollectionTable
import jakarta.persistence.Column
import jakarta.persistence.ElementCollection
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import java.util.UUID;
@Entity
class FormElementSection(
@Id
@GeneratedValue
var id: UUID? = null,
@Column(nullable = false)
var title: String,
var shortTitle: String? = null,
var description: String? = null,
@OneToMany(mappedBy = "formElementSection", cascade = [CascadeType.ALL], orphanRemoval = true)
var formElements: MutableList<FormElement> = mutableListOf(),
@ManyToOne
@JoinColumn(name = "application_form_id", nullable = false)
var applicationForm: ApplicationForm? = null,
)

View File

@@ -0,0 +1,53 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementSectionDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionDto
import org.springframework.stereotype.Component
@Component
class FormElementSectionMapper(
private val formElementMapper: FormElementMapper,
private val applicationFormRepository: ApplicationFormRepository
) {
fun toFormElementSectionDto(formElementSection: FormElementSection): FormElementSectionDto {
return FormElementSectionDto(
id = formElementSection.id ?: throw IllegalStateException("FormElementSection ID must not be null!"),
title = formElementSection.title,
description = formElementSection.description,
shortTitle = formElementSection.shortTitle,
formElements = formElementSection.formElements.map { formElementMapper.toFormElementDto(it) },
applicationFormId = formElementSection.applicationForm?.id
?: throw IllegalStateException("ApplicationForm ID must not be null!")
)
}
fun toFormElementSection(formElementSection: FormElementSectionDto): FormElementSection {
val applicationForm = applicationFormRepository.findById(formElementSection.applicationFormId)
.orElseThrow { ApplicationFormNotFoundException(formElementSection.applicationFormId) }
return FormElementSection(
id = formElementSection.id,
title = formElementSection.title,
description = formElementSection.description,
shortTitle = formElementSection.shortTitle,
formElements = formElementSection.formElements.map { formElementMapper.toFormElement(it) }.toMutableList(),
applicationForm = applicationForm
)
}
fun toFormElementSection(createFormElementSection: CreateFormElementSectionDto, applicationForm: ApplicationForm): FormElementSection {
val formElementSection = FormElementSection(
title = createFormElementSection.title,
description = createFormElementSection.description,
shortTitle = createFormElementSection.shortTitle,
applicationForm = applicationForm
)
formElementSection.formElements = createFormElementSection.formElements
.map { formElementMapper.toFormElement(it, formElementSection) }
.toMutableList()
return formElementSection
}
}

View File

@@ -0,0 +1,8 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.UUID
@Repository
interface FormElementSectionRepository : JpaRepository<FormElementSection, UUID>

View File

@@ -50,7 +50,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormElementDto, FormOptionDto } from '~/.api-client' import type { CommentDto, FormElementDto, FormOptionDto } from '~/.api-client'
import { useComment } from '~/composables/comment/useComment' import { useComment } from '~/composables/comment/useComment'
import { resolveComponent } from 'vue' import { resolveComponent } from 'vue'

View File

@@ -1,82 +0,0 @@
<template>
<UDashboardPanel id="home">
<template #header>
<UDashboardNavbar title="Home" :ui="{ right: 'gap-3' }">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<UDropdownMenu :items="items">
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
</UDropdownMenu>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left> toolbar left </template>
</UDashboardToolbar>
</template>
<template #body>
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
<UCard variant="subtle">
<FormEngine
v-if="applicationForm"
v-model="applicationForm.formElements"
:application-form-id="applicationForm.id"
:disabled="isReadOnly"
@click:comments="openComments"
/>
<UButton :disabled="isReadOnly" class="my-3 lg:my-4" @click="onSubmit">Submit</UButton>
</UCard>
</div>
</template>
</UDashboardPanel>
</template>
<script setup lang="ts">
import type { ApplicationFormDto } from '~/.api-client'
const { getApplicationFormById, updateApplicationForm } = useApplicationForm()
const route = useRoute()
const { user } = useAuth()
const items = [
[
{
label: 'Neuer Mitbestimmungsantrag',
icon: 'i-lucide-send',
to: '/create'
}
]
]
const { data } = await useAsyncData<ApplicationFormDto>(async () => {
return await getApplicationFormById(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
})
const applicationForm = computed({
get: () => data?.value,
set: (val) => {
if (val && data.value) {
data.value = val
}
}
})
const isReadOnly = computed(() => {
return applicationForm.value?.createdBy.id !== user.value?.id
})
async function onSubmit() {
if (data?.value) {
await updateApplicationForm(data.value.id, data.value)
await navigateTo('/')
}
}
function openComments(formElementId: string) {
console.log('open comments for', formElementId)
}
</script>

View File

@@ -0,0 +1,159 @@
<template>
<UDashboardPanel id="home">
<template #header>
<UDashboardNavbar title="Home" :ui="{ right: 'gap-3' }">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<UDropdownMenu :items="items">
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
</UDropdownMenu>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left> toolbar left </template>
</UDashboardToolbar>
</template>
<template #body>
<div class="flex flex-col w-full lg:max-w-4xl mx-auto">
<UStepper ref="stepper" v-model="activeStepperItemIndex" :items="stepperItems" class="w-full" />
<h1 class="text-xl text-pretty font-bold text-highlighted">
{{ currentFormElementSection.title }}
</h1>
<UCard variant="subtle">
<FormEngine
v-if="applicationForm"
v-model="currentFormElementSection.formElements"
:application-form-id="applicationForm.id"
:disabled="isReadOnly"
@click:comments="openComments"
/>
<div class="flex gap-2 justify-between mt-4">
<UButton
leading-icon="i-lucide-arrow-left"
:disabled="!stepper?.hasPrev"
@click="navigateStepper('backward')"
>
Prev
</UButton>
<UButton
v-if="stepper?.hasNext"
trailing-icon="i-lucide-arrow-right"
:disabled="!stepper?.hasNext"
@click="navigateStepper('forward')"
>
Next
</UButton>
<UButton
v-if="!stepper?.hasNext"
trailing-icon="i-lucide-send-horizontal"
:disabled="isReadOnly"
@click="onSubmit"
>
Submit
</UButton>
</div>
</UCard>
</div>
</template>
</UDashboardPanel>
</template>
<script setup lang="ts">
import type { ApplicationFormDto, FormElementSectionDto } from '~/.api-client'
import type { StepperItem } from '@nuxt/ui'
const { getApplicationFormById, updateApplicationForm } = useApplicationForm()
const route = useRoute()
const { user } = useAuth()
const items = [
[
{
label: 'Neuer Mitbestimmungsantrag',
icon: 'i-lucide-send',
to: '/create'
}
]
]
const stepper = useTemplateRef('stepper')
const activeStepperItemIndex = ref<number>(0)
const currentFormElementSection = computed<FormElementSectionDto>(() => {
return applicationForm.value?.formElementSections[activeStepperItemIndex.value]
})
watch(activeStepperItemIndex, async (newActiveStepperItem: number) => {
activeStepperItemIndex.value = newActiveStepperItem
await navigateTo(`/application-forms/${route.params.id}/${newActiveStepperItem}`)
})
watch(
() => route.path,
(_) => {
const sectionIndex = parseInt(route.params.sectionIndex[0])
activeStepperItemIndex.value = !isNaN(sectionIndex) ? sectionIndex : 0
},
{ immediate: true }
)
const { data, error } = await useAsyncData<ApplicationFormDto>(async () => {
return await getApplicationFormById(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
})
if (error.value) {
throw createError({ statusText: error.value.message })
}
const applicationForm = computed<ApplicationFormDto>({
get: () => data?.value as ApplicationFormDto,
set: (val: ApplicationFormDto) => {
if (val && data.value) {
data.value = val
}
}
})
const isReadOnly = computed(() => {
return applicationForm.value?.createdBy.id !== user.value?.id
})
const stepperItems = computed(() => {
const stepperItems: StepperItem[] = []
applicationForm.value.formElementSections.forEach((section: FormElementSectionDto, index: number, _) => {
stepperItems.push({
title: section.shortTitle,
description: section.description
})
})
return stepperItems
})
async function navigateStepper(direction: 'forward' | 'backward') {
if (direction === 'forward') {
stepper.value?.next()
} else {
stepper.value?.prev()
}
const targetSectionIndex =
direction === 'forward' ? activeStepperItemIndex.value + 1 : activeStepperItemIndex.value - 1
await navigateTo(`/application-forms/${route.params.id}/${targetSectionIndex}`)
}
async function onSubmit() {
if (data?.value) {
await updateApplicationForm(data.value.id, data.value)
await navigateTo('/')
}
}
function openComments(formElementId: string) {
console.log('open comments for', formElementId)
}
</script>

View File

@@ -26,8 +26,33 @@
<UFormField label="Name"> <UFormField label="Name">
<UInput v-if="applicationFormTemplate" v-model="applicationFormTemplate.name" /> <UInput v-if="applicationFormTemplate" v-model="applicationFormTemplate.name" />
</UFormField> </UFormField>
<FormEngine v-model="formElements" /> <UStepper ref="stepper" v-model="activeStepperItem" :items="stepperItems" class="w-full" />
<UButton type="submit">Submit</UButton> <h1 v-if="currentFormElementSection?.title" class="text-xl text-pretty font-bold text-highlighted">
{{ currentFormElementSection.title }}
</h1>
<FormEngine
v-if="currentFormElementSection?.formElements"
v-model="currentFormElementSection.formElements"
/>
<div class="flex gap-2 justify-between mt-4">
<UButton
leading-icon="i-lucide-arrow-left"
:disabled="!stepper?.hasPrev"
@click="navigateStepper('backward')"
>
Prev
</UButton>
<UButton
v-if="stepper?.hasNext"
trailing-icon="i-lucide-arrow-right"
:disabled="!stepper?.hasNext"
@click="navigateStepper('forward')"
>
Next
</UButton>
<UButton v-if="!stepper?.hasNext" @click="onSubmit"> Submit </UButton>
</div>
</UForm> </UForm>
</UPageCard> </UPageCard>
</div> </div>
@@ -36,20 +61,60 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client' import { ComplianceStatus, type FormElementSectionDto, type PagedApplicationFormDto } from '~/.api-client'
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator' import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
import type { FormElementId } from '~/types/FormElement' import type { FormElementId } from '~/types/FormElement'
import type { StepperItem } from '@nuxt/ui'
const { getAllApplicationFormTemplates } = useApplicationFormTemplate() const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
const { createApplicationForm } = useApplicationForm() const { createApplicationForm } = useApplicationForm()
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator() const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
const { userDto, selectedOrganization } = useAuth() const { userDto, selectedOrganization } = useAuth()
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => { const stepper = useTemplateRef('stepper')
const activeStepperItem = ref<number>(0)
const currentFormElementSection = computed(() => {
return applicationFormTemplate.value?.formElementSections[activeStepperItem.value]
})
watch(activeStepperItem, async (newActiveStepperItem: number) => {
activeStepperItem.value = newActiveStepperItem
})
const { data, error } = await useAsyncData<PagedApplicationFormDto>(async () => {
return await getAllApplicationFormTemplates() return await getAllApplicationFormTemplates()
}) })
if (error.value) {
throw createError({ statusText: error.value.message })
}
const stepperItems = computed(() => {
const stepperItems: StepperItem[] = []
if (!applicationFormTemplate.value) {
return stepperItems
}
applicationFormTemplate.value.formElementSections.forEach((section: FormElementSectionDto) => {
stepperItems.push({
title: section.shortTitle,
description: section.description
})
})
return stepperItems
})
async function navigateStepper(direction: 'forward' | 'backward') {
if (direction === 'forward') {
stepper.value?.next()
} else {
stepper.value?.prev()
}
}
const applicationFormTemplate = computed({ const applicationFormTemplate = computed({
// TODO: Don't select always the first item, allow user to select a template
get: () => data?.value?.content[0] ?? undefined, get: () => data?.value?.content[0] ?? undefined,
set: (val) => { set: (val) => {
if (val && data.value) { if (val && data.value) {
@@ -59,10 +124,11 @@ const applicationFormTemplate = computed({
}) })
const formElements = computed({ const formElements = computed({
get: () => applicationFormTemplate.value?.formElements ?? [], get: () => currentFormElementSection?.value?.formElements ?? [],
set: (val) => { set: (val) => {
if (val && applicationFormTemplate.value) { if (val && applicationFormTemplate.value) {
applicationFormTemplate.value.formElements = val if (!currentFormElementSection.value) return
currentFormElementSection.value.formElements = val
} }
} }
}) })

View File

@@ -36,7 +36,7 @@
v-for="(applicationFormElem, index) in applicationForms" v-for="(applicationFormElem, index) in applicationForms"
:key="applicationFormElem.id" :key="applicationFormElem.id"
class="flex justify-between items-center p-4 bg-white rounded-lg shadow-md" class="flex justify-between items-center p-4 bg-white rounded-lg shadow-md"
@click="navigateTo(`application-forms/${applicationFormElem.id}`)" @click="navigateTo(`application-forms/${applicationFormElem.id}/0`)"
> >
<div> <div>
<p class="font-medium text-(--ui-text-highlighted) text-base"> <p class="font-medium text-(--ui-text-highlighted) text-base">

View File

@@ -3,83 +3,97 @@
"name": "", "name": "",
"createdBy": "Denis", "createdBy": "Denis",
"lastModifiedBy": "Denis", "lastModifiedBy": "Denis",
"formElements": [ "formElementSections": [
{ {
"title": "Zustimmung erforderlich", "title": "Section 1",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", "shortTitle": "S1",
"options": [ "description": "First section of the form",
"formElements": [
{ {
"value": "false", "title": "Zustimmung erforderlich",
"label": "Zustimmen (schwerwiegend)", "description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"processingPurpose": "BUSINESS_PROCESS", "options": [
"employeeDataCategory": "SENSITIVE" {
} "value": "false",
], "label": "Zustimmen (schwerwiegend)",
"type": "SWITCH" "processingPurpose": "BUSINESS_PROCESS",
}, "employeeDataCategory": "SENSITIVE"
{ }
"title": "Zustimmung erforderlich", ],
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", "type": "SWITCH"
"options": [
{
"value": "false",
"label": "Zustimmen (keine Auswirkungen)",
"processingPurpose": "NONE",
"employeeDataCategory": "NONE"
}
],
"type": "SWITCH"
},
{
"title": "Zustimmung erforderlich",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"options": [
{
"value": "false",
"label": "Zustimmen (Mittel)",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "CHECKBOX"
},
{
"title": "Eine weitere Zustimmung erforderlich",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"options": [
{
"value": "false",
"label": "Zustimmen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "SENSITIVE"
}, },
{ {
"value": "false", "title": "Zustimmung erforderlich",
"label": "Ablehnen", "description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"processingPurpose": "DATA_ANALYSIS", "options": [
"employeeDataCategory": "REVIEW_REQUIRED" {
} "value": "false",
], "label": "Zustimmen (keine Auswirkungen)",
"type": "SELECT" "processingPurpose": "NONE",
}, "employeeDataCategory": "NONE"
{ }
"title": "Eine weitere Zustimmung erforderlich", ],
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", "type": "SWITCH"
"options": [
{
"value": "false",
"label": "Zustimmen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "SENSITIVE"
}, },
{ {
"value": "false", "title": "Zustimmung erforderlich",
"label": "Ablehnen", "description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"processingPurpose": "DATA_ANALYSIS", "options": [
"employeeDataCategory": "REVIEW_REQUIRED" {
"value": "false",
"label": "Zustimmen (Mittel)",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "CHECKBOX"
} }
], ]
"type": "RADIOBUTTON" },
{
"title": "Section 2",
"shortTitle": "S2",
"description": "Second section of the form",
"formElements": [
{
"title": "Eine weitere Zustimmung erforderlich",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"options": [
{
"value": "false",
"label": "Zustimmen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "false",
"label": "Ablehnen",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "SELECT"
},
{
"title": "Eine weitere Zustimmung erforderlich",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
"options": [
{
"value": "false",
"label": "Zustimmen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "false",
"label": "Ablehnen",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "RADIOBUTTON"
}
]
} }
] ]
} }