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 │ ├── processingPurpose
│ └── employeeDataCategory │ └── employeeDataCategory
└── visibilityCondition (FormElementVisibilityCondition) └── visibilityCondition (FormElementVisibilityCondition)
├── conditionType (SHOW, HIDE) ├── formElementConditionType (SHOW, HIDE)
├── sourceFormElementReference (string - reference key of source element) ├── sourceFormElementReference (string - reference key of source element)
├── expectedValue (string - value to compare against) ├── formElementExpectedValue (string - value to compare against)
└── operator (EQUALS, NOT_EQUALS, IS_EMPTY, IS_NOT_EMPTY) └── 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: **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": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt", "sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja", "formElementExpectedValue": "Ja",
"operator": "EQUALS" "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" - For checkboxes, checked = "true", unchecked = "false"
- Test visibility conditions thoroughly before deployment - 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 ## Project Structure
@@ -303,9 +403,12 @@ legalconsenthub/
│ │ ├── complianceMap.ts │ │ ├── complianceMap.ts
│ │ ├── useApplicationFormNavigation.ts │ │ ├── useApplicationFormNavigation.ts
│ │ ├── useApplicationFormValidator.ts │ │ ├── useApplicationFormValidator.ts
│ │ ├── useClonableElements.ts
│ │ ├── useFormElementManagement.ts │ │ ├── useFormElementManagement.ts
│ │ ├── useFormElementVisibility.ts
│ │ ├── useFormStepper.ts │ │ ├── useFormStepper.ts
│ │ ├── usePermissions.ts │ │ ├── usePermissions.ts
│ │ ├── useSectionSpawning.ts
│ │ └── useServerHealth.ts │ │ └── useServerHealth.ts
│ ├── layouts/ # Layout components │ ├── layouts/ # Layout components
│ │ ├── auth.vue │ │ ├── auth.vue

View File

