feat(#13): Show form elements depending on other form element values
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||
|
||||
enum class VisibilityConditionOperator {
|
||||
EQUALS,
|
||||
NOT_EQUALS,
|
||||
IS_EMPTY,
|
||||
IS_NOT_EMPTY,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||
|
||||
enum class VisibilityConditionType {
|
||||
SHOW,
|
||||
HIDE,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user