major(fullstack): Add dynamic section spawning, removal of app. form create DTOs,

This commit is contained in:
2025-12-15 19:12:00 +01:00
parent 7bacff967e
commit 844ab8661c
47 changed files with 1283 additions and 511 deletions

115
CLAUDE.md
View File

@@ -106,10 +106,10 @@ Application Form
│ ├── processingPurpose
│ └── employeeDataCategory
└── visibilityCondition (FormElementVisibilityCondition)
├── conditionType (SHOW, HIDE)
├── formElementConditionType (SHOW, HIDE)
├── sourceFormElementReference (string - reference key of source element)
├── expectedValue (string - value to compare against)
└── operator (EQUALS, NOT_EQUALS, IS_EMPTY, IS_NOT_EMPTY)
├── formElementExpectedValue (string - value to compare against)
└── formElementOperator (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:
@@ -237,10 +237,10 @@ Form elements can be conditionally shown or hidden based on the values of other
}
],
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
"formElementExpectedValue": "Ja",
"formElementOperator": "EQUALS"
}
}
```
@@ -269,6 +269,106 @@ In this example, the "Testphase Zeitraum" field is only visible when the form el
- For checkboxes, checked = "true", unchecked = "false"
- Test visibility conditions thoroughly before deployment
### 11. Dynamic Section Spawning
Form elements can trigger the dynamic creation of full sections based on user input. This enables complex workflows where additional form sections appear when certain conditions are met.
**Key Concepts**:
- **Section Templates**: Pre-defined section blueprints marked with `isTemplate: true`
- **Spawn Triggers**: Rules on form elements that define when to spawn a section template
- **Clonable Elements**: Form elements that can be duplicated by users (e.g., adding multiple modules)
- **Title Interpolation**: Section titles can include placeholders like `{{triggerValue}}`
**Section Template Properties**:
- `isTemplate: boolean` - If true, section is a template (excluded from display/PDF)
- `templateReference: string` - Unique identifier for the template
- `titleTemplate: string` - Title with placeholder (e.g., "Modul: {{triggerValue}}")
- `spawnedFromElementReference: string` - Links spawned instance to trigger element
**Form Element Properties for Spawning**:
- `sectionSpawnTrigger`: Defines spawn conditions
- `templateReference`: Which template to spawn
- `sectionSpawnConditionType`: SHOW or HIDE
- `sectionSpawnExpectedValue`: Value to trigger spawning
- `sectionSpawnOperator`: EQUALS, NOT_EQUALS, IS_EMPTY, IS_NOT_EMPTY
- `isClonable: boolean` - Shows "Add another" button when true
**Example - Clonable Module with Dynamic Section**:
```json
{
"reference": "modul_1",
"title": "Modulname",
"type": "TEXTAREA",
"isClonable": true,
"sectionSpawnTrigger": {
"templateReference": "module_details_template",
"sectionSpawnConditionType": "SHOW",
"sectionSpawnOperator": "IS_NOT_EMPTY"
}
}
```
**Example - Template Section**:
```json
{
"title": "Moduldetails",
"isTemplate": true,
"templateReference": "module_details_template",
"titleTemplate": "Modul: {{triggerValue}}",
"formElementSubSections": [
{
"title": "Modulinformationen",
"formElements": [...]
}
]
}
```
**Client-Side Spawning Flow**:
1. User fills trigger element (e.g., types "SAP Finance")
2. Frontend clones template into CreateFormElementSectionDto (no ID)
3. Title interpolated: "Modul: SAP Finance"
4. User clicks "Add another" → new element clone created
5. User saves → backend generates IDs
**Element Cloning Implementation**:
- **Deep Cloning**: Elements are deep-cloned using `JSON.parse(JSON.stringify())` to avoid shared references between cloned elements
- **Reference Generation**: Cloned elements get auto-generated references (e.g., `modul_1``modul_2``modul_3`)
- **Value Reset**: TEXTAREA and TEXTFIELD elements have their values reset to empty string when cloned
- **ID Removal**: Cloned elements don't have IDs (they're `CreateFormElementDto`), IDs are generated by the backend on save
- **Independent Instances**: Each cloned element is completely independent - updating one doesn't affect others
**Spawn Trigger Evaluation**:
- Spawn triggers are evaluated when form element values change
- Each element with a unique reference can spawn its own section independently
- Multiple spawns are handled sequentially using `resultSections` to ensure correct state
- Spawned sections are linked to their trigger element via `spawnedFromElementReference`
**Element Update Matching**:
- Form element updates use unique identifiers (`id` or `reference`) instead of array indices
- This prevents cross-element updates when elements are filtered by visibility
- Matching priority: `id` (if present) → `reference` (fallback)
**Backend Behavior**:
- Template sections (`isTemplate=true`) are filtered out from PDF/HTML exports
- Spawned sections are included in exports with their interpolated titles
- Versioning captures all template and spawn trigger fields
**Frontend Composables**:
- `useSectionSpawning`: Handles spawn trigger evaluation and template cloning
- `useClonableElements`: Handles element cloning with reference generation and deep cloning
---
## Project Structure
@@ -303,9 +403,12 @@ legalconsenthub/
│ │ ├── complianceMap.ts
│ │ ├── useApplicationFormNavigation.ts
│ │ ├── useApplicationFormValidator.ts
│ │ ├── useClonableElements.ts
│ │ ├── useFormElementManagement.ts
│ │ ├── useFormElementVisibility.ts
│ │ ├── useFormStepper.ts
│ │ ├── usePermissions.ts
│ │ ├── useSectionSpawning.ts
│ │ └── useServerHealth.ts
│ ├── layouts/ # Layout components
│ │ ├── auth.vue

View File

@@ -46,7 +46,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateApplicationFormDto"
$ref: "#/components/schemas/ApplicationFormDto"
responses:
"201":
description: Successfully created application form
@@ -256,7 +256,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateFormElementDto"
$ref: "#/components/schemas/FormElementDto"
responses:
"201":
description: Form element successfully added
@@ -406,7 +406,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateApplicationFormDto"
$ref: "#/components/schemas/ApplicationFormDto"
responses:
"201":
description: Successfully created application form template
@@ -1088,63 +1088,44 @@ components:
ApplicationFormDto:
type: object
required:
- id
- name
- formElementSections
- isTemplate
- organizationId
- createdBy
- lastModifiedBy
- createdAt
- modifiedAt
- status
properties:
id:
type: string
format: uuid
nullable: true
name:
type: string
formElementSections:
type: array
items:
$ref: "#/components/schemas/FormElementSectionDto"
isTemplate:
type: boolean
organizationId:
type: string
createdBy:
$ref: "#/components/schemas/UserDto"
lastModifiedBy:
$ref: "#/components/schemas/UserDto"
createdAt:
type: string
format: date-time
modifiedAt:
type: string
format: date-time
status:
$ref: "#/components/schemas/ApplicationFormStatus"
CreateApplicationFormDto:
required:
- name
- formElementSections
- isTemplate
type: object
properties:
name:
type: string
formElementSections:
type: array
items:
$ref: "#/components/schemas/CreateFormElementSectionDto"
isTemplate:
type: boolean
default: false
organizationId:
type: string
nullable: true
createdBy:
$ref: "#/components/schemas/UserDto"
nullable: true
lastModifiedBy:
$ref: "#/components/schemas/UserDto"
nullable: true
createdAt:
type: string
format: date-time
nullable: true
modifiedAt:
type: string
format: date-time
nullable: true
status:
$ref: "#/components/schemas/ApplicationFormStatus"
allOf:
- $ref: "#/components/schemas/ApplicationFormStatus"
nullable: true
PagedApplicationFormDto:
type: object
@@ -1249,14 +1230,13 @@ components:
FormElementSectionDto:
type: object
required:
- id
- title
- formElementSubSections
- applicationFormId
properties:
id:
type: string
format: uuid
nullable: true
title:
type: string
shortTitle:
@@ -1270,6 +1250,20 @@ components:
applicationFormId:
type: string
format: uuid
nullable: true
isTemplate:
type: boolean
default: false
description: If true, this section is a template for spawning
templateReference:
type: string
description: Unique reference key for this template section
titleTemplate:
type: string
description: Title template with placeholder (e.g., "Modul{{triggerValue}}")
spawnedFromElementReference:
type: string
description: Reference of the form element that triggered this section
FormElementSectionSnapshotDto:
type: object
@@ -1287,38 +1281,26 @@ components:
type: array
items:
$ref: "#/components/schemas/FormElementSubSectionSnapshotDto"
CreateFormElementSectionDto:
type: object
required:
- title
- formElementSubSections
properties:
id:
isTemplate:
type: boolean
default: false
templateReference:
type: string
format: uuid
title:
titleTemplate:
type: string
shortTitle:
spawnedFromElementReference:
type: string
description:
type: string
formElementSubSections:
type: array
items:
$ref: "#/components/schemas/CreateFormElementSubSectionDto"
FormElementSubSectionDto:
type: object
required:
- id
- title
- formElements
- formElementSectionId
properties:
id:
type: string
format: uuid
nullable: true
title:
type: string
subtitle:
@@ -1330,6 +1312,7 @@ components:
formElementSectionId:
type: string
format: uuid
nullable: true
FormElementSubSectionSnapshotDto:
type: object
@@ -1346,32 +1329,16 @@ components:
items:
$ref: "#/components/schemas/FormElementSnapshotDto"
CreateFormElementSubSectionDto:
type: object
required:
- title
- formElements
properties:
title:
type: string
subtitle:
type: string
formElements:
type: array
items:
$ref: "#/components/schemas/CreateFormElementDto"
FormElementDto:
type: object
required:
- id
- options
- type
- formElementSubSectionId
properties:
id:
type: string
format: uuid
nullable: true
reference:
type: string
description: Unique reference key for this form element (e.g., "art_der_massnahme")
@@ -1388,8 +1355,15 @@ components:
formElementSubSectionId:
type: string
format: uuid
nullable: true
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
sectionSpawnTrigger:
$ref: "#/components/schemas/SectionSpawnTriggerDto"
isClonable:
type: boolean
default: false
description: If true, user can add more instances of this element
FormElementSnapshotDto:
type: object
@@ -1411,27 +1385,11 @@ components:
$ref: "#/components/schemas/FormOptionDto"
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
CreateFormElementDto:
type: object
required:
- options
- type
properties:
reference:
type: string
title:
type: string
description:
type: string
options:
type: array
items:
$ref: "#/components/schemas/FormOptionDto"
type:
$ref: "#/components/schemas/FormElementType"
visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition"
sectionSpawnTrigger:
$ref: "#/components/schemas/SectionSpawnTriggerDto"
isClonable:
type: boolean
default: false
FormOptionDto:
type: object
@@ -1465,19 +1423,19 @@ components:
FormElementVisibilityCondition:
type: object
required:
- conditionType
- formElementConditionType
- sourceFormElementReference
- expectedValue
- formElementExpectedValue
properties:
conditionType:
formElementConditionType:
$ref: "#/components/schemas/VisibilityConditionType"
sourceFormElementReference:
type: string
description: Reference key of the source form element to check
expectedValue:
formElementExpectedValue:
type: string
description: Expected value to compare against the source element's value property
operator:
formElementOperator:
$ref: "#/components/schemas/VisibilityConditionOperator"
default: EQUALS
@@ -1495,6 +1453,24 @@ components:
- IS_EMPTY
- IS_NOT_EMPTY
SectionSpawnTriggerDto:
type: object
required:
- templateReference
- sectionSpawnConditionType
- sectionSpawnOperator
properties:
templateReference:
type: string
description: Reference key of the section template to spawn
sectionSpawnConditionType:
$ref: "#/components/schemas/VisibilityConditionType"
sectionSpawnExpectedValue:
type: string
description: Expected value to trigger spawning
sectionSpawnOperator:
$ref: "#/components/schemas/VisibilityConditionOperator"
####### UserDto #######
UserDto:
type: object

View File

@@ -2,8 +2,7 @@ package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource
@@ -24,13 +23,11 @@ class ApplicationFormController(
@PreAuthorize(
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
)
override fun createApplicationForm(
createApplicationFormDto: CreateApplicationFormDto,
): ResponseEntity<ApplicationFormDto> {
val updatedCreateApplicationFormDto = createApplicationFormDto.copy(isTemplate = false)
override fun createApplicationForm(applicationFormDto: ApplicationFormDto): ResponseEntity<ApplicationFormDto> {
val updatedApplicationFormDto = applicationFormDto.copy(isTemplate = false)
return ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto(
applicationFormService.createApplicationForm(updatedCreateApplicationFormDto),
applicationFormService.createApplicationForm(updatedApplicationFormDto),
),
)
}
@@ -88,7 +85,7 @@ class ApplicationFormController(
): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto(
applicationFormService.updateApplicationForm(applicationFormDto),
applicationFormService.updateApplicationForm(id, applicationFormDto),
),
)
@@ -117,14 +114,14 @@ class ApplicationFormController(
applicationFormId: UUID,
subsectionId: UUID,
position: Int,
createFormElementDto: CreateFormElementDto,
formElementDto: FormElementDto,
): ResponseEntity<ApplicationFormDto> =
ResponseEntity.status(201).body(
applicationFormMapper.toApplicationFormDto(
applicationFormService.addFormElementToSubSection(
applicationFormId,
subsectionId,
createFormElementDto,
formElementDto,
position,
),
),

View File

@@ -43,38 +43,45 @@ class ApplicationFormFormatService(
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
applicationForm.formElementSections
.filter { !it.isTemplate }
.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 (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,
isTemplate = section.isTemplate,
templateReference = section.templateReference,
titleTemplate = section.titleTemplate,
spawnedFromElementReference = section.spawnedFromElementReference,
formElementSubSections = filteredSubSections.toMutableList(),
applicationForm = 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,
@@ -122,9 +129,10 @@ class ApplicationFormFormatService(
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
val sourceValue = getFormElementValue(sourceElement)
val conditionMet = evaluateCondition(sourceValue, condition.expectedValue, condition.operator)
val conditionMet =
evaluateCondition(sourceValue, condition.formElementExpectedValue, condition.formElementOperator)
return when (condition.conditionType) {
return when (condition.formElementConditionType) {
VisibilityConditionType.SHOW -> conditionMet
VisibilityConditionType.HIDE -> !conditionMet
}

View File

@@ -4,9 +4,9 @@ import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSectionMap
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
import com.betriebsratkanzlei.legalconsenthub.user.UserService
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.util.UUID
@Component
class ApplicationFormMapper(
@@ -16,7 +16,7 @@ class ApplicationFormMapper(
) {
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto =
ApplicationFormDto(
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
id = applicationForm.id,
name = applicationForm.name,
formElementSections =
applicationForm.formElementSections.map {
@@ -34,45 +34,53 @@ class ApplicationFormMapper(
status = applicationForm.status,
)
fun toApplicationForm(applicationForm: ApplicationFormDto): ApplicationForm {
val form =
ApplicationForm(
id = applicationForm.id,
name = applicationForm.name,
isTemplate = applicationForm.isTemplate,
organizationId = applicationForm.organizationId,
status = applicationForm.status,
createdBy = userMapper.toUser(applicationForm.createdBy),
lastModifiedBy = userMapper.toUser(applicationForm.lastModifiedBy),
createdAt = applicationForm.createdAt,
modifiedAt = applicationForm.modifiedAt,
)
form.formElementSections =
applicationForm.formElementSections
.map {
formElementSectionMapper.toFormElementSection(it, form)
}.toMutableList()
return form
}
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
fun toNewApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
val currentUser = userService.getCurrentUser()
val applicationForm =
ApplicationForm(
name = createApplicationFormDto.name,
isTemplate = createApplicationFormDto.isTemplate,
organizationId = createApplicationFormDto.organizationId ?: "",
id = null,
name = applicationFormDto.name,
isTemplate = applicationFormDto.isTemplate,
organizationId = applicationFormDto.organizationId ?: "",
status =
createApplicationFormDto.status
applicationFormDto.status
?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT,
createdBy = currentUser,
lastModifiedBy = currentUser,
)
applicationForm.formElementSections =
createApplicationFormDto.formElementSections
.map { formElementSectionMapper.toFormElementSection(it, applicationForm) }
applicationFormDto.formElementSections
.map { formElementSectionMapper.toNewFormElementSection(it, applicationForm) }
.toMutableList()
return applicationForm
}
fun toUpdatedApplicationForm(
id: UUID,
applicationFormDto: ApplicationFormDto,
existingApplicationForm: ApplicationForm,
): ApplicationForm {
val currentUser = userService.getCurrentUser()
val form =
ApplicationForm(
id = id,
name = applicationFormDto.name,
isTemplate = applicationFormDto.isTemplate,
organizationId = applicationFormDto.organizationId ?: existingApplicationForm.organizationId,
status = applicationFormDto.status ?: existingApplicationForm.status,
createdBy = existingApplicationForm.createdBy,
lastModifiedBy = currentUser,
createdAt = existingApplicationForm.createdAt,
modifiedAt = existingApplicationForm.modifiedAt,
)
form.formElementSections =
applicationFormDto.formElementSections
.map { formElementSectionMapper.toFormElementSection(it, form) }
.toMutableList()
return form
}
}

View File

@@ -13,9 +13,8 @@ import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService
import com.betriebsratkanzlei.legalconsenthub.user.UserService
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Page
@@ -33,8 +32,8 @@ class ApplicationFormService(
private val userService: UserService,
private val eventPublisher: ApplicationEventPublisher,
) {
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto)
fun createApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toNewApplicationForm(applicationFormDto)
val savedApplicationForm: ApplicationForm
try {
savedApplicationForm = applicationFormRepository.save(applicationForm)
@@ -67,17 +66,26 @@ class ApplicationFormService(
return applicationFormRepository.findAllByIsTemplateFalseAndOrganizationId(organizationId, pageable)
}
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
val existingApplicationForm = getApplicationFormById(applicationFormDto.id)
fun updateApplicationForm(
id: UUID,
applicationFormDto: ApplicationFormDto,
): ApplicationForm {
println("Updating ApplicationForm: $applicationFormDto")
val existingApplicationForm = getApplicationFormById(id)
val existingSnapshot = versionService.createSnapshot(existingApplicationForm)
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
val applicationForm =
applicationFormMapper.toUpdatedApplicationForm(
id,
applicationFormDto,
existingApplicationForm,
)
val updatedApplicationForm: ApplicationForm
try {
updatedApplicationForm = applicationFormRepository.save(applicationForm)
} catch (e: Exception) {
throw ApplicationFormNotUpdatedException(e, applicationFormDto.id)
throw ApplicationFormNotUpdatedException(e, id)
}
val currentUser = userService.getCurrentUser()
@@ -160,7 +168,7 @@ class ApplicationFormService(
fun addFormElementToSubSection(
applicationFormId: UUID,
subsectionId: UUID,
createFormElementDto: CreateFormElementDto,
formElementDto: FormElementDto,
position: Int,
): ApplicationForm {
val applicationForm = getApplicationFormById(applicationFormId)
@@ -171,7 +179,7 @@ class ApplicationFormService(
.find { it.id == subsectionId }
?: throw IllegalArgumentException("FormElementSubSection with id $subsectionId not found")
val newFormElement = formElementMapper.toFormElement(createFormElementDto, subsection)
val newFormElement = formElementMapper.toNewFormElement(formElementDto, subsection)
if (position >= 0 && position < subsection.formElements.size) {
subsection.formElements.add(position, newFormElement)

View File

@@ -4,7 +4,6 @@ import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormMa
import com.betriebsratkanzlei.legalconsenthub.application_form.PagedApplicationFormMapper
import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormTemplateApi
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
@@ -21,11 +20,13 @@ class ApplicationFormTemplateController(
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
)
override fun createApplicationFormTemplate(
createApplicationFormDto: CreateApplicationFormDto,
applicationFormDto: ApplicationFormDto,
): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto(
applicationFormTemplateService.createApplicationFormTemplate(createApplicationFormDto),
applicationFormTemplateService.createApplicationFormTemplate(
applicationFormDto.copy(isTemplate = true),
),
),
)
@@ -58,7 +59,7 @@ class ApplicationFormTemplateController(
): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto(
applicationFormTemplateService.updateApplicationFormTemplate(applicationFormDto),
applicationFormTemplateService.updateApplicationFormTemplate(id, applicationFormDto),
),
)

View File

@@ -8,7 +8,6 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedExc
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
@@ -19,8 +18,8 @@ class ApplicationFormTemplateService(
private val applicationFormRepository: ApplicationFormRepository,
private val applicationFormMapper: ApplicationFormMapper,
) {
fun createApplicationFormTemplate(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto)
fun createApplicationFormTemplate(applicationFormDto: ApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toNewApplicationForm(applicationFormDto)
val savedApplicationForm: ApplicationForm
try {
savedApplicationForm = applicationFormRepository.save(applicationForm)
@@ -41,14 +40,23 @@ class ApplicationFormTemplateService(
return applicationFormRepository.findAllByIsTemplateTrue(pageable)
}
fun updateApplicationFormTemplate(applicationFormDto: ApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto)
fun updateApplicationFormTemplate(
id: UUID,
applicationFormDto: ApplicationFormDto,
): ApplicationForm {
val existingApplicationForm = getApplicationFormTemplateById(id)
val applicationForm =
applicationFormMapper.toUpdatedApplicationForm(
id,
applicationFormDto,
existingApplicationForm,
)
val updatedApplicationForm: ApplicationForm
try {
updatedApplicationForm = applicationFormRepository.save(applicationForm)
} catch (e: Exception) {
throw ApplicationFormNotUpdatedException(e, applicationFormDto.id)
throw ApplicationFormNotUpdatedException(e, id)
}
return updatedApplicationForm

View File

@@ -8,6 +8,7 @@ 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.FormOption
import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper
import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
@@ -24,6 +25,7 @@ class ApplicationFormVersionService(
private val versionRepository: ApplicationFormVersionRepository,
private val applicationFormRepository: ApplicationFormRepository,
private val objectMapper: ObjectMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
) {
@Transactional
fun createVersion(
@@ -103,6 +105,10 @@ class ApplicationFormVersionService(
title = section.title,
shortTitle = section.shortTitle,
description = section.description,
isTemplate = section.isTemplate,
templateReference = section.templateReference,
titleTemplate = section.titleTemplate,
spawnedFromElementReference = section.spawnedFromElementReference,
subsections =
section.formElementSubSections.map { subsection ->
FormElementSubSectionSnapshotDto(
@@ -111,6 +117,7 @@ class ApplicationFormVersionService(
elements =
subsection.formElements.map { element ->
FormElementSnapshotDto(
reference = element.reference,
title = element.title,
description = element.description,
type = element.type,
@@ -123,6 +130,11 @@ class ApplicationFormVersionService(
employeeDataCategory = option.employeeDataCategory,
)
},
sectionSpawnTrigger =
element.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
},
isClonable = element.isClonable,
)
},
)
@@ -140,6 +152,10 @@ class ApplicationFormVersionService(
title = sectionSnapshot.title,
shortTitle = sectionSnapshot.shortTitle,
description = sectionSnapshot.description,
isTemplate = sectionSnapshot.isTemplate ?: false,
templateReference = sectionSnapshot.templateReference,
titleTemplate = sectionSnapshot.titleTemplate,
spawnedFromElementReference = sectionSnapshot.spawnedFromElementReference,
applicationForm = applicationForm,
)
@@ -154,6 +170,7 @@ class ApplicationFormVersionService(
subsectionSnapshot.elements.forEach { elementSnapshot ->
val element =
FormElement(
reference = elementSnapshot.reference,
title = elementSnapshot.title,
description = elementSnapshot.description,
type = elementSnapshot.type,
@@ -168,6 +185,11 @@ class ApplicationFormVersionService(
employeeDataCategory = optionDto.employeeDataCategory,
)
}.toMutableList(),
sectionSpawnTrigger =
elementSnapshot.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = elementSnapshot.isClonable ?: false,
)
subsection.formElements.add(element)
}

View File

@@ -4,6 +4,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
import jakarta.persistence.CollectionTable
import jakarta.persistence.Column
import jakarta.persistence.ElementCollection
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
@@ -27,5 +28,9 @@ class FormElement(
@ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null,
@Embedded
var visibilityCondition: FormElementVisibilityCondition? = null,
@Embedded
var sectionSpawnTrigger: SectionSpawnTrigger? = null,
var isClonable: Boolean = false,
)

View File

@@ -1,6 +1,5 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import org.springframework.stereotype.Component
@@ -8,10 +7,11 @@ import org.springframework.stereotype.Component
class FormElementMapper(
private val formOptionMapper: FormOptionMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
) {
fun toFormElementDto(formElement: FormElement): FormElementDto =
FormElementDto(
id = formElement.id ?: throw IllegalStateException("FormElement ID must not be null!"),
id = formElement.id,
reference = formElement.reference,
title = formElement.title,
description = formElement.description,
@@ -24,6 +24,11 @@ class FormElementMapper(
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
},
sectionSpawnTrigger =
formElement.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
},
isClonable = formElement.isClonable,
)
fun toFormElement(
@@ -42,10 +47,15 @@ class FormElementMapper(
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it)
},
sectionSpawnTrigger =
formElement.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = formElement.isClonable ?: false,
)
fun toFormElement(
formElement: CreateFormElementDto,
fun toNewFormElement(
formElement: FormElementDto,
formElementSubSection: FormElementSubSection,
): FormElement =
FormElement(
@@ -60,5 +70,10 @@ class FormElementMapper(
formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it)
},
sectionSpawnTrigger =
formElement.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = formElement.isClonable ?: false,
)
}

View File

@@ -21,6 +21,10 @@ class FormElementSection(
var title: String,
var shortTitle: String? = null,
var description: String? = null,
var isTemplate: Boolean = false,
var templateReference: String? = null,
var titleTemplate: String? = null,
var spawnedFromElementReference: String? = null,
@OneToMany(mappedBy = "formElementSection", cascade = [CascadeType.ALL], orphanRemoval = true)
@OrderColumn(name = "form_element_sub_section_order")
var formElementSubSections: MutableList<FormElementSubSection> = mutableListOf(),

View File

@@ -1,7 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementSectionDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionDto
import org.springframework.stereotype.Component
@@ -11,7 +10,7 @@ class FormElementSectionMapper(
) {
fun toFormElementSectionDto(formElementSection: FormElementSection): FormElementSectionDto =
FormElementSectionDto(
id = formElementSection.id ?: throw IllegalStateException("FormElementSection ID must not be null!"),
id = formElementSection.id,
title = formElementSection.title,
description = formElementSection.description,
shortTitle = formElementSection.shortTitle,
@@ -22,6 +21,10 @@ class FormElementSectionMapper(
applicationFormId =
formElementSection.applicationForm?.id
?: throw IllegalStateException("ApplicationForm ID must not be null!"),
isTemplate = formElementSection.isTemplate,
templateReference = formElementSection.templateReference,
titleTemplate = formElementSection.titleTemplate,
spawnedFromElementReference = formElementSection.spawnedFromElementReference,
)
fun toFormElementSection(
@@ -34,6 +37,10 @@ class FormElementSectionMapper(
title = formElementSection.title,
description = formElementSection.description,
shortTitle = formElementSection.shortTitle,
isTemplate = formElementSection.isTemplate ?: false,
templateReference = formElementSection.templateReference,
titleTemplate = formElementSection.titleTemplate,
spawnedFromElementReference = formElementSection.spawnedFromElementReference,
applicationForm = applicationForm,
)
section.formElementSubSections =
@@ -43,21 +50,26 @@ class FormElementSectionMapper(
return section
}
fun toFormElementSection(
createFormElementSection: CreateFormElementSectionDto,
fun toNewFormElementSection(
formElementSection: FormElementSectionDto,
applicationForm: ApplicationForm,
): FormElementSection {
val formElementSection =
val section =
FormElementSection(
title = createFormElementSection.title,
description = createFormElementSection.description,
shortTitle = createFormElementSection.shortTitle,
id = null,
title = formElementSection.title,
description = formElementSection.description,
shortTitle = formElementSection.shortTitle,
isTemplate = formElementSection.isTemplate ?: false,
templateReference = formElementSection.templateReference,
titleTemplate = formElementSection.titleTemplate,
spawnedFromElementReference = formElementSection.spawnedFromElementReference,
applicationForm = applicationForm,
)
formElementSection.formElementSubSections =
createFormElementSection.formElementSubSections
.map { formElementSubSectionMapper.toFormElementSubSection(it, formElementSection) }
section.formElementSubSections =
formElementSection.formElementSubSections
.map { formElementSubSectionMapper.toNewFormElementSubSection(it, section) }
.toMutableList()
return formElementSection
return section
}
}

View File

@@ -1,6 +1,5 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementSubSectionDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionDto
import org.springframework.stereotype.Component
@@ -10,7 +9,7 @@ class FormElementSubSectionMapper(
) {
fun toFormElementSubSectionDto(formElementSubSection: FormElementSubSection): FormElementSubSectionDto =
FormElementSubSectionDto(
id = formElementSubSection.id ?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
id = formElementSubSection.id,
title = formElementSubSection.title,
subtitle = formElementSubSection.subtitle,
formElements = formElementSubSection.formElements.map { formElementMapper.toFormElementDto(it) },
@@ -37,20 +36,21 @@ class FormElementSubSectionMapper(
return subsection
}
fun toFormElementSubSection(
createFormElementSubSection: CreateFormElementSubSectionDto,
fun toNewFormElementSubSection(
formElementSubSection: FormElementSubSectionDto,
formElementSection: FormElementSection,
): FormElementSubSection {
val formElementSubSection =
val subsection =
FormElementSubSection(
title = createFormElementSubSection.title,
subtitle = createFormElementSubSection.subtitle,
id = null,
title = formElementSubSection.title,
subtitle = formElementSubSection.subtitle,
formElementSection = formElementSection,
)
formElementSubSection.formElements =
createFormElementSubSection.formElements
.map { formElementMapper.toFormElement(it, formElementSubSection) }
subsection.formElements =
formElementSubSection.formElements
.map { formElementMapper.toNewFormElement(it, subsection) }
.toMutableList()
return formElementSubSection
return subsection
}
}

View File

@@ -7,9 +7,9 @@ import jakarta.persistence.Enumerated
@Embeddable
data class FormElementVisibilityCondition(
@Enumerated(EnumType.STRING)
val conditionType: VisibilityConditionType,
val formElementConditionType: VisibilityConditionType,
val sourceFormElementReference: String,
val expectedValue: String,
val formElementExpectedValue: String,
@Enumerated(EnumType.STRING)
val operator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
val formElementOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
)

View File

@@ -11,21 +11,21 @@ class FormElementVisibilityConditionMapper {
condition: FormElementVisibilityCondition,
): FormElementVisibilityConditionDto =
FormElementVisibilityConditionDto(
conditionType = toVisibilityConditionTypeDto(condition.conditionType),
formElementConditionType = toVisibilityConditionTypeDto(condition.formElementConditionType),
sourceFormElementReference = condition.sourceFormElementReference,
expectedValue = condition.expectedValue,
operator = toVisibilityConditionOperatorDto(condition.operator),
formElementExpectedValue = condition.formElementExpectedValue,
formElementOperator = toVisibilityConditionOperatorDto(condition.formElementOperator),
)
fun toFormElementVisibilityCondition(
conditionDto: FormElementVisibilityConditionDto,
): FormElementVisibilityCondition =
FormElementVisibilityCondition(
conditionType = toVisibilityConditionType(conditionDto.conditionType),
formElementConditionType = toVisibilityConditionType(conditionDto.formElementConditionType),
sourceFormElementReference = conditionDto.sourceFormElementReference,
expectedValue = conditionDto.expectedValue,
operator =
conditionDto.operator?.let { toVisibilityConditionOperator(it) }
formElementExpectedValue = conditionDto.formElementExpectedValue,
formElementOperator =
conditionDto.formElementOperator?.let { toVisibilityConditionOperator(it) }
?: VisibilityConditionOperator.EQUALS,
)

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 SectionSpawnTrigger(
val templateReference: String,
@Enumerated(EnumType.STRING)
val sectionSpawnConditionType: VisibilityConditionType,
val sectionSpawnExpectedValue: String? = null,
@Enumerated(EnumType.STRING)
val sectionSpawnOperator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
)

View File

@@ -0,0 +1,57 @@
package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.SectionSpawnTriggerDto
import org.springframework.stereotype.Component
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto
@Component
class SectionSpawnTriggerMapper {
fun toSectionSpawnTriggerDto(trigger: SectionSpawnTrigger): SectionSpawnTriggerDto =
SectionSpawnTriggerDto(
templateReference = trigger.templateReference,
sectionSpawnConditionType = toVisibilityConditionTypeDto(trigger.sectionSpawnConditionType),
sectionSpawnExpectedValue = trigger.sectionSpawnExpectedValue,
sectionSpawnOperator = toVisibilityConditionOperatorDto(trigger.sectionSpawnOperator),
)
fun toSectionSpawnTrigger(triggerDto: SectionSpawnTriggerDto): SectionSpawnTrigger =
SectionSpawnTrigger(
templateReference = triggerDto.templateReference,
sectionSpawnConditionType = toVisibilityConditionType(triggerDto.sectionSpawnConditionType),
sectionSpawnExpectedValue = triggerDto.sectionSpawnExpectedValue,
sectionSpawnOperator = toVisibilityConditionOperator(triggerDto.sectionSpawnOperator),
)
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

@@ -45,7 +45,7 @@ logging:
springframework:
security: TRACE
oauth2: TRACE
web: DEBUG
web: TRACE
org.testcontainers: INFO
com.github.dockerjava: WARN

View File

@@ -63,26 +63,37 @@ create table form_element_options
create table form_element
(
form_element_order integer,
type smallint not null check (type between 0 and 6),
is_clonable boolean not null,
type smallint not null check (type between 0 and 7),
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')),
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')),
form_element_expected_value varchar(255),
form_element_operator varchar(255) check (form_element_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
reference varchar(255),
section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')),
section_spawn_expected_value varchar(255),
section_spawn_operator varchar(255) check (section_spawn_operator in
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
source_form_element_reference varchar(255),
template_reference varchar(255),
title varchar(255),
primary key (id)
);
create table form_element_section
(
application_form_id uuid not null,
id uuid not null,
description varchar(255),
short_title varchar(255),
title varchar(255) not null,
is_template boolean not null,
application_form_id uuid not null,
id uuid not null,
description varchar(255),
short_title varchar(255),
spawned_from_element_reference varchar(255),
template_reference varchar(255),
title varchar(255) not null,
title_template varchar(255),
primary key (id)
);

View File

@@ -4,3 +4,4 @@ coverage
.nuxt
.output
.api-client
pnpm-lock.yaml

View File

@@ -5,7 +5,7 @@
</template>
<template #footer>
<UButton :label="$t('common.cancel')" color="neutral" variant="outline" @click="$emit('update:isOpen', false)" />
<UButton :label="$t('common.delete')" color="neutral" @click="$emit('delete', applicationFormToDelete.id)" />
<UButton :label="$t('common.delete')" color="neutral" :disabled="!applicationFormToDelete.id" @click="onDelete" />
</template>
</UModal>
</template>
@@ -13,13 +13,20 @@
<script setup lang="ts">
import type { ApplicationFormDto } from '~~/.api-client'
defineEmits<{
const emit = defineEmits<{
(e: 'delete', id: string): void
(e: 'update:isOpen', value: boolean): void
}>()
defineProps<{
const props = defineProps<{
applicationFormToDelete: ApplicationFormDto
isOpen: boolean
}>()
function onDelete() {
if (!props.applicationFormToDelete.id) {
return
}
emit('delete', props.applicationFormToDelete.id)
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<template v-for="(formElement, index) in visibleFormElements" :key="formElement.id">
<template v-for="(formElement, index) in visibleFormElements" :key="getElementKey(formElement, index)">
<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,10 +8,20 @@
:is="getResolvedComponent(formElement)"
:form-options="formElement.options"
:disabled="props.disabled"
@update:form-options="updateFormOptions($event, formElement.id)"
@update:form-options="updateFormOptions($event, index)"
/>
<div v-if="formElement.isClonable && !props.disabled" class="mt-3">
<UButton
variant="outline"
size="sm"
leading-icon="i-lucide-copy-plus"
@click="handleCloneElement(formElement, index)"
>
{{ $t('applicationForms.formElements.addAnother') }}
</UButton>
</div>
<TheComment
v-if="applicationFormId && activeFormElement === formElement.id"
v-if="applicationFormId && formElement.id && activeFormElement === formElement.id"
:form-element-id="formElement.id"
:application-form-id="applicationFormId"
:comments="comments?.[formElement.id]"
@@ -20,13 +30,13 @@
<div
:class="[
'transition-opacity duration-200',
openDropdownId === formElement.id ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
openDropdownId === getElementKey(formElement, index) ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
]"
>
<UDropdownMenu
:items="getDropdownItems(formElement.id, index)"
:items="getDropdownItems(getElementKey(formElement, index), index)"
:content="{ align: 'end' }"
@update:open="(isOpen) => handleDropdownToggle(formElement.id, isOpen)"
@update:open="(isOpen) => handleDropdownToggle(getElementKey(formElement, index), isOpen)"
>
<UButton icon="i-lucide-ellipsis-vertical" color="neutral" variant="ghost" />
</UDropdownMenu>
@@ -54,6 +64,7 @@ const emit = defineEmits<{
(e: 'update:modelValue', formElementDto: FormElementDto[]): void
(e: 'click:comments', formElementId: string): void
(e: 'add:input-form', position: number): void
(e: 'clone:element', element: FormElementDto, position: number): void
}>()
const commentStore = useCommentStore()
@@ -69,8 +80,15 @@ const route = useRoute()
const activeFormElement = ref('')
const openDropdownId = ref<string | null>(null)
function getElementKey(element: FormElementDto, index: number): string {
return element.id || element.reference || `element-${index}`
}
const visibleFormElements = computed(() => {
return props.modelValue.filter((element) => props.visibilityMap.get(element.id) !== false)
return props.modelValue.filter((element) => {
const key = element.id || element.reference
return key ? props.visibilityMap.get(key) !== false : true
})
})
function handleDropdownToggle(formElementId: string, isOpen: boolean) {
@@ -121,10 +139,17 @@ function getDropdownItems(formElementId: string, formElementPosition: number): D
return [items]
}
function updateFormOptions(formOptions: FormOptionDto[], formElementId: string) {
console.log('Updating form options for element ID:', formElementId, formOptions)
function updateFormOptions(formOptions: FormOptionDto[], formElementIndex: number) {
const targetElement = visibleFormElements.value[formElementIndex]
if (!targetElement) {
return
}
const updatedModelValue = props.modelValue.map((element) => {
if (element.id === formElementId) {
if (targetElement.id && element.id === targetElement.id) {
return { ...element, options: formOptions }
}
if (targetElement.reference && element.reference === targetElement.reference) {
return { ...element, options: formOptions }
}
return element
@@ -140,4 +165,8 @@ function toggleComments(formElementId: string) {
activeFormElement.value = formElementId
emit('click:comments', formElementId)
}
function handleCloneElement(formElement: FormElementDto, position: number) {
emit('clone:element', formElement, position)
}
</script>

View File

@@ -9,8 +9,8 @@
</h1>
<UCard
v-for="subsection in visibleSubsections"
:key="subsection.id"
v-for="{ subsection, sectionIndex } in visibleSubsections"
:key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)"
variant="subtle"
class="mb-6"
>
@@ -18,13 +18,24 @@
<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)"
/>
<FormEngine
:model-value="subsection.formElements"
:visibility-map="visibilityMap"
:application-form-id="applicationFormId"
:disabled="disabled"
@update:model-value="
(elements) =>
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
"
@add:input-form="
(position) =>
handleAddInputForm(position, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
"
@clone:element="
(element, position) =>
handleCloneElement(element, position, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
"
/>
</UCard>
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
@@ -62,7 +73,12 @@
</template>
<script setup lang="ts">
import type { ApplicationFormDto, FormElementSectionDto } from '~~/.api-client'
import type {
ApplicationFormDto,
FormElementSectionDto,
FormElementDto,
FormElementSubSectionDto
} from '~~/.api-client'
const props = defineProps<{
formElementSections: FormElementSectionDto[]
@@ -75,6 +91,7 @@ const emit = defineEmits<{
save: []
submit: []
'add-input-form': [updatedForm: ApplicationFormDto | undefined]
'update:formElementSections': [sections: FormElementSectionDto[]]
navigate: [{ direction: 'forward' | 'backward'; index: number }]
}>()
@@ -82,16 +99,16 @@ const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection
computed(() => props.formElementSections)
)
const { evaluateVisibility } = useFormElementVisibility()
const { processSpawnTriggers } = useSectionSpawning()
const { cloneElement } = useClonableElements()
const allFormElements = computed(() => {
return props.formElementSections.flatMap(section =>
section.formElementSubSections?.flatMap(subsection =>
subsection.formElements
) ?? []
return props.formElementSections.flatMap(
(section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? []
)
})
const { evaluateVisibility } = useFormElementVisibility()
const visibilityMap = computed(() => {
return evaluateVisibility(allFormElements.value)
})
@@ -100,10 +117,20 @@ 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)
})
return currentFormElementSection.value.formElementSubSections
.map((subsection) => ({ subsection, sectionIndex: currentSectionIndex.value }))
.filter(({ subsection }) => {
return subsection.formElements.some((element) => {
const key = element.id || element.reference
return key ? visibilityMap.value.get(key) !== false : true
})
})
})
const currentSectionIndex = computed(() => {
if (!currentFormElementSection.value) return -1
return props.formElementSections.indexOf(currentFormElementSection.value)
})
onMounted(() => {
@@ -112,27 +139,90 @@ onMounted(() => {
}
})
async function handleAddInputForm(position: number, subsectionId: string) {
const subsection = props.formElementSections
.flatMap((section) => section.formElementSubSections)
.find((sub) => sub.id === subsectionId)
async function handleAddInputForm(position: number, subsectionKey: string) {
const foundSubsection = findSubsectionByKey(subsectionKey)
if (!foundSubsection) return
if (!subsection) {
return
const { addInputFormElement } = useFormElementManagement()
const updatedElements = addInputFormElement(foundSubsection.formElements, position)
const updatedSections = updateSubsectionElements(props.formElementSections, subsectionKey, updatedElements)
emit('update:formElementSections', updatedSections)
}
function findSubsectionByKey(subsectionKey: string): FormElementSubSectionDto | undefined {
for (let sectionIdx = 0; sectionIdx < props.formElementSections.length; sectionIdx++) {
const section = props.formElementSections[sectionIdx]
if (!section) continue
for (let i = 0; i < section.formElementSubSections.length; i++) {
const subsection = section.formElementSubSections[i]
if (subsection && getSubsectionKey(section, sectionIdx, subsection) === subsectionKey) {
return subsection
}
}
}
const { addFormElementToSubSection } = useFormElementManagement()
const updatedForm = await addFormElementToSubSection(
props.applicationFormId,
subsectionId,
subsection.formElements,
position
)
emit('add-input-form', updatedForm)
return undefined
}
async function handleNavigate(direction: 'forward' | 'backward') {
await navigateStepper(direction)
emit('navigate', { direction, index: activeStepperItemIndex.value })
}
function handleCloneElement(element: FormElementDto, position: number, subsectionKey: string) {
const clonedElement = cloneElement(element, allFormElements.value)
const updatedSections = props.formElementSections.map((section, sectionIdx) => ({
...section,
formElementSubSections: section.formElementSubSections.map((subsection) => {
if (getSubsectionKey(section, sectionIdx, subsection) === subsectionKey) {
const newFormElements = [...subsection.formElements]
newFormElements.splice(position + 1, 0, clonedElement as FormElementDto)
return { ...subsection, formElements: newFormElements }
}
return subsection
})
}))
emit('update:formElementSections', updatedSections)
}
function handleFormElementUpdate(updatedFormElements: FormElementDto[], subsectionKey: string) {
let updatedSections = updateSubsectionElements(props.formElementSections, subsectionKey, updatedFormElements)
updatedSections = processSpawnTriggers(updatedSections, updatedFormElements)
emit('update:formElementSections', updatedSections)
}
function getSubsectionKey(
section: FormElementSectionDto | undefined,
sectionIndex: number,
subsection: FormElementSubSectionDto
): string {
if (subsection.id) {
return subsection.id
}
const spawnedFrom = section?.spawnedFromElementReference ?? 'root'
const templateRef = section?.templateReference ?? 'no_tmpl'
const title = subsection.title ?? ''
return `spawned:${spawnedFrom}|tmpl:${templateRef}|sec:${sectionIndex}|title:${title}`
}
function updateSubsectionElements(
sections: FormElementSectionDto[],
subsectionKey: string,
updatedFormElements: FormElementDto[]
): FormElementSectionDto[] {
return sections.map((section, sectionIdx) => ({
...section,
formElementSubSections: section.formElementSubSections.map((subsection) => {
if (getSubsectionKey(section, sectionIdx, subsection) === subsectionKey) {
return { ...subsection, formElements: updatedFormElements }
}
return subsection
})
}))
}
</script>

View File

@@ -51,9 +51,9 @@ const dateValue = computed({
const firstOption = props.formOptions[0]
if (firstOption) {
const updatedModelValue = [...props.formOptions]
updatedModelValue[0] = {
...firstOption,
value: val ? val.toString() : ''
updatedModelValue[0] = {
...firstOption,
value: val ? val.toString() : ''
}
emit('update:formOptions', updatedModelValue)
}

View File

@@ -28,4 +28,3 @@ const modelValue = computed({
}
})
</script>

View File

@@ -1,19 +1,12 @@
import type {
CreateApplicationFormDto,
CreateFormElementDto,
ApplicationFormDto,
PagedApplicationFormDto
} from '~~/.api-client'
import type { ApplicationFormDto, PagedApplicationFormDto, FormElementDto } from '~~/.api-client'
import { useApplicationFormApi } from './useApplicationFormApi'
export function useApplicationForm() {
const applicationFormApi = useApplicationFormApi()
async function createApplicationForm(
createApplicationFormDto: CreateApplicationFormDto
): Promise<ApplicationFormDto> {
async function createApplicationForm(applicationFormDto: ApplicationFormDto): Promise<ApplicationFormDto> {
try {
return await applicationFormApi.createApplicationForm(createApplicationFormDto)
return await applicationFormApi.createApplicationForm(applicationFormDto)
} catch (e: unknown) {
console.error('Failed creating application form:', e)
return Promise.reject(e)
@@ -46,6 +39,7 @@ export function useApplicationForm() {
return Promise.reject(new Error('ID or application form DTO missing'))
}
console.log('Updating application form with ID:', id, applicationFormDto)
try {
return await applicationFormApi.updateApplicationForm(id, applicationFormDto)
} catch (e: unknown) {
@@ -79,7 +73,7 @@ export function useApplicationForm() {
async function addFormElementToSubSection(
applicationFormId: string,
subsectionId: string,
createFormElementDto: CreateFormElementDto,
formElementDto: FormElementDto,
position: number
): Promise<ApplicationFormDto> {
if (!applicationFormId || !subsectionId) {
@@ -90,7 +84,7 @@ export function useApplicationForm() {
return await applicationFormApi.addFormElementToSubSection(
applicationFormId,
subsectionId,
createFormElementDto,
formElementDto,
position
)
} catch (e: unknown) {

View File

@@ -1,10 +1,9 @@
import {
ApplicationFormApi,
Configuration,
type CreateApplicationFormDto,
type CreateFormElementDto,
type ApplicationFormDto,
type PagedApplicationFormDto
type PagedApplicationFormDto,
type FormElementDto
} from '~~/.api-client'
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
import { wrappedFetchWrap } from '~/utils/wrappedFetch'
@@ -25,10 +24,8 @@ export function useApplicationFormApi() {
new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) })
)
async function createApplicationForm(
createApplicationFormDto: CreateApplicationFormDto
): Promise<ApplicationFormDto> {
return applicationFormApiClient.createApplicationForm({ createApplicationFormDto })
async function createApplicationForm(applicationFormDto: ApplicationFormDto): Promise<ApplicationFormDto> {
return applicationFormApiClient.createApplicationForm({ applicationFormDto })
}
async function getAllApplicationForms(organizationId: string): Promise<PagedApplicationFormDto> {
@@ -57,13 +54,13 @@ export function useApplicationFormApi() {
async function addFormElementToSubSection(
applicationFormId: string,
subsectionId: string,
createFormElementDto: CreateFormElementDto,
formElementDto: FormElementDto,
position: number
): Promise<ApplicationFormDto> {
return applicationFormApiClient.addFormElementToSubSection({
applicationFormId,
subsectionId,
createFormElementDto,
formElementDto,
position
})
}

View File

@@ -1,9 +1,4 @@
import {
type CreateApplicationFormDto,
type ApplicationFormDto,
type PagedApplicationFormDto,
ResponseError
} from '~~/.api-client'
import { type ApplicationFormDto, type PagedApplicationFormDto, ResponseError } from '~~/.api-client'
import { useApplicationFormTemplateApi } from './useApplicationFormTemplateApi'
const currentApplicationForm: Ref<ApplicationFormDto | undefined> = ref()
@@ -11,11 +6,9 @@ const currentApplicationForm: Ref<ApplicationFormDto | undefined> = ref()
export async function useApplicationFormTemplate() {
const applicationFormApi = await useApplicationFormTemplateApi()
async function createApplicationFormTemplate(
createApplicationFormDto: CreateApplicationFormDto
): Promise<ApplicationFormDto> {
async function createApplicationFormTemplate(applicationFormDto: ApplicationFormDto): Promise<ApplicationFormDto> {
try {
currentApplicationForm.value = await applicationFormApi.createApplicationFormTemplate(createApplicationFormDto)
currentApplicationForm.value = await applicationFormApi.createApplicationFormTemplate(applicationFormDto)
return currentApplicationForm.value
} catch (e: unknown) {
if (e instanceof ResponseError) {

View File

@@ -1,5 +1,5 @@
import { ApplicationFormTemplateApi, Configuration } from '../../../.api-client'
import type { CreateApplicationFormDto, ApplicationFormDto, PagedApplicationFormDto } from '~~/.api-client'
import type { ApplicationFormDto, PagedApplicationFormDto } from '~~/.api-client'
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
import { wrappedFetchWrap } from '~/utils/wrappedFetch'
@@ -19,10 +19,8 @@ export async function useApplicationFormTemplateApi() {
new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) })
)
async function createApplicationFormTemplate(
createApplicationFormDto: CreateApplicationFormDto
): Promise<ApplicationFormDto> {
return applicationFormApiClient.createApplicationFormTemplate({ createApplicationFormDto })
async function createApplicationFormTemplate(applicationFormDto: ApplicationFormDto): Promise<ApplicationFormDto> {
return applicationFormApiClient.createApplicationFormTemplate({ applicationFormDto })
}
async function getAllApplicationFormTemplates(): Promise<PagedApplicationFormDto> {

View File

@@ -7,3 +7,5 @@ export { useNotification } from './notification/useNotification'
export { useNotificationApi } from './notification/useNotificationApi'
export { useUser } from './user/useUser'
export { useUserApi } from './user/useUserApi'
export { useSectionSpawning } from './useSectionSpawning'
export { useClonableElements } from './useClonableElements'

View File

@@ -19,8 +19,10 @@ export function useApplicationFormValidator() {
): Map<FormElementId, ComplianceStatus> {
formElementComplianceMap.value.clear()
formElements.forEach((formElement) => {
if (visibilityMap && visibilityMap.get(formElement.id) === false) {
formElements.forEach((formElement, index) => {
const elementKey = formElement.id || formElement.reference || `element-${index}`
if (visibilityMap && visibilityMap.get(elementKey) === false) {
return
}
@@ -49,15 +51,15 @@ export function useApplicationFormValidator() {
const currentHighestComplianceStatusPos =
Object.values(ComplianceStatus).indexOf(currentHighestComplianceStatus)
if (formElementComplianceMap.value.has(formElement.id)) {
const newComplianceStatus = formElementComplianceMap.value.get(formElement.id)!
if (formElementComplianceMap.value.has(elementKey)) {
const newComplianceStatus = formElementComplianceMap.value.get(elementKey)!
const newComplianceStatusPos = Object.values(ComplianceStatus).indexOf(newComplianceStatus)
if (newComplianceStatusPos > currentHighestComplianceStatusPos) {
formElementComplianceMap.value.set(formElement.id, newComplianceStatus)
formElementComplianceMap.value.set(elementKey, newComplianceStatus)
}
} else {
formElementComplianceMap.value.set(formElement.id, currentHighestComplianceStatus)
formElementComplianceMap.value.set(elementKey, currentHighestComplianceStatus)
}
})
})

View File

@@ -0,0 +1,48 @@
import type { FormElementDto } from '~~/.api-client'
export function useClonableElements() {
function cloneElement(element: FormElementDto, existingElements: FormElementDto[]): FormElementDto {
const newReference = element.reference ? generateNextReference(existingElements, element.reference) : undefined
const isTextField = element.type === 'TEXTAREA' || element.type === 'TEXTFIELD'
const clonedElement = JSON.parse(JSON.stringify(element)) as FormElementDto
const resetOptions = clonedElement.options.map((option) => ({
...option,
value: isTextField ? '' : option.value
}))
return {
...clonedElement,
id: undefined,
formElementSubSectionId: undefined,
reference: newReference,
options: resetOptions
}
}
function generateNextReference(existingElements: FormElementDto[], baseReference: string): string {
const { base } = extractReferenceBase(baseReference)
const existingSuffixes = existingElements
.filter((el) => el.reference && el.reference.startsWith(base))
.map((el) => {
const { suffix } = extractReferenceBase(el.reference!)
return suffix
})
const maxSuffix = existingSuffixes.length > 0 ? Math.max(...existingSuffixes) : 0
return `${base}_${maxSuffix + 1}`
}
function extractReferenceBase(reference: string): { base: string; suffix: number } {
const match = reference.match(/^(.+?)_(\d+)$/)
if (match && match[1] && match[2]) {
return { base: match[1], suffix: parseInt(match[2], 10) }
}
return { base: reference, suffix: 1 }
}
return {
cloneElement
}
}

View File

@@ -1,15 +1,15 @@
import type { ApplicationFormDto, CreateFormElementDto, FormElementDto } from '~~/.api-client'
import type { FormElementDto } from '~~/.api-client'
export function useFormElementManagement() {
const applicationForm = useApplicationForm()
function addInputFormElement(elements: FormElementDto[], position: number): FormElementDto[] {
const inputFormElement = createInputFormElement()
const updatedElements = [...elements]
updatedElements.splice(position + 1, 0, inputFormElement)
return updatedElements
}
async function addFormElementToSubSection(
applicationFormId: string | undefined,
subsectionId: string,
formElements: FormElementDto[],
position: number
): Promise<ApplicationFormDto | undefined> {
const inputFormElement: CreateFormElementDto = {
function createInputFormElement(): FormElementDto {
return {
title: 'Formular ergänzen',
description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.',
options: [
@@ -22,27 +22,9 @@ export function useFormElementManagement() {
],
type: 'TITLE_BODY_TEXTFIELDS'
}
if (applicationFormId) {
try {
return await applicationForm.addFormElementToSubSection(
applicationFormId,
subsectionId,
inputFormElement,
position + 1
)
} catch (error) {
console.error('Failed to add form element:', error)
throw error
}
} else {
// @ts-expect-error Add CreateFormElementDto to formElements array. ID will be generated by the backend.
formElements.splice(position + 1, 0, inputFormElement)
return undefined
}
}
return {
addFormElementToSubSection
addInputFormElement
}
}

View File

@@ -8,7 +8,10 @@ export function useFormElementVisibility() {
allFormElements.forEach((element) => {
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap)
visibilityMap.set(element.id, isVisible)
const key = element.id || element.reference
if (key) {
visibilityMap.set(key, isVisible)
}
})
return visibilityMap
@@ -42,10 +45,10 @@ export function useFormElementVisibility() {
const sourceValue = getFormElementValue(sourceElement)
const operator = condition.operator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.expectedValue, operator)
const operator = condition.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
return condition.conditionType === VCType.Show ? conditionMet : !conditionMet
return condition.formElementConditionType === VCType.Show ? conditionMet : !conditionMet
}
function getFormElementValue(element: FormElementDto): string {

View File

@@ -20,9 +20,11 @@ export function useFormStepper(
const sections = computed(() => toValue(formElementSections) ?? [])
const visibleSections = computed(() => sections.value.filter((section) => !section.isTemplate))
const stepperItems = computed(() => {
const items: StepperItem[] = []
sections.value.forEach((section: FormElementSectionDto) => {
visibleSections.value.forEach((section: FormElementSectionDto) => {
items.push({
title: section.shortTitle,
description: section.description
@@ -32,7 +34,7 @@ export function useFormStepper(
})
const currentFormElementSection = computed<FormElementSectionDto | undefined>(
() => sections.value[activeStepperItemIndex.value]
() => visibleSections.value[activeStepperItemIndex.value]
)
async function navigateStepper(direction: 'forward' | 'backward') {

View File

@@ -0,0 +1,197 @@
import type { FormElementDto, FormElementSectionDto, SectionSpawnTriggerDto } from '~~/.api-client'
import { VisibilityConditionOperator, VisibilityConditionType } from '~~/.api-client'
export function useSectionSpawning() {
function processSpawnTriggers(
sections: FormElementSectionDto[],
updatedFormElements: FormElementDto[]
): FormElementSectionDto[] {
let resultSections = sections
for (const formElement of updatedFormElements) {
if (!formElement.sectionSpawnTrigger || !formElement.reference) {
continue
}
// Extract trigger configuration and current element value
const trigger = formElement.sectionSpawnTrigger
const triggerValue = getFormElementValue(formElement)
const shouldSpawn = shouldSpawnSection(trigger, triggerValue)
// Use resultSections to check for existing spawned sections (in case multiple spawns happen)
const existingSpawnedSections = getSpawnedSectionsForElement(resultSections, formElement.reference)
// Handle three spawn states:
// 1. Condition met but no section spawned yet → create new section
if (shouldSpawn && existingSpawnedSections.length === 0) {
resultSections = spawnNewSection(resultSections, formElement, trigger, triggerValue)
}
// 2. Condition no longer met but section exists → remove spawned section
else if (!shouldSpawn && existingSpawnedSections.length > 0) {
resultSections = removeSpawnedSections(resultSections, formElement.reference)
}
// 3. Condition still met and section exists → update section titles if value changed
else if (shouldSpawn && existingSpawnedSections.length > 0 && triggerValue) {
resultSections = updateSpawnedSectionTitles(resultSections, formElement.reference, trigger, triggerValue)
}
}
return resultSections
}
function spawnNewSection(
sections: FormElementSectionDto[],
element: FormElementDto,
trigger: SectionSpawnTriggerDto,
triggerValue: string
): FormElementSectionDto[] {
const templateSection = findTemplateSection(sections, trigger.templateReference)
if (!templateSection) {
return sections
}
const newSection = spawnSectionFromTemplate(templateSection, element.reference!, triggerValue)
return sections.concat(newSection as FormElementSectionDto)
}
function updateSpawnedSectionTitles(
sections: FormElementSectionDto[],
elementReference: string,
trigger: SectionSpawnTriggerDto,
triggerValue: string
): FormElementSectionDto[] {
const template = findTemplateSection(sections, trigger.templateReference)
if (!template) {
return sections
}
const hasTitleTemplate = template.titleTemplate
const hasShortTitleTemplate = template.shortTitle?.includes('{{triggerValue}}')
const hasDescriptionTemplate = template.description?.includes('{{triggerValue}}')
if (!hasTitleTemplate && !hasShortTitleTemplate && !hasDescriptionTemplate) {
return sections
}
return sections.map((section) => {
if (section.spawnedFromElementReference === elementReference && !section.isTemplate) {
const sectionUpdate: Partial<FormElementSectionDto> = {}
if (hasTitleTemplate) {
sectionUpdate.title = interpolateTitle(template.titleTemplate!, triggerValue)
}
if (hasShortTitleTemplate && template.shortTitle) {
sectionUpdate.shortTitle = interpolateTitle(template.shortTitle, triggerValue)
}
if (hasDescriptionTemplate && template.description) {
sectionUpdate.description = interpolateTitle(template.description, triggerValue)
}
return { ...section, ...sectionUpdate }
}
return section
})
}
function removeSpawnedSections(sections: FormElementSectionDto[], elementReference: string): FormElementSectionDto[] {
return sections.filter((section) => section.spawnedFromElementReference !== elementReference || section.isTemplate)
}
function spawnSectionFromTemplate(
templateSection: FormElementSectionDto,
triggerElementReference: string,
triggerValue: string
): FormElementSectionDto {
const clonedSection = JSON.parse(JSON.stringify(templateSection)) as FormElementSectionDto
const title = templateSection.titleTemplate
? interpolateTitle(templateSection.titleTemplate, triggerValue)
: templateSection.title
const shortTitle = templateSection.shortTitle?.includes('{{triggerValue}}')
? interpolateTitle(templateSection.shortTitle, triggerValue)
: templateSection.shortTitle
const description = templateSection.description?.includes('{{triggerValue}}')
? interpolateTitle(templateSection.description, triggerValue)
: templateSection.description
return {
...clonedSection,
id: undefined,
applicationFormId: undefined,
title,
shortTitle,
description,
isTemplate: false,
spawnedFromElementReference: triggerElementReference,
formElementSubSections: clonedSection.formElementSubSections.map((subsection) => ({
...subsection,
id: undefined,
formElementSectionId: undefined,
formElements: subsection.formElements.map((element) => ({
...element,
id: undefined,
formElementSubSectionId: undefined
}))
}))
}
}
function shouldSpawnSection(trigger: SectionSpawnTriggerDto, triggerElementValue: string): boolean {
const operator = trigger.sectionSpawnOperator || VisibilityConditionOperator.Equals
const isConditionMet = evaluateCondition(triggerElementValue, trigger.sectionSpawnExpectedValue || '', operator)
return trigger.sectionSpawnConditionType === VisibilityConditionType.Show ? isConditionMet : !isConditionMet
}
function getSpawnedSectionsForElement(
sections: FormElementSectionDto[],
elementReference: string
): FormElementSectionDto[] {
return sections.filter((section) => !section.isTemplate && section.spawnedFromElementReference === elementReference)
}
function findTemplateSection(
sections: FormElementSectionDto[],
templateReference: string
): FormElementSectionDto | undefined {
return sections.find((section) => section.isTemplate && section.templateReference === templateReference)
}
function evaluateCondition(
actualValue: string,
expectedValue: string,
operator: VisibilityConditionOperator
): boolean {
switch (operator) {
case VisibilityConditionOperator.Equals:
return actualValue.toLowerCase() === expectedValue.toLowerCase()
case VisibilityConditionOperator.NotEquals:
return actualValue.toLowerCase() !== expectedValue.toLowerCase()
case VisibilityConditionOperator.IsEmpty:
return actualValue === ''
case VisibilityConditionOperator.IsNotEmpty:
return actualValue !== ''
default:
return false
}
}
function interpolateTitle(titleTemplate: string, triggerValue: string): string {
return titleTemplate.replace(/\{\{triggerValue\}\}/g, triggerValue)
}
function getFormElementValue(element: FormElementDto): string {
if (element.type === 'TEXTAREA' || element.type === 'TEXTFIELD') {
return element.options[0]?.value || ''
}
const selectedOption = element.options.find((option) => option.value === 'true')
return selectedOption?.label || ''
}
return {
processSpawnTriggers
}
}

View File

@@ -14,9 +14,7 @@ export function useUserApi() {
)
)
const userApiClient = new UserApi(
new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) })
)
const userApiClient = new UserApi(new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }))
async function getUserById(id: string): Promise<UserDto> {
return userApiClient.getUserById({ id })
@@ -39,4 +37,3 @@ export function useUserApi() {
deleteUser
}
}

View File

@@ -40,7 +40,8 @@
<div>
<h3 class="font-semibold text-lg">{{ currentTemplate.name }}</h3>
<p class="text-sm text-muted mt-1">
{{ $t('templates.lastModified') }} {{ formatDate(new Date(currentTemplate.modifiedAt)) }}
{{ $t('templates.lastModified') }}
{{ currentTemplate.modifiedAt ? formatDate(new Date(currentTemplate.modifiedAt)) : '-' }}
</p>
</div>
<UBadge v-if="hasUnsavedChanges" :label="$t('templates.unsavedChanges')" color="warning" variant="subtle" />
@@ -206,17 +207,20 @@ async function saveTemplate() {
try {
const parsedData = JSON.parse(editorContent.value)
const dataWithDates = convertDates(parsedData) as ApplicationFormDto
const dataWithDates = convertDates(parsedData)
if (currentTemplate.value?.id) {
currentTemplate.value = await updateApplicationFormTemplate(currentTemplate.value.id, dataWithDates)
currentTemplate.value = await updateApplicationFormTemplate(
currentTemplate.value.id,
dataWithDates as ApplicationFormDto
)
toast.add({
title: $t('common.success'),
description: $t('templates.updated'),
color: 'success'
})
} else {
currentTemplate.value = await createApplicationFormTemplate(dataWithDates)
currentTemplate.value = await createApplicationFormTemplate(dataWithDates as ApplicationFormDto)
toast.add({
title: $t('common.success'),
description: $t('templates.created'),

View File

@@ -34,12 +34,13 @@
<FormStepperWithNavigation
:form-element-sections="applicationForm.formElementSections"
:initial-section-index="sectionIndex"
:application-form-id="applicationForm.id"
:application-form-id="applicationForm.id ?? undefined"
:disabled="isReadOnly"
@save="onSave"
@submit="onSubmit"
@navigate="handleNavigate"
@add-input-form="handleAddInputForm"
@update:form-element-sections="handleFormElementSectionsUpdate"
/>
</div>
</template>
@@ -87,7 +88,7 @@ const sectionIndex = computed(() => {
})
const isReadOnly = computed(() => {
return applicationForm.value?.createdBy.keycloakId !== user.value?.keycloakId
return applicationForm.value?.createdBy?.keycloakId !== user.value?.keycloakId
})
const validationMap = ref<Map<FormElementId, ComplianceStatus> | undefined>()
@@ -115,7 +116,7 @@ watch(
)
async function onSave() {
if (applicationForm.value) {
if (applicationForm.value?.id) {
const updated = await updateForm(applicationForm.value.id, applicationForm.value)
if (updated) {
updateApplicationForm(updated)
@@ -125,7 +126,7 @@ async function onSave() {
}
async function onSubmit() {
if (applicationForm.value) {
if (applicationForm.value?.id) {
await submitApplicationForm(applicationForm.value.id)
await navigateTo('/')
toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' })
@@ -141,4 +142,10 @@ function handleAddInputForm(updatedForm: ApplicationFormDto | undefined) {
updateApplicationForm(updatedForm)
}
}
function handleFormElementSectionsUpdate(sections: FormElementSectionDto[]) {
if (applicationForm.value) {
applicationForm.value.formElementSections = sections
}
}
</script>

View File

@@ -16,7 +16,7 @@
<template #body>
<div class="p-6">
<VersionHistory
v-if="applicationForm"
v-if="applicationForm?.id"
:application-form-id="applicationForm.id"
:current-form="applicationForm"
@restored="handleRestored"
@@ -46,6 +46,9 @@ async function handleRestored() {
description: $t('versions.restoredDescription'),
color: 'success'
})
if (!applicationForm.value?.id) {
return
}
router.push(`/application-forms/${applicationForm.value.id}/0`)
}
</script>

View File

@@ -28,6 +28,7 @@
@save="onSave"
@submit="onSubmit"
@add-input-form="handleAddInputForm"
@update:form-element-sections="handleFormElementSectionsUpdate"
>
<UFormField :label="$t('common.name')" class="mb-4">
<UInput v-model="applicationFormTemplate.name" class="w-full" />
@@ -110,7 +111,7 @@ async function onSave() {
async function onSubmit() {
const applicationForm = await prepareAndCreateApplicationForm()
if (applicationForm) {
if (applicationForm?.id) {
await submitApplicationForm(applicationForm.id)
await navigateTo('/')
toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' })
@@ -123,6 +124,12 @@ function handleAddInputForm() {
// No action needed here
}
function handleFormElementSectionsUpdate(sections: FormElementSectionDto[]) {
if (applicationFormTemplate.value) {
applicationFormTemplate.value.formElementSections = sections
}
}
async function prepareAndCreateApplicationForm() {
if (!applicationFormTemplate.value) {
console.error('Application form data is undefined')

View File

@@ -49,9 +49,9 @@
<div class="flex flex-col gap-4 sm:gap-6 w-full lg:max-w-4xl mx-auto p-4">
<UCard
v-for="(applicationFormElem, index) in applicationForms"
:key="applicationFormElem.id"
:key="applicationFormElem.id ?? index"
class="cursor-pointer hover:ring-2 hover:ring-primary transition-all duration-200"
@click="navigateTo(`application-forms/${applicationFormElem.id}/0`)"
@click="openApplicationForm(applicationFormElem.id)"
>
<template #header>
<div class="flex items-start justify-between gap-3">
@@ -63,6 +63,7 @@
</div>
<div class="flex items-center gap-2">
<UBadge
v-if="applicationFormElem.status"
:label="applicationFormElem.status"
:color="getStatusColor(applicationFormElem.status)"
variant="subtle"
@@ -87,8 +88,11 @@
<UIcon name="i-lucide-pencil" class="size-4 text-muted shrink-0" />
<span class="text-muted">
{{ $t('applicationForms.lastEditedBy') }}
<span class="font-medium text-highlighted">{{ applicationFormElem.lastModifiedBy.name }}</span>
{{ $t('common.on') }} {{ formatDate(applicationFormElem.modifiedAt) }}
<span class="font-medium text-highlighted">
{{ applicationFormElem.lastModifiedBy?.name ?? '-' }}
</span>
{{ $t('common.on') }}
{{ applicationFormElem.modifiedAt ? formatDate(applicationFormElem.modifiedAt) : '-' }}
</span>
</div>
@@ -96,8 +100,11 @@
<UIcon name="i-lucide-user-plus" class="size-4 text-muted shrink-0" />
<span class="text-muted">
{{ $t('applicationForms.createdBy') }}
<span class="font-medium text-highlighted">{{ applicationFormElem.createdBy.name }}</span>
{{ $t('common.on') }} {{ formatDate(applicationFormElem.createdAt) }}
<span class="font-medium text-highlighted">
{{ applicationFormElem.createdBy?.name ?? '-' }}
</span>
{{ $t('common.on') }}
{{ applicationFormElem.createdAt ? formatDate(applicationFormElem.createdAt) : '-' }}
</span>
</div>
</div>
@@ -177,6 +184,9 @@ const applicationForms = computed({
})
function getLinksForApplicationForm(applicationForm: ApplicationFormDto) {
if (!applicationForm.id) {
return []
}
return [
{
label: $t('common.delete'),
@@ -206,4 +216,11 @@ async function deleteApplicationForm(applicationFormId: string) {
)
isDeleteModalOpen.value = false
}
function openApplicationForm(applicationFormId: string | null | undefined) {
if (!applicationFormId) {
return
}
navigateTo(`application-forms/${applicationFormId}/0`)
}
</script>

View File

@@ -17,6 +17,7 @@
"formElements": {
"comments": "Kommentare",
"addInputBelow": "Eingabefeld hinzufügen",
"addAnother": "Weiteres hinzufügen",
"selectPlaceholder": "Status auswählen",
"selectDate": "Datum auswählen",
"title": "Titel",

View File

@@ -17,6 +17,7 @@
"formElements": {
"comments": "Comments",
"addInputBelow": "Add input field below",
"addAnother": "Add another",
"selectPlaceholder": "Select status",
"selectDate": "Select a date",
"title": "Title",

View File

@@ -12,8 +12,7 @@
"type-check": "nuxi typecheck",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"api:generate": "openapi-generator-cli generate -i ../api/legalconsenthub.yml -g typescript-fetch -o .api-client",
"api:middleware:generate": "openapi-generator-cli generate -i ../api/legalconsenthub-middleware.yml -g typescript-fetch -o .api-client-middleware"
"api:generate": "openapi-generator-cli generate -i ../api/legalconsenthub.yml -g typescript-fetch -o .api-client"
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.6.0",

View File

@@ -63,10 +63,10 @@
],
"type": "DATE",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Einführung",
"operator": "EQUALS"
"formElementExpectedValue": "Einführung",
"formElementOperator": "EQUALS"
}
},
{
@@ -89,10 +89,10 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -109,10 +109,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
"formElementExpectedValue": "Ja",
"formElementOperator": "EQUALS"
}
},
{
@@ -135,10 +135,10 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja",
"operator": "EQUALS"
"formElementExpectedValue": "Ja",
"formElementOperator": "EQUALS"
}
},
{
@@ -155,10 +155,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
"formElementExpectedValue": "Nein",
"formElementOperator": "EQUALS"
}
},
{
@@ -175,10 +175,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
"formElementExpectedValue": "Nein",
"formElementOperator": "EQUALS"
}
},
{
@@ -195,10 +195,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein",
"operator": "EQUALS"
"formElementExpectedValue": "Nein",
"formElementOperator": "EQUALS"
}
},
{
@@ -215,10 +215,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -235,10 +235,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Änderung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Änderung IT-System",
"formElementOperator": "EQUALS"
}
}
]
@@ -261,10 +261,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -281,10 +281,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -301,10 +301,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
}
]
@@ -327,10 +327,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -347,10 +347,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -367,10 +367,10 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -393,10 +393,10 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -425,10 +425,10 @@
],
"type": "SELECT",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -451,10 +451,10 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -477,70 +477,36 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
"reference": "modul_1",
"title": "Modul 1",
"description": "Beschreibung des ersten Moduls",
"title": "Modulname",
"description": "Name des Moduls eingeben",
"options": [
{
"value": "",
"label": "Modul 1",
"label": "Modulname",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
],
"type": "TEXTAREA",
"isClonable": true,
"sectionSpawnTrigger": {
"templateReference": "module_details_template",
"sectionSpawnConditionType": "SHOW",
"sectionSpawnOperator": "IS_NOT_EMPTY"
},
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "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": "TEXTAREA",
"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": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"sourceFormElementReference": "modulbasiertes_system",
"expectedValue": "Ja",
"operator": "EQUALS"
"formElementExpectedValue": "Ja",
"formElementOperator": "EQUALS"
}
},
{
@@ -562,11 +528,18 @@
}
],
"type": "RADIOBUTTON",
"isClonable": false,
"sectionSpawnTrigger": {
"templateReference": "ki_details_template",
"sectionSpawnConditionType": "SHOW",
"sectionSpawnExpectedValue": "Ja",
"sectionSpawnOperator": "EQUALS"
},
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -589,10 +562,10 @@
],
"type": "RADIOBUTTON",
"visibilityCondition": {
"conditionType": "HIDE",
"formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS"
"formElementExpectedValue": "Ablösung/Einstellung IT-System",
"formElementOperator": "EQUALS"
}
},
{
@@ -609,15 +582,184 @@
],
"type": "TEXTAREA",
"visibilityCondition": {
"conditionType": "SHOW",
"formElementConditionType": "SHOW",
"sourceFormElementReference": "wirtschaftliche_auswirkungen",
"expectedValue": "Ja",
"operator": "EQUALS"
"formElementExpectedValue": "Ja",
"formElementOperator": "EQUALS"
}
}
]
}
]
},
{
"title": "Moduldetails",
"shortTitle": "{{triggerValue}}",
"description": "Detaillierte Informationen zum Modul",
"isTemplate": true,
"templateReference": "module_details_template",
"titleTemplate": "Modul: {{triggerValue}}",
"formElementSubSections": [
{
"title": "Modulinformationen",
"formElements": [
{
"reference": "modul_beschreibung",
"title": "Modulbeschreibung",
"description": "Beschreiben Sie die Funktionalität des Moduls",
"type": "TEXTAREA",
"options": [
{
"value": "",
"label": "Beschreibung",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
}
]
},
{
"reference": "modul_nutzergruppen",
"title": "Nutzergruppen",
"description": "Welche Nutzergruppen verwenden dieses Modul?",
"type": "TEXTAREA",
"options": [
{
"value": "",
"label": "Nutzergruppen",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "REVIEW_REQUIRED"
}
]
},
{
"reference": "modul_datenkategorien",
"title": "Verarbeitete Datenkategorien",
"type": "CHECKBOX",
"options": [
{
"value": "false",
"label": "Stammdaten",
"processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "false",
"label": "Leistungsdaten",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "false",
"label": "Verhaltensdaten",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
]
}
]
}
]
},
{
"title": "Details zum KI-Einsatz",
"shortTitle": "KI-Einsatz",
"description": "Informationen zum Einsatz künstlicher Intelligenz",
"isTemplate": true,
"templateReference": "ki_details_template",
"titleTemplate": "Details zum KI-Einsatz",
"formElementSubSections": [
{
"title": "KI-Informationen",
"formElements": [
{
"reference": "ki_art",
"title": "Art der KI",
"description": "Um welche Art von KI handelt es sich?",
"type": "CHECKBOX",
"options": [
{
"value": "false",
"label": "Machine Learning",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "false",
"label": "Generative KI (LLM)",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "false",
"label": "Regelbasierte KI",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "REVIEW_REQUIRED"
}
]
},
{
"reference": "ki_zweck",
"title": "Einsatzzweck der KI",
"description": "Für welchen Zweck wird die KI eingesetzt?",
"type": "TEXTAREA",
"options": [
{
"value": "",
"label": "Einsatzzweck",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
]
},
{
"reference": "ki_entscheidungen",
"title": "Automatisierte Entscheidungen",
"description": "Werden durch die KI automatisierte Entscheidungen getroffen, die Beschäftigte betreffen?",
"type": "RADIOBUTTON",
"options": [
{
"value": "Nein",
"label": "Nein",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Ja, mit menschlicher Überprüfung",
"label": "Ja, mit menschlicher Überprüfung",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
},
{
"value": "Ja, vollautomatisch",
"label": "Ja, vollautomatisch",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
]
},
{
"reference": "ki_trainingsdaten",
"title": "Trainingsdaten",
"description": "Werden Beschäftigtendaten für das Training der KI verwendet?",
"type": "RADIOBUTTON",
"options": [
{
"value": "Nein",
"label": "Nein",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "NON_CRITICAL"
},
{
"value": "Ja",
"label": "Ja",
"processingPurpose": "DATA_ANALYSIS",
"employeeDataCategory": "SENSITIVE"
}
]
}
]
}
]
}
]
}