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

@@ -97,12 +97,19 @@ Application Form
└── SubSection (FormElementSubSection) └── SubSection (FormElementSubSection)
├── title, subtitle ├── title, subtitle
└── Form Elements (FormElement) └── Form Elements (FormElement)
├── id (UUID - generated by backend)
├── reference (string - custom key like "art_der_massnahme")
├── type (SELECT, CHECKBOX, RADIOBUTTON, TEXTFIELD, SWITCH, TITLE_BODY_TEXTFIELDS) ├── type (SELECT, CHECKBOX, RADIOBUTTON, TEXTFIELD, SWITCH, TITLE_BODY_TEXTFIELDS)
├── title, description ├── title, description
── options (FormOption[]) ── options (FormOption[])
├── value, label ├── value, label
├── processingPurpose ├── processingPurpose
└── employeeDataCategory └── employeeDataCategory
└── visibilityCondition (FormElementVisibilityCondition)
├── conditionType (SHOW, HIDE)
├── sourceFormElementReference (string - reference key of source element)
├── expectedValue (string - value to compare against)
└── operator (EQUALS, NOT_EQUALS, IS_EMPTY, IS_NOT_EMPTY)
``` ```
**Dynamic Addition**: Users can add new form elements to any subsection at runtime via the API endpoint: **Dynamic Addition**: Users can add new form elements to any subsection at runtime via the API endpoint:
@@ -185,6 +192,83 @@ User roles in the system:
Roles are managed via Keycloak and enforced using Spring Security's `@PreAuthorize` annotations. Roles are managed via Keycloak and enforced using Spring Security's `@PreAuthorize` annotations.
### 10. Conditional Form Element Visibility
Form elements can be conditionally shown or hidden based on the values of other form elements. This enables dynamic forms that adapt to user input.
**Key Concepts**:
- **Visibility Conditions**: Rules attached to form elements that determine when they should be visible
- **Source Element**: The form element whose value is being evaluated
- **Target Element**: The form element with visibility conditions (the one being shown/hidden)
- **Condition Types**:
- `SHOW`: Element is visible only if the condition is met
- `HIDE`: Element is hidden if the condition is met
- **Operators**:
- `EQUALS`: Source value exactly matches expected value (case-insensitive)
- `NOT_EQUALS`: Source value does not match expected value
- `IS_EMPTY`: Source value is empty string
- `IS_NOT_EMPTY`: Source value is not empty
**Implementation Details**:
- Visibility conditions reference other form elements by their **reference key** (custom string identifier)
- Reference keys are stable and meaningful (e.g., "art_der_massnahme", "testphase_findet_statt")
- Each form element can have one visibility condition (single object, not array)
- Hidden fields are automatically excluded from validation
- PDF/HTML exports only include currently visible fields based on form state
- Versioning system captures visibility conditions in snapshots
- Conditions check the **value** property of form options, not labels
**Example Configuration**:
```json
{
"reference": "testphase_zeitraum",
"title": "Testphase Zeitraum",
"description": "Zeitraum der Testphase",
"type": "TEXTFIELD",
"options": [
{
"value": "",
"label": "Testphase Zeitraum",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
}
}
```
In this example, the "Testphase Zeitraum" field is only visible when the form element with reference `testphase_findet_statt` has the value "Ja" selected.
**Frontend Behavior**:
- Visibility is evaluated client-side using the `useFormElementVisibility` composable
- Form elements smoothly appear/disappear as conditions change
- Hidden elements are excluded from the form submission
**Backend Behavior**:
- Visibility is evaluated server-side when generating PDF/HTML exports
- Backend filters form elements before rendering to Thymeleaf templates
- Hidden elements are not included in exported documents
**Best Practices**:
- Avoid circular dependencies (A depends on B, B depends on A)
- Use meaningful reference keys (snake_case recommended, e.g., "art_der_massnahme", "testphase_findet_statt")
- Reference keys should be unique within a form
- Keep visibility logic simple for better user experience
- For radio buttons and selects, use the actual label text as the value
- For checkboxes, checked = "true", unchecked = "false"
- Test visibility conditions thoroughly before deployment
--- ---
## Project Structure ## Project Structure

View File