@@ -46,7 +46,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/CreateApplicationFormDto" $ref: "#/components/schemas/ApplicationFormDto"
responses: responses:
"201": "201":
description: Successfully created application form description: Successfully created application form
@@ -256,7 +256,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/CreateFormElementDto" $ref: "#/components/schemas/FormElementDto"
responses: responses:
"201": "201":
description: Form element successfully added description: Form element successfully added
@@ -406,7 +406,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/CreateApplicationFormDto" $ref: "#/components/schemas/ApplicationFormDto"
responses: responses:
"201": "201":
description: Successfully created application form template description: Successfully created application form template
@@ -1088,63 +1088,44 @@ components:
ApplicationFormDto: ApplicationFormDto:
type: object type: object
required: required:
- id
- name - name
- formElementSections - formElementSections
- isTemplate - isTemplate
- organizationId
- createdBy
- lastModifiedBy
- createdAt
- modifiedAt
- status
properties: properties:
id: id:
type: string type: string
format: uuid format: uuid
nullable: true
name: name:
type: string type: string
formElementSections: formElementSections:
type: array type: array
items: items:
$ref: "#/components/schemas/FormElementSectionDto" $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: isTemplate:
type: boolean type: boolean
default: false default: false
organizationId: organizationId:
type: string 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: status:
$ref: "#/components/schemas/ApplicationFormStatus" allOf:
- $ref: "#/components/schemas/ApplicationFormStatus"
nullable: true
PagedApplicationFormDto: PagedApplicationFormDto:
type: object type: object
@@ -1249,14 +1230,13 @@ components:
FormElementSectionDto: FormElementSectionDto:
type: object type: object
required: required:
- id
- title - title
- formElementSubSections - formElementSubSections
- applicationFormId
properties: properties:
id: id:
type: string type: string
format: uuid format: uuid
nullable: true
title: title:
type: string type: string
shortTitle: shortTitle:
@@ -1270,6 +1250,20 @@ components:
applicationFormId: applicationFormId:
type: string type: string
format: uuid 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: FormElementSectionSnapshotDto:
type: object type: object
@@ -1287,38 +1281,26 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/FormElementSubSectionSnapshotDto" $ref: "#/components/schemas/FormElementSubSectionSnapshotDto"
isTemplate:
CreateFormElementSectionDto: type: boolean
type: object default: false
required: templateReference:
- title
- formElementSubSections
properties:
id:
type: string type: string
format: uuid titleTemplate:
title:
type: string type: string
shortTitle: spawnedFromElementReference:
type: string type: string
description:
type: string
formElementSubSections:
type: array
items:
$ref: "#/components/schemas/CreateFormElementSubSectionDto"
FormElementSubSectionDto: FormElementSubSectionDto:
type: object type: object
required: required:
- id
- title - title
- formElements - formElements
- formElementSectionId
properties: properties:
id: id:
type: string type: string
format: uuid format: uuid
nullable: true
title: title:
type: string type: string
subtitle: subtitle:
@@ -1330,6 +1312,7 @@ components:
formElementSectionId: formElementSectionId:
type: string type: string
format: uuid format: uuid
nullable: true
FormElementSubSectionSnapshotDto: FormElementSubSectionSnapshotDto:
type: object type: object
@@ -1346,32 +1329,16 @@ components:
items: items:
$ref: "#/components/schemas/FormElementSnapshotDto" $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: FormElementDto:
type: object type: object
required: required:
- id
- options - options
- type - type
- formElementSubSectionId
properties: properties:
id: id:
type: string type: string
format: uuid format: uuid
nullable: true
reference: reference:
type: string type: string
description: Unique reference key for this form element (e.g., "art_der_massnahme") description: Unique reference key for this form element (e.g., "art_der_massnahme")
@@ -1388,8 +1355,15 @@ components:
formElementSubSectionId: formElementSubSectionId:
type: string type: string
format: uuid format: uuid
nullable: true
visibilityCondition: visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition" $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: FormElementSnapshotDto:
type: object type: object
@@ -1411,27 +1385,11 @@ components:
$ref: "#/components/schemas/FormOptionDto" $ref: "#/components/schemas/FormOptionDto"
visibilityCondition: visibilityCondition:
$ref: "#/components/schemas/FormElementVisibilityCondition" $ref: "#/components/schemas/FormElementVisibilityCondition"
sectionSpawnTrigger:
CreateFormElementDto: $ref: "#/components/schemas/SectionSpawnTriggerDto"
type: object isClonable:
required: type: boolean
- options default: false
- 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"
FormOptionDto: FormOptionDto:
type: object type: object
@@ -1465,19 +1423,19 @@ components:
FormElementVisibilityCondition: FormElementVisibilityCondition:
type: object type: object
required: required:
- conditionType - formElementConditionType
- sourceFormElementReference - sourceFormElementReference
- expectedValue - formElementExpectedValue
properties: properties:
conditionType: formElementConditionType:
$ref: "#/components/schemas/VisibilityConditionType" $ref: "#/components/schemas/VisibilityConditionType"
sourceFormElementReference: sourceFormElementReference:
type: string type: string
description: Reference key of the source form element to check description: Reference key of the source form element to check
expectedValue: formElementExpectedValue:
type: string type: string
description: Expected value to compare against the source element's value property description: Expected value to compare against the source element's value property
operator: formElementOperator:
$ref: "#/components/schemas/VisibilityConditionOperator" $ref: "#/components/schemas/VisibilityConditionOperator"
default: EQUALS default: EQUALS
@@ -1495,6 +1453,24 @@ components:
- IS_EMPTY - IS_EMPTY
- IS_NOT_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 #######
UserDto: UserDto:
type: object 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.api.ApplicationFormApi
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource import org.springframework.core.io.Resource
@@ -24,13 +23,11 @@ class ApplicationFormController(
@PreAuthorize( @PreAuthorize(
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')", "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
) )
override fun createApplicationForm( override fun createApplicationForm(applicationFormDto: ApplicationFormDto): ResponseEntity<ApplicationFormDto> {
createApplicationFormDto: CreateApplicationFormDto, val updatedApplicationFormDto = applicationFormDto.copy(isTemplate = false)
): ResponseEntity<ApplicationFormDto> {
val updatedCreateApplicationFormDto = createApplicationFormDto.copy(isTemplate = false)
return ResponseEntity.ok( return ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto( applicationFormMapper.toApplicationFormDto(
applicationFormService.createApplicationForm(updatedCreateApplicationFormDto), applicationFormService.createApplicationForm(updatedApplicationFormDto),
), ),
) )
} }
@@ -88,7 +85,7 @@ class ApplicationFormController(
): ResponseEntity<ApplicationFormDto> = ): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok( ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto( applicationFormMapper.toApplicationFormDto(
applicationFormService.updateApplicationForm(applicationFormDto), applicationFormService.updateApplicationForm(id, applicationFormDto),
), ),
) )
@@ -117,14 +114,14 @@ class ApplicationFormController(
applicationFormId: UUID, applicationFormId: UUID,
subsectionId: UUID, subsectionId: UUID,
position: Int, position: Int,
createFormElementDto: CreateFormElementDto, formElementDto: FormElementDto,
): ResponseEntity<ApplicationFormDto> = ): ResponseEntity<ApplicationFormDto> =
ResponseEntity.status(201).body( ResponseEntity.status(201).body(
applicationFormMapper.toApplicationFormDto( applicationFormMapper.toApplicationFormDto(
applicationFormService.addFormElementToSubSection( applicationFormService.addFormElementToSubSection(
applicationFormId, applicationFormId,
subsectionId, subsectionId,
createFormElementDto, formElementDto,
position, position,
), ),
), ),

View File

@@ -43,38 +43,45 @@ class ApplicationFormFormatService(
val visibilityMap = evaluateVisibility(formElementsByRef) val visibilityMap = evaluateVisibility(formElementsByRef)
val filteredSections = val filteredSections =
applicationForm.formElementSections.mapNotNull { section -> applicationForm.formElementSections
val filteredSubSections = .filter { !it.isTemplate }
section.formElementSubSections.mapNotNull { subsection -> .mapNotNull { section ->
val filteredElements = val filteredSubSections =
subsection.formElements.filter { element -> section.formElementSubSections
visibilityMap[element.id] == true .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()) { if (filteredSubSections.isEmpty()) {
null null
} else { } else {
FormElementSubSection( FormElementSection(
id = subsection.id, id = section.id,
title = subsection.title, title = section.title,
subtitle = subsection.subtitle, shortTitle = section.shortTitle,
formElements = filteredElements.toMutableList(), description = section.description,
formElementSection = null, 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( return ApplicationForm(
id = applicationForm.id, id = applicationForm.id,
@@ -122,9 +129,10 @@ class ApplicationFormFormatService(
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
val sourceValue = getFormElementValue(sourceElement) 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.SHOW -> conditionMet
VisibilityConditionType.HIDE -> !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.UserMapper
import com.betriebsratkanzlei.legalconsenthub.user.UserService import com.betriebsratkanzlei.legalconsenthub.user.UserService
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID
@Component @Component
class ApplicationFormMapper( class ApplicationFormMapper(
@@ -16,7 +16,7 @@ class ApplicationFormMapper(
) { ) {
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto = fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto =
ApplicationFormDto( ApplicationFormDto(
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"), id = applicationForm.id,
name = applicationForm.name, name = applicationForm.name,
formElementSections = formElementSections =
applicationForm.formElementSections.map { applicationForm.formElementSections.map {
@@ -34,45 +34,53 @@ class ApplicationFormMapper(
status = applicationForm.status, status = applicationForm.status,
) )
fun toApplicationForm(applicationForm: ApplicationFormDto): ApplicationForm { fun toNewApplicationForm(applicationFormDto: 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 {
val currentUser = userService.getCurrentUser() val currentUser = userService.getCurrentUser()
val applicationForm = val applicationForm =
ApplicationForm( ApplicationForm(
name = createApplicationFormDto.name, id = null,
isTemplate = createApplicationFormDto.isTemplate, name = applicationFormDto.name,
organizationId = createApplicationFormDto.organizationId ?: "", isTemplate = applicationFormDto.isTemplate,
organizationId = applicationFormDto.organizationId ?: "",
status = status =
createApplicationFormDto.status applicationFormDto.status
?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT, ?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT,
createdBy = currentUser, createdBy = currentUser,
lastModifiedBy = currentUser, lastModifiedBy = currentUser,
) )
applicationForm.formElementSections = applicationForm.formElementSections =
createApplicationFormDto.formElementSections applicationFormDto.formElementSections
.map { formElementSectionMapper.toFormElementSection(it, applicationForm) } .map { formElementSectionMapper.toNewFormElementSection(it, applicationForm) }
.toMutableList() .toMutableList()
return applicationForm 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.user.UserService
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus 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.CreateNotificationDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Page import org.springframework.data.domain.Page
@@ -33,8 +32,8 @@ class ApplicationFormService(
private val userService: UserService, private val userService: UserService,
private val eventPublisher: ApplicationEventPublisher, private val eventPublisher: ApplicationEventPublisher,
) { ) {
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { fun createApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto) val applicationForm = applicationFormMapper.toNewApplicationForm(applicationFormDto)
val savedApplicationForm: ApplicationForm val savedApplicationForm: ApplicationForm
try { try {
savedApplicationForm = applicationFormRepository.save(applicationForm) savedApplicationForm = applicationFormRepository.save(applicationForm)
@@ -67,17 +66,26 @@ class ApplicationFormService(
return applicationFormRepository.findAllByIsTemplateFalseAndOrganizationId(organizationId, pageable) return applicationFormRepository.findAllByIsTemplateFalseAndOrganizationId(organizationId, pageable)
} }
fun updateApplicationForm(applicationFormDto: ApplicationFormDto): ApplicationForm { fun updateApplicationForm(
val existingApplicationForm = getApplicationFormById(applicationFormDto.id) id: UUID,
applicationFormDto: ApplicationFormDto,
): ApplicationForm {
println("Updating ApplicationForm: $applicationFormDto")
val existingApplicationForm = getApplicationFormById(id)
val existingSnapshot = versionService.createSnapshot(existingApplicationForm) val existingSnapshot = versionService.createSnapshot(existingApplicationForm)
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto) val applicationForm =
applicationFormMapper.toUpdatedApplicationForm(
id,
applicationFormDto,
existingApplicationForm,
)
val updatedApplicationForm: ApplicationForm val updatedApplicationForm: ApplicationForm
try { try {
updatedApplicationForm = applicationFormRepository.save(applicationForm) updatedApplicationForm = applicationFormRepository.save(applicationForm)
} catch (e: Exception) { } catch (e: Exception) {
throw ApplicationFormNotUpdatedException(e, applicationFormDto.id) throw ApplicationFormNotUpdatedException(e, id)
} }
val currentUser = userService.getCurrentUser() val currentUser = userService.getCurrentUser()
@@ -160,7 +168,7 @@ class ApplicationFormService(
fun addFormElementToSubSection( fun addFormElementToSubSection(
applicationFormId: UUID, applicationFormId: UUID,
subsectionId: UUID, subsectionId: UUID,
createFormElementDto: CreateFormElementDto, formElementDto: FormElementDto,
position: Int, position: Int,
): ApplicationForm { ): ApplicationForm {
val applicationForm = getApplicationFormById(applicationFormId) val applicationForm = getApplicationFormById(applicationFormId)
@@ -171,7 +179,7 @@ class ApplicationFormService(
.find { it.id == subsectionId } .find { it.id == subsectionId }
?: throw IllegalArgumentException("FormElementSubSection with id $subsectionId not found") ?: 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) { if (position >= 0 && position < subsection.formElements.size) {
subsection.formElements.add(position, newFormElement) 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.application_form.PagedApplicationFormMapper
import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormTemplateApi import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormTemplateApi
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize 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')", "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
) )
override fun createApplicationFormTemplate( override fun createApplicationFormTemplate(
createApplicationFormDto: CreateApplicationFormDto, applicationFormDto: ApplicationFormDto,
): ResponseEntity<ApplicationFormDto> = ): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok( ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto( applicationFormMapper.toApplicationFormDto(
applicationFormTemplateService.createApplicationFormTemplate(createApplicationFormDto), applicationFormTemplateService.createApplicationFormTemplate(
applicationFormDto.copy(isTemplate = true),
),
), ),
) )
@@ -58,7 +59,7 @@ class ApplicationFormTemplateController(
): ResponseEntity<ApplicationFormDto> = ): ResponseEntity<ApplicationFormDto> =
ResponseEntity.ok( ResponseEntity.ok(
applicationFormMapper.toApplicationFormDto( 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.ApplicationFormNotFoundException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto 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.Page
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -19,8 +18,8 @@ class ApplicationFormTemplateService(
private val applicationFormRepository: ApplicationFormRepository, private val applicationFormRepository: ApplicationFormRepository,
private val applicationFormMapper: ApplicationFormMapper, private val applicationFormMapper: ApplicationFormMapper,
) { ) {
fun createApplicationFormTemplate(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { fun createApplicationFormTemplate(applicationFormDto: ApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto) val applicationForm = applicationFormMapper.toNewApplicationForm(applicationFormDto)
val savedApplicationForm: ApplicationForm val savedApplicationForm: ApplicationForm
try { try {
savedApplicationForm = applicationFormRepository.save(applicationForm) savedApplicationForm = applicationFormRepository.save(applicationForm)
@@ -41,14 +40,23 @@ class ApplicationFormTemplateService(
return applicationFormRepository.findAllByIsTemplateTrue(pageable) return applicationFormRepository.findAllByIsTemplateTrue(pageable)
} }
fun updateApplicationFormTemplate(applicationFormDto: ApplicationFormDto): ApplicationForm { fun updateApplicationFormTemplate(
val applicationForm = applicationFormMapper.toApplicationForm(applicationFormDto) id: UUID,
applicationFormDto: ApplicationFormDto,
): ApplicationForm {
val existingApplicationForm = getApplicationFormTemplateById(id)
val applicationForm =
applicationFormMapper.toUpdatedApplicationForm(
id,
applicationFormDto,
existingApplicationForm,
)
val updatedApplicationForm: ApplicationForm val updatedApplicationForm: ApplicationForm
try { try {
updatedApplicationForm = applicationFormRepository.save(applicationForm) updatedApplicationForm = applicationFormRepository.save(applicationForm)
} catch (e: Exception) { } catch (e: Exception) {
throw ApplicationFormNotUpdatedException(e, applicationFormDto.id) throw ApplicationFormNotUpdatedException(e, id)
} }
return updatedApplicationForm 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.FormElementSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormOption import com.betriebsratkanzlei.legalconsenthub.form_element.FormOption
import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper
import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
@@ -24,6 +25,7 @@ class ApplicationFormVersionService(
private val versionRepository: ApplicationFormVersionRepository, private val versionRepository: ApplicationFormVersionRepository,
private val applicationFormRepository: ApplicationFormRepository, private val applicationFormRepository: ApplicationFormRepository,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
) { ) {
@Transactional @Transactional
fun createVersion( fun createVersion(
@@ -103,6 +105,10 @@ class ApplicationFormVersionService(
title = section.title, title = section.title,
shortTitle = section.shortTitle, shortTitle = section.shortTitle,
description = section.description, description = section.description,
isTemplate = section.isTemplate,
templateReference = section.templateReference,
titleTemplate = section.titleTemplate,
spawnedFromElementReference = section.spawnedFromElementReference,
subsections = subsections =
section.formElementSubSections.map { subsection -> section.formElementSubSections.map { subsection ->
FormElementSubSectionSnapshotDto( FormElementSubSectionSnapshotDto(
@@ -111,6 +117,7 @@ class ApplicationFormVersionService(
elements = elements =
subsection.formElements.map { element -> subsection.formElements.map { element ->
FormElementSnapshotDto( FormElementSnapshotDto(
reference = element.reference,
title = element.title, title = element.title,
description = element.description, description = element.description,
type = element.type, type = element.type,
@@ -123,6 +130,11 @@ class ApplicationFormVersionService(
employeeDataCategory = option.employeeDataCategory, employeeDataCategory = option.employeeDataCategory,
) )
}, },
sectionSpawnTrigger =
element.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
},
isClonable = element.isClonable,
) )
}, },
) )
@@ -140,6 +152,10 @@ class ApplicationFormVersionService(
title = sectionSnapshot.title, title = sectionSnapshot.title,
shortTitle = sectionSnapshot.shortTitle, shortTitle = sectionSnapshot.shortTitle,
description = sectionSnapshot.description, description = sectionSnapshot.description,
isTemplate = sectionSnapshot.isTemplate ?: false,
templateReference = sectionSnapshot.templateReference,
titleTemplate = sectionSnapshot.titleTemplate,
spawnedFromElementReference = sectionSnapshot.spawnedFromElementReference,
applicationForm = applicationForm, applicationForm = applicationForm,
) )
@@ -154,6 +170,7 @@ class ApplicationFormVersionService(
subsectionSnapshot.elements.forEach { elementSnapshot -> subsectionSnapshot.elements.forEach { elementSnapshot ->
val element = val element =
FormElement( FormElement(
reference = elementSnapshot.reference,
title = elementSnapshot.title, title = elementSnapshot.title,
description = elementSnapshot.description, description = elementSnapshot.description,
type = elementSnapshot.type, type = elementSnapshot.type,
@@ -168,6 +185,11 @@ class ApplicationFormVersionService(
employeeDataCategory = optionDto.employeeDataCategory, employeeDataCategory = optionDto.employeeDataCategory,
) )
}.toMutableList(), }.toMutableList(),
sectionSpawnTrigger =
elementSnapshot.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = elementSnapshot.isClonable ?: false,
) )
subsection.formElements.add(element) subsection.formElements.add(element)
} }

View File

@@ -4,6 +4,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
import jakarta.persistence.CollectionTable import jakarta.persistence.CollectionTable
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.ElementCollection import jakarta.persistence.ElementCollection
import jakarta.persistence.Embedded
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id import jakarta.persistence.Id
@@ -27,5 +28,9 @@ class FormElement(
@ManyToOne @ManyToOne
@JoinColumn(name = "form_element_sub_section_id", nullable = false) @JoinColumn(name = "form_element_sub_section_id", nullable = false)
var formElementSubSection: FormElementSubSection? = null, var formElementSubSection: FormElementSubSection? = null,
@Embedded
var visibilityCondition: FormElementVisibilityCondition? = null, 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 package com.betriebsratkanzlei.legalconsenthub.form_element
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementDto
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -8,10 +7,11 @@ import org.springframework.stereotype.Component
class FormElementMapper( class FormElementMapper(
private val formOptionMapper: FormOptionMapper, private val formOptionMapper: FormOptionMapper,
private val visibilityConditionMapper: FormElementVisibilityConditionMapper, private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
) { ) {
fun toFormElementDto(formElement: FormElement): FormElementDto = fun toFormElementDto(formElement: FormElement): FormElementDto =
FormElementDto( FormElementDto(
id = formElement.id ?: throw IllegalStateException("FormElement ID must not be null!"), id = formElement.id,
reference = formElement.reference, reference = formElement.reference,
title = formElement.title, title = formElement.title,
description = formElement.description, description = formElement.description,
@@ -24,6 +24,11 @@ class FormElementMapper(
formElement.visibilityCondition?.let { formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityConditionDto(it) visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
}, },
sectionSpawnTrigger =
formElement.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
},
isClonable = formElement.isClonable,
) )
fun toFormElement( fun toFormElement(
@@ -42,10 +47,15 @@ class FormElementMapper(
formElement.visibilityCondition?.let { formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it) visibilityConditionMapper.toFormElementVisibilityCondition(it)
}, },
sectionSpawnTrigger =
formElement.sectionSpawnTrigger?.let {
spawnTriggerMapper.toSectionSpawnTrigger(it)
},
isClonable = formElement.isClonable ?: false,
) )
fun toFormElement( fun toNewFormElement(
formElement: CreateFormElementDto, formElement: FormElementDto,
formElementSubSection: FormElementSubSection, formElementSubSection: FormElementSubSection,
): FormElement = ): FormElement =
FormElement( FormElement(
@@ -60,5 +70,10 @@ class FormElementMapper(
formElement.visibilityCondition?.let { formElement.visibilityCondition?.let {
visibilityConditionMapper.toFormElementVisibilityCondition(it) 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 title: String,
var shortTitle: String? = null, var shortTitle: String? = null,
var description: 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) @OneToMany(mappedBy = "formElementSection", cascade = [CascadeType.ALL], orphanRemoval = true)
@OrderColumn(name = "form_element_sub_section_order") @OrderColumn(name = "form_element_sub_section_order")
var formElementSubSections: MutableList<FormElementSubSection> = mutableListOf(), var formElementSubSections: MutableList<FormElementSubSection> = mutableListOf(),

View File

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

View File

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

View File

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

View File

@@ -11,21 +11,21 @@ class FormElementVisibilityConditionMapper {
condition: FormElementVisibilityCondition, condition: FormElementVisibilityCondition,
): FormElementVisibilityConditionDto = ): FormElementVisibilityConditionDto =
FormElementVisibilityConditionDto( FormElementVisibilityConditionDto(
conditionType = toVisibilityConditionTypeDto(condition.conditionType), formElementConditionType = toVisibilityConditionTypeDto(condition.formElementConditionType),
sourceFormElementReference = condition.sourceFormElementReference, sourceFormElementReference = condition.sourceFormElementReference,
expectedValue = condition.expectedValue, formElementExpectedValue = condition.formElementExpectedValue,
operator = toVisibilityConditionOperatorDto(condition.operator), formElementOperator = toVisibilityConditionOperatorDto(condition.formElementOperator),
) )
fun toFormElementVisibilityCondition( fun toFormElementVisibilityCondition(
conditionDto: FormElementVisibilityConditionDto, conditionDto: FormElementVisibilityConditionDto,
): FormElementVisibilityCondition = ): FormElementVisibilityCondition =
FormElementVisibilityCondition( FormElementVisibilityCondition(
conditionType = toVisibilityConditionType(conditionDto.conditionType), formElementConditionType = toVisibilityConditionType(conditionDto.formElementConditionType),
sourceFormElementReference = conditionDto.sourceFormElementReference, sourceFormElementReference = conditionDto.sourceFormElementReference,
expectedValue = conditionDto.expectedValue, formElementExpectedValue = conditionDto.formElementExpectedValue,
operator = formElementOperator =
conditionDto.operator?.let { toVisibilityConditionOperator(it) } conditionDto.formElementOperator?.let { toVisibilityConditionOperator(it) }
?: VisibilityConditionOperator.EQUALS, ?: 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: springframework:
security: TRACE security: TRACE
oauth2: TRACE oauth2: TRACE
web: DEBUG web: TRACE
org.testcontainers: INFO org.testcontainers: INFO
com.github.dockerjava: WARN com.github.dockerjava: WARN

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@
</h1> </h1>
<UCard <UCard
v-for="subsection in visibleSubsections" v-for="{ subsection, sectionIndex } in visibleSubsections"
:key="subsection.id" :key="getSubsectionKey(currentFormElementSection, sectionIndex, subsection)"
variant="subtle" variant="subtle"
class="mb-6" class="mb-6"
> >
@@ -18,13 +18,24 @@
<h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2> <h2 class="text-lg font-semibold text-highlighted">{{ subsection.title }}</h2>
<p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p> <p v-if="subsection.subtitle" class="text-sm text-dimmed">{{ subsection.subtitle }}</p>
</div> </div>
<FormEngine <FormEngine
v-model="subsection.formElements" :model-value="subsection.formElements"
:visibility-map="visibilityMap" :visibility-map="visibilityMap"
:application-form-id="applicationFormId" :application-form-id="applicationFormId"
:disabled="disabled" :disabled="disabled"
@add:input-form="(position) => handleAddInputForm(position, subsection.id)" @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>
<UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6"> <UCard v-if="visibleSubsections.length === 0" variant="subtle" class="mb-6">
@@ -62,7 +73,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ApplicationFormDto, FormElementSectionDto } from '~~/.api-client' import type {
ApplicationFormDto,
FormElementSectionDto,
FormElementDto,
FormElementSubSectionDto
} from '~~/.api-client'
const props = defineProps<{ const props = defineProps<{
formElementSections: FormElementSectionDto[] formElementSections: FormElementSectionDto[]
@@ -75,6 +91,7 @@ const emit = defineEmits<{
save: [] save: []
submit: [] submit: []
'add-input-form': [updatedForm: ApplicationFormDto | undefined] 'add-input-form': [updatedForm: ApplicationFormDto | undefined]
'update:formElementSections': [sections: FormElementSectionDto[]]
navigate: [{ direction: 'forward' | 'backward'; index: number }] navigate: [{ direction: 'forward' | 'backward'; index: number }]
}>() }>()
@@ -82,16 +99,16 @@ const { stepper, activeStepperItemIndex, stepperItems, currentFormElementSection
computed(() => props.formElementSections) computed(() => props.formElementSections)
) )
const { evaluateVisibility } = useFormElementVisibility()
const { processSpawnTriggers } = useSectionSpawning()
const { cloneElement } = useClonableElements()
const allFormElements = computed(() => { const allFormElements = computed(() => {
return props.formElementSections.flatMap(section => return props.formElementSections.flatMap(
section.formElementSubSections?.flatMap(subsection => (section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? []
subsection.formElements
) ?? []
) )
}) })
const { evaluateVisibility } = useFormElementVisibility()
const visibilityMap = computed(() => { const visibilityMap = computed(() => {
return evaluateVisibility(allFormElements.value) return evaluateVisibility(allFormElements.value)
}) })
@@ -100,10 +117,20 @@ const visibleSubsections = computed(() => {
if (!currentFormElementSection.value?.formElementSubSections) { if (!currentFormElementSection.value?.formElementSubSections) {
return [] return []
} }
return currentFormElementSection.value.formElementSubSections.filter(subsection => { return currentFormElementSection.value.formElementSubSections
return subsection.formElements.some(element => visibilityMap.value.get(element.id) !== false) .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(() => { onMounted(() => {
@@ -112,27 +139,90 @@ onMounted(() => {
} }
}) })
async function handleAddInputForm(position: number, subsectionId: string) { async function handleAddInputForm(position: number, subsectionKey: string) {
const subsection = props.formElementSections const foundSubsection = findSubsectionByKey(subsectionKey)
.flatMap((section) => section.formElementSubSections) if (!foundSubsection) return
.find((sub) => sub.id === subsectionId)
if (!subsection) { const { addInputFormElement } = useFormElementManagement()
return 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
}
}
} }
return undefined
const { addFormElementToSubSection } = useFormElementManagement()
const updatedForm = await addFormElementToSubSection(
props.applicationFormId,
subsectionId,
subsection.formElements,
position
)
emit('add-input-form', updatedForm)
} }
async function handleNavigate(direction: 'forward' | 'backward') { async function handleNavigate(direction: 'forward' | 'backward') {
await navigateStepper(direction) await navigateStepper(direction)
emit('navigate', { direction, index: activeStepperItemIndex.value }) 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> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,3 +7,5 @@ export { useNotification } from './notification/useNotification'
export { useNotificationApi } from './notification/useNotificationApi' export { useNotificationApi } from './notification/useNotificationApi'
export { useUser } from './user/useUser' export { useUser } from './user/useUser'
export { useUserApi } from './user/useUserApi' 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> { ): Map<FormElementId, ComplianceStatus> {
formElementComplianceMap.value.clear() formElementComplianceMap.value.clear()
formElements.forEach((formElement) => { formElements.forEach((formElement, index) => {
if (visibilityMap && visibilityMap.get(formElement.id) === false) { const elementKey = formElement.id || formElement.reference || `element-${index}`
if (visibilityMap && visibilityMap.get(elementKey) === false) {
return return
} }
@@ -49,15 +51,15 @@ export function useApplicationFormValidator() {
const currentHighestComplianceStatusPos = const currentHighestComplianceStatusPos =
Object.values(ComplianceStatus).indexOf(currentHighestComplianceStatus) Object.values(ComplianceStatus).indexOf(currentHighestComplianceStatus)
if (formElementComplianceMap.value.has(formElement.id)) { if (formElementComplianceMap.value.has(elementKey)) {
const newComplianceStatus = formElementComplianceMap.value.get(formElement.id)! const newComplianceStatus = formElementComplianceMap.value.get(elementKey)!
const newComplianceStatusPos = Object.values(ComplianceStatus).indexOf(newComplianceStatus) const newComplianceStatusPos = Object.values(ComplianceStatus).indexOf(newComplianceStatus)
if (newComplianceStatusPos > currentHighestComplianceStatusPos) { if (newComplianceStatusPos > currentHighestComplianceStatusPos) {
formElementComplianceMap.value.set(formElement.id, newComplianceStatus) formElementComplianceMap.value.set(elementKey, newComplianceStatus)
} }
} else { } 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() { 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( function createInputFormElement(): FormElementDto {
applicationFormId: string | undefined, return {
subsectionId: string,
formElements: FormElementDto[],
position: number
): Promise<ApplicationFormDto | undefined> {
const inputFormElement: CreateFormElementDto = {
title: 'Formular ergänzen', title: 'Formular ergänzen',
description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.', description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.',
options: [ options: [
@@ -22,27 +22,9 @@ export function useFormElementManagement() {
], ],
type: 'TITLE_BODY_TEXTFIELDS' 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 { return {
addFormElementToSubSection addInputFormElement
} }
} }

View File

@@ -8,7 +8,10 @@ export function useFormElementVisibility() {
allFormElements.forEach((element) => { allFormElements.forEach((element) => {
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap) 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 return visibilityMap
@@ -42,10 +45,10 @@ export function useFormElementVisibility() {
const sourceValue = getFormElementValue(sourceElement) const sourceValue = getFormElementValue(sourceElement)
const operator = condition.operator || VCOperator.Equals const operator = condition.formElementOperator || VCOperator.Equals
const conditionMet = evaluateCondition(sourceValue, condition.expectedValue, operator) 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 { function getFormElementValue(element: FormElementDto): string {

View File

@@ -20,9 +20,11 @@ export function useFormStepper(
const sections = computed(() => toValue(formElementSections) ?? []) const sections = computed(() => toValue(formElementSections) ?? [])
const visibleSections = computed(() => sections.value.filter((section) => !section.isTemplate))
const stepperItems = computed(() => { const stepperItems = computed(() => {
const items: StepperItem[] = [] const items: StepperItem[] = []
sections.value.forEach((section: FormElementSectionDto) => { visibleSections.value.forEach((section: FormElementSectionDto) => {
items.push({ items.push({
title: section.shortTitle, title: section.shortTitle,
description: section.description description: section.description
@@ -32,7 +34,7 @@ export function useFormStepper(
}) })
const currentFormElementSection = computed<FormElementSectionDto | undefined>( const currentFormElementSection = computed<FormElementSectionDto | undefined>(
() => sections.value[activeStepperItemIndex.value] () => visibleSections.value[activeStepperItemIndex.value]
) )
async function navigateStepper(direction: 'forward' | 'backward') { 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( const userApiClient = new UserApi(new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }))
new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) })
)
async function getUserById(id: string): Promise<UserDto> { async function getUserById(id: string): Promise<UserDto> {
return userApiClient.getUserById({ id }) return userApiClient.getUserById({ id })
@@ -39,4 +37,3 @@ export function useUserApi() {
deleteUser deleteUser
} }
} }

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@
@save="onSave" @save="onSave"
@submit="onSubmit" @submit="onSubmit"
@add-input-form="handleAddInputForm" @add-input-form="handleAddInputForm"
@update:form-element-sections="handleFormElementSectionsUpdate"
> >
<UFormField :label="$t('common.name')" class="mb-4"> <UFormField :label="$t('common.name')" class="mb-4">
<UInput v-model="applicationFormTemplate.name" class="w-full" /> <UInput v-model="applicationFormTemplate.name" class="w-full" />
@@ -110,7 +111,7 @@ async function onSave() {
async function onSubmit() { async function onSubmit() {
const applicationForm = await prepareAndCreateApplicationForm() const applicationForm = await prepareAndCreateApplicationForm()
if (applicationForm) { if (applicationForm?.id) {
await submitApplicationForm(applicationForm.id) await submitApplicationForm(applicationForm.id)
await navigateTo('/') await navigateTo('/')
toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' }) toast.add({ title: $t('common.success'), description: $t('applicationForms.submitted'), color: 'success' })
@@ -123,6 +124,12 @@ function handleAddInputForm() {
// No action needed here // No action needed here
} }
function handleFormElementSectionsUpdate(sections: FormElementSectionDto[]) {
if (applicationFormTemplate.value) {
applicationFormTemplate.value.formElementSections = sections
}
}
async function prepareAndCreateApplicationForm() { async function prepareAndCreateApplicationForm() {
if (!applicationFormTemplate.value) { if (!applicationFormTemplate.value) {
console.error('Application form data is undefined') 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"> <div class="flex flex-col gap-4 sm:gap-6 w-full lg:max-w-4xl mx-auto p-4">
<UCard <UCard
v-for="(applicationFormElem, index) in applicationForms" 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" 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> <template #header>
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
@@ -63,6 +63,7 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UBadge <UBadge
v-if="applicationFormElem.status"
:label="applicationFormElem.status" :label="applicationFormElem.status"
:color="getStatusColor(applicationFormElem.status)" :color="getStatusColor(applicationFormElem.status)"
variant="subtle" variant="subtle"
@@ -87,8 +88,11 @@
<UIcon name="i-lucide-pencil" class="size-4 text-muted shrink-0" /> <UIcon name="i-lucide-pencil" class="size-4 text-muted shrink-0" />
<span class="text-muted"> <span class="text-muted">
{{ $t('applicationForms.lastEditedBy') }} {{ $t('applicationForms.lastEditedBy') }}
<span class="font-medium text-highlighted">{{ applicationFormElem.lastModifiedBy.name }}</span> <span class="font-medium text-highlighted">
{{ $t('common.on') }} {{ formatDate(applicationFormElem.modifiedAt) }} {{ applicationFormElem.lastModifiedBy?.name ?? '-' }}
</span>
{{ $t('common.on') }}
{{ applicationFormElem.modifiedAt ? formatDate(applicationFormElem.modifiedAt) : '-' }}
</span> </span>
</div> </div>
@@ -96,8 +100,11 @@
<UIcon name="i-lucide-user-plus" class="size-4 text-muted shrink-0" /> <UIcon name="i-lucide-user-plus" class="size-4 text-muted shrink-0" />
<span class="text-muted"> <span class="text-muted">
{{ $t('applicationForms.createdBy') }} {{ $t('applicationForms.createdBy') }}
<span class="font-medium text-highlighted">{{ applicationFormElem.createdBy.name }}</span> <span class="font-medium text-highlighted">
{{ $t('common.on') }} {{ formatDate(applicationFormElem.createdAt) }} {{ applicationFormElem.createdBy?.name ?? '-' }}
</span>
{{ $t('common.on') }}
{{ applicationFormElem.createdAt ? formatDate(applicationFormElem.createdAt) : '-' }}
</span> </span>
</div> </div>
</div> </div>
@@ -177,6 +184,9 @@ const applicationForms = computed({
}) })
function getLinksForApplicationForm(applicationForm: ApplicationFormDto) { function getLinksForApplicationForm(applicationForm: ApplicationFormDto) {
if (!applicationForm.id) {
return []
}
return [ return [
{ {
label: $t('common.delete'), label: $t('common.delete'),
@@ -206,4 +216,11 @@ async function deleteApplicationForm(applicationFormId: string) {
) )
isDeleteModalOpen.value = false isDeleteModalOpen.value = false
} }
function openApplicationForm(applicationFormId: string | null | undefined) {
if (!applicationFormId) {
return
}
navigateTo(`application-forms/${applicationFormId}/0`)
}
</script> </script>

View File

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

View File

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

View File

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

View File

@@ -63,10 +63,10 @@
], ],
"type": "DATE", "type": "DATE",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Einführung", "formElementExpectedValue": "Einführung",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -89,10 +89,10 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -109,10 +109,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt", "sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja", "formElementExpectedValue": "Ja",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -135,10 +135,10 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "testphase_findet_statt", "sourceFormElementReference": "testphase_findet_statt",
"expectedValue": "Ja", "formElementExpectedValue": "Ja",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -155,10 +155,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten", "sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein", "formElementExpectedValue": "Nein",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -175,10 +175,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten", "sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein", "formElementExpectedValue": "Nein",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -195,10 +195,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "verwendung_anonymisierter_daten", "sourceFormElementReference": "verwendung_anonymisierter_daten",
"expectedValue": "Nein", "formElementExpectedValue": "Nein",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -215,10 +215,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -235,10 +235,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Änderung IT-System", "formElementExpectedValue": "Änderung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
} }
] ]
@@ -261,10 +261,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -281,10 +281,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -301,10 +301,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
} }
] ]
@@ -327,10 +327,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -347,10 +347,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -367,10 +367,10 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -393,10 +393,10 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -425,10 +425,10 @@
], ],
"type": "SELECT", "type": "SELECT",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -451,10 +451,10 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -477,70 +477,36 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
"reference": "modul_1", "reference": "modul_1",
"title": "Modul 1", "title": "Modulname",
"description": "Beschreibung des ersten Moduls", "description": "Name des Moduls eingeben",
"options": [ "options": [
{ {
"value": "", "value": "",
"label": "Modul 1", "label": "Modulname",
"processingPurpose": "SYSTEM_OPERATION", "processingPurpose": "SYSTEM_OPERATION",
"employeeDataCategory": "NON_CRITICAL" "employeeDataCategory": "NON_CRITICAL"
} }
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"isClonable": true,
"sectionSpawnTrigger": {
"templateReference": "module_details_template",
"sectionSpawnConditionType": "SHOW",
"sectionSpawnOperator": "IS_NOT_EMPTY"
},
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "modulbasiertes_system", "sourceFormElementReference": "modulbasiertes_system",
"expectedValue": "Ja", "formElementExpectedValue": "Ja",
"operator": "EQUALS" "formElementOperator": "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"
} }
}, },
{ {
@@ -562,11 +528,18 @@
} }
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"isClonable": false,
"sectionSpawnTrigger": {
"templateReference": "ki_details_template",
"sectionSpawnConditionType": "SHOW",
"sectionSpawnExpectedValue": "Ja",
"sectionSpawnOperator": "EQUALS"
},
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -589,10 +562,10 @@
], ],
"type": "RADIOBUTTON", "type": "RADIOBUTTON",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "HIDE", "formElementConditionType": "HIDE",
"sourceFormElementReference": "art_der_massnahme", "sourceFormElementReference": "art_der_massnahme",
"expectedValue": "Ablösung/Einstellung IT-System", "formElementExpectedValue": "Ablösung/Einstellung IT-System",
"operator": "EQUALS" "formElementOperator": "EQUALS"
} }
}, },
{ {
@@ -609,15 +582,184 @@
], ],
"type": "TEXTAREA", "type": "TEXTAREA",
"visibilityCondition": { "visibilityCondition": {
"conditionType": "SHOW", "formElementConditionType": "SHOW",
"sourceFormElementReference": "wirtschaftliche_auswirkungen", "sourceFormElementReference": "wirtschaftliche_auswirkungen",
"expectedValue": "Ja", "formElementExpectedValue": "Ja",
"operator": "EQUALS" "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"
}
]
}
]
}
]
} }
] ]
} }