feat(#13): Show form elements depending on other form element values
This commit is contained in:
92
CLAUDE.md
92
CLAUDE.md
@@ -97,12 +97,19 @@ Application Form
|
||||
└── SubSection (FormElementSubSection)
|
||||
├── title, subtitle
|
||||
└── 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)
|
||||
├── title, description
|
||||
└── options (FormOption[])
|
||||
├── value, label
|
||||
├── processingPurpose
|
||||
└── employeeDataCategory
|
||||
├── options (FormOption[])
|
||||
│ ├── value, label
|
||||
│ ├── processingPurpose
|
||||
│ └── 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:
|
||||
@@ -185,6 +192,83 @@ User roles in the system:
|
||||
|
||||
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
|
||||
|
||||
@@ -1372,6 +1372,9 @@ components:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
reference:
|
||||
type: string
|
||||
description: Unique reference key for this form element (e.g., "art_der_massnahme")
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
@@ -1385,6 +1388,8 @@ components:
|
||||
formElementSubSectionId:
|
||||
type: string
|
||||
format: uuid
|
||||
visibilityCondition:
|
||||
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
||||
|
||||
FormElementSnapshotDto:
|
||||
type: object
|
||||
@@ -1392,6 +1397,8 @@ components:
|
||||
- type
|
||||
- options
|
||||
properties:
|
||||
reference:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
@@ -1402,6 +1409,8 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/FormOptionDto"
|
||||
visibilityCondition:
|
||||
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
||||
|
||||
CreateFormElementDto:
|
||||
type: object
|
||||
@@ -1409,6 +1418,8 @@ components:
|
||||
- options
|
||||
- type
|
||||
properties:
|
||||
reference:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
@@ -1419,6 +1430,8 @@ components:
|
||||
$ref: "#/components/schemas/FormOptionDto"
|
||||
type:
|
||||
$ref: "#/components/schemas/FormElementType"
|
||||
visibilityCondition:
|
||||
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
||||
|
||||
FormOptionDto:
|
||||
type: object
|
||||
@@ -1447,6 +1460,39 @@ components:
|
||||
- SWITCH
|
||||
- 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:
|
||||
type: object
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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="flex-auto">
|
||||
<p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p>
|
||||
@@ -8,7 +8,7 @@
|
||||
:is="getResolvedComponent(formElement)"
|
||||
:form-options="formElement.options"
|
||||
:disabled="props.disabled"
|
||||
@update:form-options="updateFormOptions($event, index)"
|
||||
@update:form-options="updateFormOptions($event, formElement.id)"
|
||||
/>
|
||||
<TheComment
|
||||
v-if="applicationFormId && activeFormElement === formElement.id"
|
||||
@@ -32,7 +32,7 @@
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<USeparator v-if="index < props.modelValue.length - 1" />
|
||||
<USeparator v-if="index < visibleFormElements.length - 1" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { useCommentStore } from '~~/stores/useCommentStore'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: FormElementDto[]
|
||||
visibilityMap: Map<string, boolean>
|
||||
applicationFormId?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
@@ -68,6 +69,10 @@ const route = useRoute()
|
||||
const activeFormElement = ref('')
|
||||
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) {
|
||||
openDropdownId.value = isOpen ? formElementId : null
|
||||
}
|
||||
@@ -112,14 +117,15 @@ function getDropdownItems(formElementId: string, formElementPosition: number): D
|
||||
return [items]
|
||||
}
|
||||
|
||||
function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) {
|
||||
console.log('Updating form options for element index:', formElementIndex, formOptions)
|
||||
const updatedModelValue = [...props.modelValue]
|
||||
const currentElement = updatedModelValue[formElementIndex]
|
||||
if (currentElement) {
|
||||
updatedModelValue[formElementIndex] = { ...currentElement, options: formOptions }
|
||||
emit('update:modelValue', updatedModelValue)
|
||||
}
|
||||
function updateFormOptions(formOptions: FormOptionDto[], formElementId: string) {
|
||||
console.log('Updating form options for element ID:', formElementId, formOptions)
|
||||
const updatedModelValue = props.modelValue.map((element) => {
|
||||
if (element.id === formElementId) {
|
||||
return { ...element, options: formOptions }
|
||||
}
|
||||
return element
|
||||
})
|
||||
emit('update:modelValue', updatedModelValue)
|
||||
}
|
||||
|
||||
function toggleComments(formElementId: string) {
|
||||
|
||||
@@ -8,25 +8,31 @@
|
||||
{{ currentFormElementSection.title }}
|
||||
</h1>
|
||||
|
||||
<template v-if="currentFormElementSection?.formElementSubSections">
|
||||
<UCard
|
||||
v-for="subsection in currentFormElementSection.formElementSubSections"
|
||||
:key="subsection.id"
|
||||
variant="subtle"
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
|
||||
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
|
||||
</div>
|
||||
<UCard
|
||||
v-for="subsection in visibleSubsections"
|
||||
:key="subsection.id"
|
||||
variant="subtle"
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
|
||||
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
|
||||
</div>
|
||||
<FormEngine
|
||||
v-model="subsection.formElements"
|
||||
:visibility-map="visibilityMap"
|
||||
:application-form-id="applicationFormId"
|
||||
:disabled="disabled"
|
||||
@add:input-form="(position) => handleAddInputForm(position, subsection.id)"
|
||||
/>
|
||||
</UCard>
|
||||
</template>
|
||||
</UCard>
|
||||
|
||||
<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">
|
||||
<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)
|
||||
)
|
||||
|
||||
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(() => {
|
||||
if (props.initialSectionIndex !== undefined) {
|
||||
activeStepperItemIndex.value = props.initialSectionIndex
|
||||
|
||||
@@ -13,10 +13,17 @@ export function useApplicationFormValidator() {
|
||||
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()
|
||||
|
||||
formElements.forEach((formElement) => {
|
||||
if (visibilityMap && visibilityMap.get(formElement.id) === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!complianceCheckableElementTypes.includes(formElement.type)) return
|
||||
|
||||
// Reset any previously set compliance status when all options are false
|
||||
|
||||
83
legalconsenthub/app/composables/useFormElementVisibility.ts
Normal file
83
legalconsenthub/app/composables/useFormElementVisibility.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ const {
|
||||
|
||||
const { updateApplicationForm: updateForm, submitApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { evaluateVisibility } = useFormElementVisibility()
|
||||
const { canWriteApplicationForms } = usePermissions()
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
@@ -100,10 +101,14 @@ const allFormElements = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const visibilityMap = computed(() => {
|
||||
return evaluateVisibility(allFormElements.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => allFormElements.value,
|
||||
(updatedFormElements: FormElementDto[]) => {
|
||||
validationMap.value = validateFormElements(updatedFormElements)
|
||||
validationMap.value = validateFormElements(updatedFormElements, visibilityMap.value)
|
||||
validationStatus.value = getHighestComplianceStatus()
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
@@ -53,6 +53,7 @@ import { useUserStore } from '~~/stores/useUserStore'
|
||||
const { getAllApplicationFormTemplates } = await useApplicationFormTemplate()
|
||||
const { createApplicationForm, submitApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { evaluateVisibility } = useFormElementVisibility()
|
||||
const { canWriteApplicationForms } = usePermissions()
|
||||
const userStore = useUserStore()
|
||||
const { selectedOrganization } = storeToRefs(userStore)
|
||||
@@ -87,10 +88,14 @@ const allFormElements = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const visibilityMap = computed(() => {
|
||||
return evaluateVisibility(allFormElements.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => allFormElements.value,
|
||||
(updatedFormElements: FormElementDto[]) => {
|
||||
validationMap.value = validateFormElements(updatedFormElements)
|
||||
validationMap.value = validateFormElements(updatedFormElements, visibilityMap.value)
|
||||
validationStatus.value = getHighestComplianceStatus()
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"noPermission": "Keine Berechtigung",
|
||||
"noPermissionDescription": "Sie haben keine Berechtigung zum Erstellen von Anträgen.",
|
||||
"backToOverview": "Zurück zur Übersicht",
|
||||
"noVisibleElements": "Alle Formularelemente in diesem Abschnitt sind ausgeblendet",
|
||||
"deleteConfirm": "Möchten Sie wirklich den Mitbestimmungsantrag \"{name}\" löschen?",
|
||||
"deleteTitle": "Mitbestimmungsantrag löschen",
|
||||
"lastEditedBy": "Zuletzt bearbeitet von",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"noPermission": "No Permission",
|
||||
"noPermissionDescription": "You do not have permission to create applications.",
|
||||
"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}\"?",
|
||||
"deleteTitle": "Delete Co-determination Application",
|
||||
"lastEditedBy": "Last edited by",
|
||||
|
||||
720
testdata.json
720
testdata.json
@@ -1,95 +1,661 @@
|
||||
{
|
||||
"isTemplate": true,
|
||||
"name": "",
|
||||
"name": "Name des IT-Systems",
|
||||
"formElementSections": [
|
||||
{
|
||||
"title": "Section 1",
|
||||
"shortTitle": "S1",
|
||||
"description": "First section of the form",
|
||||
"formElements": [
|
||||
"title": "Angaben zum IT-System",
|
||||
"shortTitle": "IT-System",
|
||||
"description": "Alle Angaben zum IT-System",
|
||||
"formElementSubSections": [
|
||||
{
|
||||
"title": "Zustimmung erforderlich",
|
||||
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||
"options": [
|
||||
"title": "Art der Maßnahme",
|
||||
"subtitle": "",
|
||||
"formElements": [
|
||||
{
|
||||
"value": "false",
|
||||
"label": "Zustimmen (schwerwiegend)",
|
||||
"processingPurpose": "BUSINESS_PROCESS",
|
||||
"employeeDataCategory": "SENSITIVE"
|
||||
"reference": "art_der_massnahme",
|
||||
"title": "Art der IT-System Maßnahme",
|
||||
"description": "Handelt es sich um eine Einführung, Änderung, Erweiterung oder Ablösung/Einstellung eines IT-Systems?",
|
||||
"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",
|
||||
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||
"options": [
|
||||
"title": "Allgemeine Information",
|
||||
"subtitle": "Grundlegende Informationen zum IT-System",
|
||||
"formElements": [
|
||||
{
|
||||
"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": "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"
|
||||
"reference": "einfuehrungszeitpunkt",
|
||||
"title": "Einführungszeitpunkt",
|
||||
"description": "Geplanter Termin Going Live",
|
||||
"options": [
|
||||
{
|
||||
"value": "",
|
||||
"label": "Einführungszeitpunkt",
|
||||
"processingPurpose": "SYSTEM_OPERATION",
|
||||
"employeeDataCategory": "NON_CRITICAL"
|
||||
}
|
||||
],
|
||||
"type": "TEXTFIELD",
|
||||
"visibilityCondition": {
|
||||
"conditionType": "HIDE",
|
||||
"sourceFormElementReference": "art_der_massnahme",
|
||||
"expectedValue": "Ablösung/Einstellung IT-System",
|
||||
"operator": "EQUALS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"reference": "testphase_findet_statt",
|
||||
"title": "Testphase findet statt",
|
||||
"description": "Findet eine Testphase statt?",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"value": "false",
|
||||
"label": "Ablehnen",
|
||||
"processingPurpose": "DATA_ANALYSIS",
|
||||
"employeeDataCategory": "REVIEW_REQUIRED"
|
||||
"reference": "testphase_zeitraum",
|
||||
"title": "Testphase Zeitraum",
|
||||
"description": "Zeitraum der Testphase (von ... bis)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user