@@ -1372,6 +1372,9 @@ components:
id: id:
type: string type: string
format: uuid format: uuid
reference:
type: string
description: Unique reference key for this form element (e.g., "art_der_massnahme")
title: title:
type: string type: string
description: description:
@@ -1385,6 +1388,8 @@ components:
formElementSubSectionId: formElementSubSectionId:
type: string type: string
format: uuid format: uuid
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
FormElementSnapshotDto: FormElementSnapshotDto:
type: object type: object
@@ -1392,6 +1397,8 @@ components:
- type - type
- options - options
properties: properties:
reference:
type: string
title: title:
type: string type: string
description: description:
@@ -1402,6 +1409,8 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
CreateFormElementDto: CreateFormElementDto:
type: object type: object
@@ -1409,6 +1418,8 @@ components:
- options - options
- type - type
properties: properties:
reference:
type: string
title: title:
type: string type: string
description: description:
@@ -1419,6 +1430,8 @@ components:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
type: type:
$ref: "#/components/schemas/FormElementType" $ref: "#/components/schemas/FormElementType"
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
FormOptionDto: FormOptionDto:
type: object type: object
@@ -1447,6 +1460,39 @@ components:
- SWITCH - SWITCH
- TITLE_BODY_TEXTFIELDS - TITLE_BODY_TEXTFIELDS
FormElementVisibilityCondition:
type: object
required:
- conditionType
- sourceFormElementReference
- expectedValue
properties:
conditionType:
$ref: "#/components/schemas/VisibilityConditionType"
sourceFormElementReference:
type: string
description: Reference key of the source form element to check
expectedValue:
type: string
description: Expected value to compare against the source element's value property
operator:
$ref: "#/components/schemas/VisibilityConditionOperator"
default: EQUALS
VisibilityConditionType:
type: string
enum:
- SHOW
- HIDE
VisibilityConditionOperator:
type: string
enum:
- EQUALS
- NOT_EQUALS
- IS_EMPTY
- IS_NOT_EMPTY
####### UserDto ####### ####### UserDto #######
UserDto: UserDto:
type: object type: object

View File

