feat(#13): Show form elements depending on other form element values

This commit is contained in:
2025-11-30 18:10:51 +01:00
parent 79fbf7ce1b
commit 9dc690715b
19 changed files with 1187 additions and 116 deletions

View File

@@ -1,10 +1,16 @@
package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionOperator
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionType
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import org.springframework.stereotype.Service
import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context
import java.io.ByteArrayOutputStream
import java.util.UUID
@Service
class ApplicationFormFormatService(
@@ -24,10 +30,118 @@ class ApplicationFormFormatService(
}
fun generateHtml(applicationForm: ApplicationForm): String {
val filteredForm = filterVisibleElements(applicationForm)
val context =
Context().apply {
setVariable("applicationForm", applicationForm)
setVariable("applicationForm", filteredForm)
}
return templateEngine.process("application_form_template", context)
}
private fun filterVisibleElements(applicationForm: ApplicationForm): ApplicationForm {
val formElementsByRef = buildFormElementsMap(applicationForm)
val visibilityMap = evaluateVisibility(formElementsByRef)
val filteredSections =
applicationForm.formElementSections.mapNotNull { section ->
val filteredSubSections =
section.formElementSubSections.mapNotNull { subsection ->
val filteredElements =
subsection.formElements.filter { element ->
visibilityMap[element.id] == true
}
if (filteredElements.isEmpty()) {
null
} else {
FormElementSubSection(
id = subsection.id,
title = subsection.title,
subtitle = subsection.subtitle,
formElements = filteredElements.toMutableList(),
formElementSection = null,
)
}
}
if (filteredSubSections.isEmpty()) {
null
} else {
FormElementSection(
id = section.id,
title = section.title,
shortTitle = section.shortTitle,
description = section.description,
formElementSubSections = filteredSubSections.toMutableList(),
applicationForm = null,
)
}
}
return ApplicationForm(
id = applicationForm.id,
name = applicationForm.name,
status = applicationForm.status,
createdBy = applicationForm.createdBy,
lastModifiedBy = applicationForm.lastModifiedBy,
createdAt = applicationForm.createdAt,
modifiedAt = applicationForm.modifiedAt,
isTemplate = applicationForm.isTemplate,
organizationId = applicationForm.organizationId,
formElementSections = filteredSections.toMutableList(),
)
}
private fun buildFormElementsMap(applicationForm: ApplicationForm): Map<String, FormElement> {
val map = mutableMapOf<String, FormElement>()
applicationForm.formElementSections.forEach { section ->
section.formElementSubSections.forEach { subsection ->
subsection.formElements.forEach { element ->
element.reference?.let { map[it] = element }
}
}
}
return map
}
private fun evaluateVisibility(formElementsByRef: Map<String, FormElement>): Map<UUID?, Boolean> {
val visibilityMap = mutableMapOf<UUID?, Boolean>()
formElementsByRef.values.forEach { element ->
visibilityMap[element.id] = isElementVisible(element, formElementsByRef)
}
return visibilityMap
}
private fun isElementVisible(
element: FormElement,
formElementsByRef: Map<String, FormElement>,
): Boolean {
val condition = element.visibilityCondition ?: return true
// Don't show if source element is missing
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
val sourceValue = getFormElementValue(sourceElement)
val conditionMet = evaluateCondition(sourceValue, condition.expectedValue, condition.operator)
return when (condition.conditionType) {
VisibilityConditionType.SHOW -> conditionMet
VisibilityConditionType.HIDE -> !conditionMet
}
}
private fun getFormElementValue(element: FormElement): String =
element.options.firstOrNull { it.value == "true" }?.label ?: ""
private fun evaluateCondition(
actualValue: String,
expectedValue: String,
operator: VisibilityConditionOperator,
): Boolean =
when (operator) {
VisibilityConditionOperator.EQUALS -> actualValue.equals(expectedValue, ignoreCase = true)
VisibilityConditionOperator.NOT_EQUALS -> !actualValue.equals(expectedValue, ignoreCase = true)
VisibilityConditionOperator.IS_EMPTY -> actualValue.isEmpty()
VisibilityConditionOperator.IS_NOT_EMPTY -> actualValue.isNotEmpty()
}
}

View File

@@ -16,6 +16,7 @@ class FormElement(
@Id
@GeneratedValue
var id: UUID? = null,
var reference: String? = null,
var title: String? = null,
var description: String? = null,
@ElementCollection
@@ -26,4 +27,5 @@ class FormElement(
@ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null,
var visibilityCondition: FormElementVisibilityCondition? = null,
)

View File

@@ -7,10 +7,12 @@ import org.springframework.stereotype.Component
@Component
class FormElementMapper(
private val formOptionMapper: FormOptionMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
) {
fun toFormElementDto(formElement: FormElement): FormElementDto =
FormElementDto(
id = formElement.id ?: throw IllegalStateException("FormElement ID must not be null!"),
reference = formElement.reference,
title = formElement.title,
description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOptionDto(it) },
@@ -18,6 +20,10 @@ class FormElementMapper(
formElementSubSectionId =
formElement.formElementSubSection?.id
?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
visibilityCondition =
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
},
)
fun toFormElement(
@@ -26,11 +32,16 @@ class FormElementMapper(
): FormElement =
FormElement(
id = formElement.id,
reference = formElement.reference,
title = formElement.title,
description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type,
formElementSubSection = formElementSubSection,
visibilityCondition =
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it)
},
)
fun toFormElement(
@@ -39,10 +50,15 @@ class FormElementMapper(
): FormElement =
FormElement(
id = null,
reference = formElement.reference,
title = formElement.title,
description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
type = formElement.type,
formElementSubSection = formElementSubSection,
visibilityCondition =
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it)
},
)
}

View File

@@ -0,0 +1,15 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import jakarta.persistence.Embeddable
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
@Embeddable
data class FormElementVisibilityCondition(
@Enumerated(EnumType.STRING)
val conditionType: VisibilityConditionType,
val sourceFormElementReference: String,
val expectedValue: String,
@Enumerated(EnumType.STRING)
val operator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
)

View File

@@ -0,0 +1,71 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementVisibilityCondition as FormElementVisibilityConditionDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto
@Component
class FormElementVisibilityConditionMapper {
fun toFormElementVisibilityConditionDto(
condition: FormElementVisibilityCondition,
): FormElementVisibilityConditionDto =
FormElementVisibilityConditionDto(
conditionType = toVisibilityConditionTypeDto(condition.conditionType),
sourceFormElementReference = condition.sourceFormElementReference,
expectedValue = condition.expectedValue,
operator = toVisibilityConditionOperatorDto(condition.operator),
)
fun toFormElementVisibilityCondition(
conditionDto: FormElementVisibilityConditionDto,
): FormElementVisibilityCondition =
FormElementVisibilityCondition(
conditionType = toVisibilityConditionType(conditionDto.conditionType),
sourceFormElementReference = conditionDto.sourceFormElementReference,
expectedValue = conditionDto.expectedValue,
operator =
conditionDto.operator?.let { toVisibilityConditionOperator(it) }
?: VisibilityConditionOperator.EQUALS,
)
private fun toVisibilityConditionTypeDto(type: VisibilityConditionType): VisibilityConditionTypeDto =
when (type) {
VisibilityConditionType.SHOW -> VisibilityConditionTypeDto.SHOW
VisibilityConditionType.HIDE -> VisibilityConditionTypeDto.HIDE
}
private fun toVisibilityConditionType(typeDto: VisibilityConditionTypeDto): VisibilityConditionType =
when (typeDto) {
VisibilityConditionTypeDto.SHOW -> VisibilityConditionType.SHOW
VisibilityConditionTypeDto.HIDE -> VisibilityConditionType.HIDE
}
private fun toVisibilityConditionOperatorDto(
operator: VisibilityConditionOperator,
): VisibilityConditionOperatorDto =
when (operator) {
VisibilityConditionOperator.EQUALS ->
VisibilityConditionOperatorDto.EQUALS
VisibilityConditionOperator.NOT_EQUALS ->
VisibilityConditionOperatorDto.NOT_EQUALS
VisibilityConditionOperator.IS_EMPTY ->
VisibilityConditionOperatorDto.IS_EMPTY
VisibilityConditionOperator.IS_NOT_EMPTY ->
VisibilityConditionOperatorDto.IS_NOT_EMPTY
}
private fun toVisibilityConditionOperator(
operatorDto: VisibilityConditionOperatorDto,
): VisibilityConditionOperator =
when (operatorDto) {
VisibilityConditionOperatorDto.EQUALS ->
VisibilityConditionOperator.EQUALS
VisibilityConditionOperatorDto.NOT_EQUALS ->
VisibilityConditionOperator.NOT_EQUALS
VisibilityConditionOperatorDto.IS_EMPTY ->
VisibilityConditionOperator.IS_EMPTY
VisibilityConditionOperatorDto.IS_NOT_EMPTY ->
VisibilityConditionOperator.IS_NOT_EMPTY
}
}

View File

@@ -0,0 +1,8 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
enum class VisibilityConditionOperator {
EQUALS,
NOT_EQUALS,
IS_EMPTY,
IS_NOT_EMPTY,
}

View File

@@ -0,0 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
enum class VisibilityConditionType {
SHOW,
HIDE,
}

View File

@@ -62,12 +62,17 @@ create table form_element_options
create table form_element
(
form_element_order integer,
type smallint not null check (type between 0 and 5),
form_element_sub_section_id uuid not null,
id uuid not null,
description varchar(255),
title varchar(255),
form_element_order integer,
type smallint not null check (type between 0 and 5),
form_element_sub_section_id uuid not null,
id uuid not null,
condition_type varchar(255) check (condition_type in ('SHOW', 'HIDE')),
description varchar(255),
expected_value varchar(255),
operator varchar(255) check (operator in ('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
reference varchar(255),
source_form_element_reference varchar(255),
title varchar(255),
primary key (id)
);
@@ -165,4 +170,4 @@ alter table if exists form_element_sub_section
alter table if exists notification
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
foreign key (recipient_id)
references app_user
references app_user;