@@ -1,10 +1,16 @@
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.form_element.FormElementSubSection
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionOperator
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionType
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.thymeleaf.TemplateEngine import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context import org.thymeleaf.context.Context
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID
@Service @Service
class ApplicationFormFormatService( class ApplicationFormFormatService(
@@ -24,10 +30,118 @@ class ApplicationFormFormatService(
} }
fun generateHtml(applicationForm: ApplicationForm): String { fun generateHtml(applicationForm: ApplicationForm): String {
val filteredForm = filterVisibleElements(applicationForm)
val context = val context =
Context().apply { Context().apply {
setVariable("applicationForm", applicationForm) setVariable("applicationForm", filteredForm)
} }
return templateEngine.process("application_form_template", context) 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 @Id
@GeneratedValue @GeneratedValue
var id: UUID? = null, var id: UUID? = null,
var reference: String? = null,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
@ElementCollection @ElementCollection
@@ -26,4 +27,5 @@ class FormElement(
@ManyToOne @ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false) @JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null, var formElementSubSection: FormElementSubSection? = null,
var visibilityCondition: FormElementVisibilityCondition? = null,
) )

View File

@@ -7,10 +7,12 @@ import org.springframework.stereotype.Component
@Component @Component
class FormElementMapper( class FormElementMapper(
private val formOptionMapper: FormOptionMapper, private val formOptionMapper: FormOptionMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
) { ) {
fun toFormElementDto(formElement: FormElement): FormElementDto = fun toFormElementDto(formElement: FormElement): FormElementDto =
FormElementDto( FormElementDto(
id = formElement.id ?: throw IllegalStateException("FormElement ID must not be null!"), id = formElement.id ?: throw IllegalStateException("FormElement ID must not be null!"),
reference = formElement.reference,
title = formElement.title, title = formElement.title,
description = formElement.description, description = formElement.description,
options = formElement.options.map { formOptionMapper.toFormOptionDto(it) }, options = formElement.options.map { formOptionMapper.toFormOptionDto(it) },
@@ -18,6 +20,10 @@ class FormElementMapper(
formElementSubSectionId = formElementSubSectionId =
formElement.formElementSubSection?.id formElement.formElementSubSection?.id
?: throw IllegalStateException("FormElementSubSection ID must not be null!"), ?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
visibilityCondition =
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
},
) )
fun toFormElement( fun toFormElement(
@@ -26,11 +32,16 @@ class FormElementMapper(
): FormElement = ): FormElement =
FormElement( FormElement(
id = formElement.id, id = formElement.id,
reference = formElement.reference,
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,
formElementSubSection = formElementSubSection, formElementSubSection = formElementSubSection,
visibilityCondition =
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it)
},
) )
fun toFormElement( fun toFormElement(
@@ -39,10 +50,15 @@ class FormElementMapper(
): FormElement = ): FormElement =
FormElement( FormElement(
id = null, id = null,
reference = formElement.reference,
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,
formElementSubSection = formElementSubSection, 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 create table form_element
( (
form_element_order integer, form_element_order integer,
type smallint not null check (type between 0 and 5), type smallint not null check (type between 0 and 5),
form_element_sub_section_id uuid not null, form_element_sub_section_id uuid not null,
id uuid not null, id uuid not null,
description varchar(255), condition_type varchar(255) check (condition_type in ('SHOW', 'HIDE')),
title varchar(255), 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) primary key (id)
); );
@@ -165,4 +170,4 @@ alter table if exists form_element_sub_section
alter table if exists notification alter table if exists notification
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
foreign key (recipient_id) foreign key (recipient_id)
references app_user references app_user;

View File

@@ -1,5 +1,5 @@
<template> <template>
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id"> <template v-for="(formElement, index) in visibleFormElements" :key="formElement.id">
<div class="group flex py-3 lg:py-4"> <div class="group flex py-3 lg:py-4">
<div class="flex-auto"> <div class="flex-auto">
<p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p> <p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p>
@@ -8,7 +8,7 @@
:is="getResolvedComponent(formElement)" :is="getResolvedComponent(formElement)"
:form-options="formElement.options" :form-options="formElement.options"
:disabled="props.disabled" :disabled="props.disabled"
@update:form-options="updateFormOptions($event, index)" @update:form-options="updateFormOptions($event, formElement.id)"
/> />
<TheComment <TheComment
v-if="applicationFormId && activeFormElement === formElement.id" v-if="applicationFormId && activeFormElement === formElement.id"
@@ -32,7 +32,7 @@
</UDropdownMenu> </UDropdownMenu>
</div> </div>
</div> </div>
<USeparator v-if="index < props.modelValue.length - 1" /> <USeparator v-if="index < visibleFormElements.length - 1" />
</template> </template>
</template> </template>
@@ -45,6 +45,7 @@ import { useCommentStore } from '~~/stores/useCommentStore'
const props = defineProps<{ const props = defineProps<{
modelValue: FormElementDto[] modelValue: FormElementDto[]
visibilityMap: Map<string, boolean>
applicationFormId?: string applicationFormId?: string
disabled?: boolean disabled?: boolean
}>() }>()
@@ -68,6 +69,10 @@ const route = useRoute()
const activeFormElement = ref('') const activeFormElement = ref('')
const openDropdownId = ref<string | null>(null) const openDropdownId = ref<string | null>(null)
const visibleFormElements = computed(() => {
return props.modelValue.filter((element) => props.visibilityMap.get(element.id) !== false)
})
function handleDropdownToggle(formElementId: string, isOpen: boolean) { function handleDropdownToggle(formElementId: string, isOpen: boolean) {
openDropdownId.value = isOpen ? formElementId : null openDropdownId.value = isOpen ? formElementId : null
} }
@@ -112,14 +117,15 @@ function getDropdownItems(formElementId: string, formElementPosition: number): D
return [items] return [items]
} }
function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) { function updateFormOptions(formOptions: FormOptionDto[], formElementId: string) {
console.log('Updating form options for element index:', formElementIndex, formOptions) console.log('Updating form options for element ID:', formElementId, formOptions)
const updatedModelValue = [...props.modelValue] const updatedModelValue = props.modelValue.map((element) => {
const currentElement = updatedModelValue[formElementIndex] if (element.id === formElementId) {
if (currentElement) { return { ...element, options: formOptions }
updatedModelValue[formElementIndex] = { ...currentElement, options: formOptions } }
emit('update:modelValue', updatedModelValue) return element
} })
emit('update:modelValue', updatedModelValue)
} }
function toggleComments(formElementId: string) { function toggleComments(formElementId: string) {

View File

@@ -8,25 +8,31 @@
{{ currentFormElementSection.title }} {{ currentFormElementSection.title }}
</h1> </h1>
<template v-if="currentFormElementSection?.formElementSubSections"> <UCard
<UCard v-for="subsection in visibleSubsections"
v-for="subsection in currentFormElementSection.formElementSubSections" :key="subsection.id"
:key="subsection.id" variant="subtle"
variant="subtle" class="mb-6"
class="mb-6" >
> <div class="mb-4">
<div class="mb-4"> <h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2> <p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p> </div>
</div>
<FormEngine <FormEngine
v-model="subsection.formElements" v-model="subsection.formElements"
:visibility-map="visibilityMap"
:application-form-id="applicationFormId" :application-form-id="applicationFormId"
:disabled="disabled" :disabled="disabled"
@add:input-form="(position) => handleAddInputForm(position, subsection.id)" @add:input-form="(position) => handleAddInputForm(position, subsection.id)"
/> />
</UCard> </UCard>
</template>
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
<div class="text-center py-8 text-dimmed">
<UIcon name="i-lucide-eye-off" class="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>{{ $t('applicationForms.noVisibleElements') }}</p>
</div>
</UCard>
<div class="flex gap-2 justify-between"> <div class="flex gap-2 justify-between">
<UButton leading-icon="i-lucide-arrow-left" :disabled="!stepper?.hasPrev" @click="handleNavigate('backward')"> <UButton leading-icon="i-lucide-arrow-left" :disabled="!stepper?.hasPrev" @click="handleNavigate('backward')">
@@ -76,6 +82,30 @@ const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection
computed(() => props.formElementSections) computed(() => props.formElementSections)
) )
const allFormElements = computed(() => {
return props.formElementSections.flatMap(section =>
section.formElementSubSections?.flatMap(subsection =>
subsection.formElements
) ?? []
)
})
const { evaluateVisibility } = useFormElementVisibility()
const visibilityMap = computed(() => {
return evaluateVisibility(allFormElements.value)
})
const visibleSubsections = computed(() => {
if (!currentFormElementSection.value?.formElementSubSections) {
return []
}
return currentFormElementSection.value.formElementSubSections.filter(subsection => {
return subsection.formElements.some(element => visibilityMap.value.get(element.id) !== false)
})
})
onMounted(() => { onMounted(() => {
if (props.initialSectionIndex !== undefined) { if (props.initialSectionIndex !== undefined) {
activeStepperItemIndex.value = props.initialSectionIndex activeStepperItemIndex.value = props.initialSectionIndex

View File

@@ -13,10 +13,17 @@ export function useApplicationFormValidator() {
return Object.values(ComplianceStatus)[highestComplianceNumber] ?? ComplianceStatus.NonCritical return Object.values(ComplianceStatus)[highestComplianceNumber] ?? ComplianceStatus.NonCritical
} }
function validateFormElements(formElements: FormElementDto[]): Map<FormElementId, ComplianceStatus> { function validateFormElements(
formElements: FormElementDto[],
visibilityMap?: Map<string, boolean>
): Map<FormElementId, ComplianceStatus> {
formElementComplianceMap.value.clear() formElementComplianceMap.value.clear()
formElements.forEach((formElement) => { formElements.forEach((formElement) => {
if (visibilityMap && visibilityMap.get(formElement.id) === false) {
return
}
if (!complianceCheckableElementTypes.includes(formElement.type)) return if (!complianceCheckableElementTypes.includes(formElement.type)) return
// Reset any previously set compliance status when all options are false // Reset any previously set compliance status when all options are false

View File

@@ -0,0 +1,83 @@
import type { FormElementDto, VisibilityConditionOperator } from '~~/.api-client'
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client'
export function useFormElementVisibility() {
function evaluateVisibility(allFormElements: FormElementDto[]): Map<string, boolean> {
const formElementsByRef = buildFormElementsMap(allFormElements)
const visibilityMap = new Map<string, boolean>()
allFormElements.forEach((element) => {
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap)
visibilityMap.set(element.id, isVisible)
})
return visibilityMap
}
function buildFormElementsMap(formElements: FormElementDto[]): Map<string, FormElementDto> {
const map = new Map<string, FormElementDto>()
formElements.forEach((element) => {
if (element.reference) {
map.set(element.reference, element)
}
})
return map
}
function isElementVisible(
element: FormElementDto,
formElementsByRef: Map<string, FormElementDto>,
_visibilityMap: Map<string, boolean>
): boolean {
if (!element.visibilityCondition) {
return true
}
const condition = element.visibilityCondition
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference)
if (!sourceElement) {
return false
}
const sourceValue = getFormElementValue(sourceElement)
const operator = condition.operator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.expectedValue, operator)
return condition.conditionType === VCType.Show ? conditionMet : !conditionMet
}
function getFormElementValue(element: FormElementDto): string {
const selectedOption = element.options.find((option) => option.value === 'true')
return selectedOption?.label || ''
}
function evaluateCondition(
actualValue: string,
expectedValue: string,
operator: VisibilityConditionOperator
): boolean {
let result: boolean
switch (operator) {
case VCOperator.Equals:
result = actualValue.toLowerCase() === expectedValue.toLowerCase()
return result
case VCOperator.NotEquals:
result = actualValue.toLowerCase() !== expectedValue.toLowerCase()
return result
case VCOperator.IsEmpty:
result = actualValue === ''
return result
case VCOperator.IsNotEmpty:
result = actualValue !== ''
return result
default:
return false
}
}
return {
evaluateVisibility
}
}

View File

@@ -75,6 +75,7 @@ const {
const { updateApplicationForm: updateForm, submitApplicationForm } = useApplicationForm() const { updateApplicationForm: updateForm, submitApplicationForm } = useApplicationForm()
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator() const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
const { evaluateVisibility } = useFormElementVisibility()
const { canWriteApplicationForms } = usePermissions() const { canWriteApplicationForms } = usePermissions()
const userStore = useUserStore() const userStore = useUserStore()
const { user } = storeToRefs(userStore) const { user } = storeToRefs(userStore)
@@ -100,10 +101,14 @@ const allFormElements = computed(() => {
) )
}) })
const visibilityMap = computed(() => {
return evaluateVisibility(allFormElements.value)
})
watch( watch(
() => allFormElements.value, () => allFormElements.value,
(updatedFormElements: FormElementDto[]) => { (updatedFormElements: FormElementDto[]) => {
validationMap.value = validateFormElements(updatedFormElements) validationMap.value = validateFormElements(updatedFormElements, visibilityMap.value)
validationStatus.value = getHighestComplianceStatus() validationStatus.value = getHighestComplianceStatus()
}, },
{ deep: true } { deep: true }

View File

@@ -53,6 +53,7 @@ import { useUserStore } from '~~/stores/useUserStore'
const { getAllApplicationFormTemplates } = await useApplicationFormTemplate() const { getAllApplicationFormTemplates } = await useApplicationFormTemplate()
const { createApplicationForm, submitApplicationForm } = useApplicationForm() const { createApplicationForm, submitApplicationForm } = useApplicationForm()
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator() const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
const { evaluateVisibility } = useFormElementVisibility()
const { canWriteApplicationForms } = usePermissions() const { canWriteApplicationForms } = usePermissions()
const userStore = useUserStore() const userStore = useUserStore()
const { selectedOrganization } = storeToRefs(userStore) const { selectedOrganization } = storeToRefs(userStore)
@@ -87,10 +88,14 @@ const allFormElements = computed(() => {
) )
}) })
const visibilityMap = computed(() => {
return evaluateVisibility(allFormElements.value)
})
watch( watch(
() => allFormElements.value, () => allFormElements.value,
(updatedFormElements: FormElementDto[]) => { (updatedFormElements: FormElementDto[]) => {
validationMap.value = validateFormElements(updatedFormElements) validationMap.value = validateFormElements(updatedFormElements, visibilityMap.value)
validationStatus.value = getHighestComplianceStatus() validationStatus.value = getHighestComplianceStatus()
}, },
{ deep: true } { deep: true }

View File

@@ -6,6 +6,7 @@
"noPermission": "Keine Berechtigung", "noPermission": "Keine Berechtigung",
"noPermissionDescription": "Sie haben keine Berechtigung zum Erstellen von Anträgen.", "noPermissionDescription": "Sie haben keine Berechtigung zum Erstellen von Anträgen.",
"backToOverview": "Zurück zur Übersicht", "backToOverview": "Zurück zur Übersicht",
"noVisibleElements": "Alle Formularelemente in diesem Abschnitt sind ausgeblendet",
"deleteConfirm": "Möchten Sie wirklich den Mitbestimmungsantrag \"{name}\" löschen?", "deleteConfirm": "Möchten Sie wirklich den Mitbestimmungsantrag \"{name}\" löschen?",
"deleteTitle": "Mitbestimmungsantrag löschen", "deleteTitle": "Mitbestimmungsantrag löschen",
"lastEditedBy": "Zuletzt bearbeitet von", "lastEditedBy": "Zuletzt bearbeitet von",

View File

@@ -6,6 +6,7 @@
"noPermission": "No Permission", "noPermission": "No Permission",
"noPermissionDescription": "You do not have permission to create applications.", "noPermissionDescription": "You do not have permission to create applications.",
"backToOverview": "Back to Overview", "backToOverview": "Back to Overview",
"noVisibleElements": "All form elements in this section are hidden",
"deleteConfirm": "Do you really want to delete the co-determination application \"{name}\"?", "deleteConfirm": "Do you really want to delete the co-determination application \"{name}\"?",
"deleteTitle": "Delete Co-determination Application", "deleteTitle": "Delete Co-determination Application",
"lastEditedBy": "Last edited by", "lastEditedBy": "Last edited by",

View File

@@ -1,95 +1,661 @@
{ {
"isTemplate": true, "isTemplate": true,
"name": "", "name": "Name des IT-Systems",
"formElementSections": [ "formElementSections": [
{ {
"title": "Section 1", "title": "Angaben zum IT-System",
"shortTitle": "S1", "shortTitle": "IT-System",
"description": "First section of the form", "description": "Alle Angaben zum IT-System",
"formElements": [ "formElementSubSections": [
{ {
"title": "Zustimmung erforderlich", "title": "Art der Maßnahme",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", "subtitle": "",
"options": [ "formElements": [
{ {
"value": "false", "reference": "art_der_massnahme",
"label": "Zustimmen (schwerwiegend)", "title": "Art der IT-System Maßnahme",
"processingPurpose": "BUSINESS_PROCESS", "description": "Handelt es sich um eine Einführung, Änderung, Erweiterung oder Ablösung/Einstellung eines IT-Systems?",
"employeeDataCategory": "SENSITIVE" "options": [
{
"value": "Einführung",
"label": "Einführung",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Änderung IT-System",
"label": "Änderung IT-System",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Erweiterung",
"label": "Erweiterung",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Ablösung/Einstellung IT-System",
"label": "Ablösung/Einstellung IT-System",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "RADIOBUTTON"
} }
], ]
"type": "SWITCH"
}, },
{ {
"title": "Zustimmung erforderlich", "title": "Allgemeine Information",
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", "subtitle": "Grundlegende Informationen zum IT-System",
"options": [ "formElements": [
{ {
"value": "false", "reference": "einfuehrungszeitpunkt",
"label": "Zustimmen (keine Auswirkungen)", "title": "Einführungszeitpunkt",
"processingPurpose": "NONE", "description": "Geplanter Termin Going Live",
"employeeDataCategory": "NONE" "options": [
} {
], "value": "",
"type": "SWITCH" "label": "Einführungszeitpunkt",
}, "processingPurpose": "SYSTEM_OPERATION",
{ "employeeDataCategory": "NON_CRITICAL"
"title": "Zustimmung erforderlich", }
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", ],
"options": [ "type": "TEXTFIELD",
{ "visibilityCondition": {
"value": "false", "conditionType": "HIDE",
"label": "Zustimmen (Mittel)", "sourceFormElementReference": "art_der_massnahme",
"processingPurpose": "DATA_ANALYSIS", "expectedValue": "Ablösung/Einstellung IT-System",
"employeeDataCategory": "REVIEW_REQUIRED" "operator": "EQUALS"
} }
],
"type": "CHECKBOX"
}
]
},
{
"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", "reference": "testphase_findet_statt",
"label": "Ablehnen", "title": "Testphase findet statt",
"processingPurpose": "DATA_ANALYSIS", "description": "Findet eine Testphase statt?",
"employeeDataCategory": "REVIEW_REQUIRED" "options": [
} {
], "value": "Ja",
"type": "SELECT" "label": "Ja",
}, "processingPurpose": "SYSTEM_OPERATION",
{ "employeeDataCategory": "NON_CRITICAL"
"title": "Eine weitere Zustimmung erforderlich", },
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.", {
"options": [ "value": "Nein",
{ "label": "Nein",
"value": "false", "processingPurpose": "SYSTEM_OPERATION",
"label": "Zustimmen", "employeeDataCategory": "NON_CRITICAL"
"processingPurpose": "BUSINESS_PROCESS", }
"employeeDataCategory": "SENSITIVE" ],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
}, },
{ {
"value": "false", "reference": "testphase_zeitraum",
"label": "Ablehnen", "title": "Testphase Zeitraum",
"processingPurpose": "DATA_ANALYSIS", "description": "Zeitraum der Testphase (von ... bis)",
"employeeDataCategory": "REVIEW_REQUIRED" "options": [
{
"value": "",
"label": "Testphase Zeitraum",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
}
},
{
"reference": "verwendung_anonymisierter_daten",
"title": "Verwendung anonymisierter/fiktiver Daten",
"description": "Während der Testphase/n wird mit anonymisierten bzw. fiktiven Daten gearbeitet",
"options": [
{
"value": "Ja",
"label": "Ja",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Nein",
"label": "Nein",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
}
},
{
"reference": "art_der_mitarbeiterdaten",
"title": "Art der Mitarbeiterdaten (falls Echtdaten)",
"description": "Welche Art von Mitarbeiterdaten werden verwendet?",
"options": [
{
"value": "",
"label": "Art der Mitarbeiterdaten",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
}
},
{
"reference": "anzahl_betroffener_mitarbeiter",
"title": "Anzahl betroffener Mitarbeiter (Testphase)",
"description": "Wie viele Mitarbeiter sind von der Testphase betroffen?",
"options": [
{
"value": "",
"label": "Anzahl betroffener Mitarbeiter",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
}
},
{
"reference": "umfang_der_mitarbeiterdatenverarbeitung",
"title": "Umfang der Mitarbeiterdatenverarbeitung",
"description": "In welchem Umfang werden Mitarbeiterdaten verarbeitet?",
"options": [
{
"value": "",
"label": "Umfang der Verarbeitung",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
}
},
{
"reference": "abgeloestes_system",
"title": "Abgelöstes System",
"description": "Folgendes System wird mit Einführung o.g. IT-Systems abgelöst",
"options": [
{
"value": "",
"label": "Nicht zutreffend",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "CHECKBOX",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "name_des_abgeloesten_systems",
"title": "Name des abgelösten Systems",
"description": "Bezeichnung des abgelösten Systems",
"options": [
{
"value": "",
"label": "Name des abgelösten Systems",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "abgeloestes_system",
"expectedValue": "true",
"operator": "NOT_EQUALS"
}
},
{
"reference": "geaendertes_system",
"title": "Geändertes System",
"description": "Folgendes System wird mit Einführung o.g. IT-Systems geändert",
"options": [
{
"value": "",
"label": "Nicht zutreffend",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "CHECKBOX",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "name_des_geaenderten_systems",
"title": "Name des geänderten Systems",
"description": "Bezeichnung des geänderten Systems",
"options": [
{
"value": "",
"label": "Name des geänderten Systems",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "geaendertes_system",
"expectedValue": "true",
"operator": "NOT_EQUALS"
}
} }
], ]
"type": "RADIOBUTTON" },
{
"title": "Verantwortlicher und betroffene Betriebe / Betriebsteile",
"subtitle": "Informationen zu Verantwortlichen und betroffenen Bereichen",
"formElements": [
{
"reference": "verantwortlicher_fachbereich",
"title": "Verantwortlicher Fachbereich und Ansprechpartner",
"description": "Bitte geben Sie den verantwortlichen Fachbereich und Ansprechpartner an",
"options": [
{
"value": "",
"label": "Fachbereich und Ansprechpartner",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "betroffene_betriebe",
"title": "Betroffene Betriebe/Betriebsteile",
"description": "Für welche Betriebe/Betriebsteile wird das IT-System eingeführt?",
"options": [
{
"value": "",
"label": "Betroffene Betriebe/Betriebsteile",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "betroffene_bereiche",
"title": "Betroffene Bereiche/Abteilungen",
"description": "Für welche Bereiche bzw. Abteilungen soll das IT-System zum Einsatz kommen?",
"options": [
{
"value": "",
"label": "Bereiche/Abteilungen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
}
]
},
{
"title": "Angaben zum IT-System",
"subtitle": "Detaillierte Informationen zum IT-System",
"formElements": [
{
"reference": "systembeschreibung",
"title": "Systembeschreibung",
"description": "Beschreibung des IT-Systems",
"options": [
{
"value": "",
"label": "Systembeschreibung",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "zielsetzung",
"title": "Zielsetzung des Systemeinsatzes",
"description": "Zielsetzung und Zweck des IT-Systems",
"options": [
{
"value": "",
"label": "Zielsetzung",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "hersteller",
"title": "Hersteller/Anbieter",
"description": "Hersteller oder Anbieter des IT-Systems",
"options": [
{
"value": "",
"label": "Hersteller/Anbieter",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "speicherort",
"title": "Speicherort",
"description": "Wo werden die Daten gespeichert?",
"options": [
{
"value": "Server im Rechenzentrum",
"label": "Server im Rechenzentrum",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "REVIEW_REQUIRED"
},
{
"value": "Cloud",
"label": "Cloud",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "SENSITIVE"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "geraeteart",
"title": "Zugriff auf die Daten erfolgt über (Geräteart)",
"description": "Über welche Art von Gerät erfolgt der Zugriff?",
"options": [
{
"value": "stationäres Endgerät",
"label": "stationäres Endgerät",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "mobile App",
"label": "mobile App",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "REVIEW_REQUIRED"
},
{
"value": "Browser",
"label": "Browser",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "SELECT",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "geraetebesitz",
"title": "Zugriff auf die Daten erfolgt über (Gerätebesitz)",
"description": "Mit welchen Geräten wird auf die Daten zugegriffen?",
"options": [
{
"value": "dienstliche Endgeräte",
"label": "dienstliche Endgeräte",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "private Endgeräte",
"label": "private Endgeräte",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "SENSITIVE"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "modulbasiertes_system",
"title": "Modulbasiertes System",
"description": "Ist das IT-System modulbasiert?",
"options": [
{
"value": "Ja",
"label": "Ja",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Nein",
"label": "Nein",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "modul_1",
"title": "Modul 1",
"description": "Beschreibung des ersten Moduls",
"options": [
{
"value": "",
"label": "Modul 1",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "modulbasiertes_system",
"expectedValue": "Ja",
"operator": "EQUALS"
}
},
{
"reference": "modul_2",
"title": "Modul 2",
"description": "Beschreibung des zweiten Moduls",
"options": [
{
"value": "",
"label": "Modul 2",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "modulbasiertes_system",
"expectedValue": "Ja",
"operator": "EQUALS"
}
},
{
"reference": "modul_3",
"title": "Modul 3",
"description": "Beschreibung des dritten Moduls",
"options": [
{
"value": "",
"label": "Modul 3",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "modulbasiertes_system",
"expectedValue": "Ja",
"operator": "EQUALS"
}
},
{
"reference": "ki_einsatz",
"title": "Einsatz Künstlicher Intelligenz",
"description": "Kommt im IT-System Künstliche Intelligenz zum Einsatz?",
"options": [
{
"value": "Nein",
"label": "Nein",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Ja",
"label": "Ja",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "wirtschaftliche_auswirkungen",
"title": "Wirtschaftliche Auswirkungen",
"description": "Zu erwartende wirtschaftliche Auswirkungen auf das Unternehmen",
"options": [
{
"value": "Keine",
"label": "Keine",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Ja",
"label": "Ja",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
}
},
{
"reference": "beschreibung_wirtschaftliche_auswirkungen",
"title": "Beschreibung wirtschaftliche Auswirkungen",
"description": "Bitte beschreiben Sie die wirtschaftlichen Auswirkungen",
"options": [
{
"value": "",
"label": "Beschreibung der Auswirkungen",
"processingPurpose": "BUSINESS_PROCESS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
],
"type": "TEXTFIELD",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "wirtschaftliche_auswirkungen",
"expectedValue": "Ja",
"operator": "EQUALS"
}
}
]
} }
] ]
} }