feat: Add table logic for role and permission sections
This commit is contained in:
@@ -1393,14 +1393,51 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
visibilityCondition:
|
visibilityConditions:
|
||||||
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
type: array
|
||||||
sectionSpawnTrigger:
|
items:
|
||||||
$ref: "#/components/schemas/SectionSpawnTriggerDto"
|
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
||||||
|
description: List of visibility conditions (all must be met for element to be visible - AND logic)
|
||||||
|
sectionSpawnTriggers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/SectionSpawnTriggerDto"
|
||||||
|
description: List of triggers that can spawn template sections when conditions are met
|
||||||
isClonable:
|
isClonable:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
description: If true, user can add more instances of this element
|
description: If true, user can add more instances of this element
|
||||||
|
tableRowPreset:
|
||||||
|
$ref: "#/components/schemas/TableRowPresetDto"
|
||||||
|
|
||||||
|
TableRowPresetDto:
|
||||||
|
type: object
|
||||||
|
description: Configuration for automatically creating table rows based on source table data
|
||||||
|
properties:
|
||||||
|
sourceTableReference:
|
||||||
|
type: string
|
||||||
|
description: Reference to source table element to pull rows from
|
||||||
|
filterCondition:
|
||||||
|
$ref: "#/components/schemas/TableColumnFilterDto"
|
||||||
|
columnMappings:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TableColumnMappingDto"
|
||||||
|
canAddRows:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: If true, users can manually add or remove rows. If false, rows are fully controlled by the source table.
|
||||||
|
|
||||||
|
TableColumnMappingDto:
|
||||||
|
type: object
|
||||||
|
description: Mapping between source and target columns for row presets
|
||||||
|
properties:
|
||||||
|
sourceColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Index of source column (0-based)
|
||||||
|
targetColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Index of target column (0-based)
|
||||||
|
|
||||||
FormElementSnapshotDto:
|
FormElementSnapshotDto:
|
||||||
type: object
|
type: object
|
||||||
@@ -1420,13 +1457,19 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/FormOptionDto"
|
$ref: "#/components/schemas/FormOptionDto"
|
||||||
visibilityCondition:
|
visibilityConditions:
|
||||||
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
type: array
|
||||||
sectionSpawnTrigger:
|
items:
|
||||||
$ref: "#/components/schemas/SectionSpawnTriggerDto"
|
$ref: "#/components/schemas/FormElementVisibilityCondition"
|
||||||
|
sectionSpawnTriggers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/SectionSpawnTriggerDto"
|
||||||
isClonable:
|
isClonable:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
tableRowPreset:
|
||||||
|
$ref: "#/components/schemas/TableRowPresetDto"
|
||||||
|
|
||||||
FormOptionDto:
|
FormOptionDto:
|
||||||
type: object
|
type: object
|
||||||
@@ -1444,6 +1487,66 @@ components:
|
|||||||
$ref: "#/components/schemas/ProcessingPurpose"
|
$ref: "#/components/schemas/ProcessingPurpose"
|
||||||
employeeDataCategory:
|
employeeDataCategory:
|
||||||
$ref: "#/components/schemas/EmployeeDataCategory"
|
$ref: "#/components/schemas/EmployeeDataCategory"
|
||||||
|
columnConfig:
|
||||||
|
$ref: "#/components/schemas/TableColumnConfigDto"
|
||||||
|
|
||||||
|
TableColumnConfigDto:
|
||||||
|
type: object
|
||||||
|
description: Configuration for table column cross-references
|
||||||
|
properties:
|
||||||
|
sourceTableReference:
|
||||||
|
type: string
|
||||||
|
description: Reference to source table element to get values from
|
||||||
|
sourceColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Index of source column to reference (0-based)
|
||||||
|
filterCondition:
|
||||||
|
$ref: "#/components/schemas/TableColumnFilterDto"
|
||||||
|
rowConstraint:
|
||||||
|
$ref: "#/components/schemas/TableRowConstraintDto"
|
||||||
|
isReadOnly:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: If true, column values cannot be edited by user
|
||||||
|
isMultipleAllowed:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: If true, allows selecting multiple values in this column
|
||||||
|
isCheckbox:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: If true, renders a checkbox instead of text input
|
||||||
|
|
||||||
|
TableRowConstraintDto:
|
||||||
|
type: object
|
||||||
|
description: Configuration for row-based value constraints from another table
|
||||||
|
properties:
|
||||||
|
constraintTableReference:
|
||||||
|
type: string
|
||||||
|
description: Reference to the constraint table that defines allowed value mappings
|
||||||
|
constraintKeyColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Column index in constraint table that matches the key (e.g., Rollen-ID)
|
||||||
|
constraintValueColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Column index in constraint table with allowed values (e.g., Permission-ID)
|
||||||
|
currentRowKeyColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Column index in current table row to use as the lookup key
|
||||||
|
|
||||||
|
TableColumnFilterDto:
|
||||||
|
type: object
|
||||||
|
description: Filter condition for constraining available values
|
||||||
|
properties:
|
||||||
|
sourceColumnIndex:
|
||||||
|
type: integer
|
||||||
|
description: Index of source column to check for filter condition
|
||||||
|
expectedValue:
|
||||||
|
type: string
|
||||||
|
description: Expected value to match in the source column
|
||||||
|
operator:
|
||||||
|
$ref: "#/components/schemas/VisibilityConditionOperator"
|
||||||
|
default: EQUALS
|
||||||
|
|
||||||
FormElementType:
|
FormElementType:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotD
|
|||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementVisibilityCondition
|
||||||
import com.fasterxml.jackson.core.type.TypeReference
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -80,6 +81,7 @@ class ApplicationFormFormatService(
|
|||||||
title = LatexEscaper.escape(element.title ?: ""),
|
title = LatexEscaper.escape(element.title ?: ""),
|
||||||
description = LatexEscaper.escape(element.description ?: ""),
|
description = LatexEscaper.escape(element.description ?: ""),
|
||||||
value = renderElementValue(element),
|
value = renderElementValue(element),
|
||||||
|
isTable = element.type.name == "TABLE",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -152,8 +154,10 @@ class ApplicationFormFormatService(
|
|||||||
val rowCount = columnData.maxOfOrNull { col -> col.size } ?: 0
|
val rowCount = columnData.maxOfOrNull { col -> col.size } ?: 0
|
||||||
if (rowCount == 0) return "Keine Daten"
|
if (rowCount == 0) return "Keine Daten"
|
||||||
|
|
||||||
val columnSpec = headers.joinToString(" | ") { "l" }
|
// Use tabularx with Y columns (auto-wrapping) for flexible width distribution
|
||||||
val headerRow = headers.joinToString(" & ")
|
// Y is defined as >{\raggedright\arraybackslash}X in the template
|
||||||
|
val columnSpec = headers.joinToString("") { "Y" }
|
||||||
|
val headerRow = headers.joinToString(" & ") { "\\textbf{$it}" }
|
||||||
val dataRows =
|
val dataRows =
|
||||||
(0 until rowCount).map { rowIndex ->
|
(0 until rowCount).map { rowIndex ->
|
||||||
columnData.joinToString(" & ") { col: List<String> ->
|
columnData.joinToString(" & ") { col: List<String> ->
|
||||||
@@ -163,15 +167,15 @@ class ApplicationFormFormatService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return buildString {
|
return buildString {
|
||||||
appendLine("\\begin{tabular}{$columnSpec}")
|
appendLine("\\begin{tabularx}{\\textwidth}{$columnSpec}")
|
||||||
appendLine("\\hline")
|
appendLine("\\toprule")
|
||||||
appendLine("$headerRow \\\\")
|
appendLine("$headerRow \\\\")
|
||||||
appendLine("\\hline")
|
appendLine("\\midrule")
|
||||||
dataRows.forEach { row: String ->
|
dataRows.forEach { row: String ->
|
||||||
appendLine("$row \\\\")
|
appendLine("$row \\\\")
|
||||||
}
|
}
|
||||||
appendLine("\\hline")
|
appendLine("\\bottomrule")
|
||||||
appendLine("\\end{tabular}")
|
appendLine("\\end{tabularx}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +217,17 @@ class ApplicationFormFormatService(
|
|||||||
element: FormElementSnapshotDto,
|
element: FormElementSnapshotDto,
|
||||||
formElementsByRef: Map<String, FormElementSnapshotDto>,
|
formElementsByRef: Map<String, FormElementSnapshotDto>,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val condition = element.visibilityCondition ?: return true
|
val conditions = element.visibilityConditions
|
||||||
|
if (conditions.isNullOrEmpty()) return true
|
||||||
|
|
||||||
|
// All conditions must be met (AND logic)
|
||||||
|
return conditions.all { condition -> evaluateSingleCondition(condition, formElementsByRef) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateSingleCondition(
|
||||||
|
condition: FormElementVisibilityCondition,
|
||||||
|
formElementsByRef: Map<String, FormElementSnapshotDto>,
|
||||||
|
): Boolean {
|
||||||
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
|
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
|
||||||
val sourceValue = getFormElementValue(sourceElement)
|
val sourceValue = getFormElementValue(sourceElement)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package com.betriebsratkanzlei.legalconsenthub.application_form.export.latex
|
|||||||
object LatexEscaper {
|
object LatexEscaper {
|
||||||
fun escape(text: String?): String {
|
fun escape(text: String?): String {
|
||||||
if (text == null) return ""
|
if (text == null) return ""
|
||||||
return text
|
// First decode common HTML entities that may be present in user input
|
||||||
|
val decoded = decodeHtmlEntities(text)
|
||||||
|
// Then escape for LaTeX
|
||||||
|
return decoded
|
||||||
.replace("\\", "\\textbackslash{}")
|
.replace("\\", "\\textbackslash{}")
|
||||||
.replace("{", "\\{")
|
.replace("{", "\\{")
|
||||||
.replace("}", "\\}")
|
.replace("}", "\\}")
|
||||||
@@ -16,4 +19,14 @@ object LatexEscaper {
|
|||||||
.replace("~", "\\textasciitilde{}")
|
.replace("~", "\\textasciitilde{}")
|
||||||
.replace("\n", "\\\\")
|
.replace("\n", "\\\\")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeHtmlEntities(text: String): String =
|
||||||
|
text
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace(""", "\"")
|
||||||
|
.replace("'", "'")
|
||||||
|
.replace("'", "'")
|
||||||
|
.replace(" ", " ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ data class LatexFormElement(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val value: String,
|
val value: String,
|
||||||
|
val isTable: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ 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.FormElementVisibilityConditionMapper
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementVisibilityConditionMapper
|
||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.FormOption
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormOptionMapper
|
||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper
|
import com.betriebsratkanzlei.legalconsenthub.form_element.SectionSpawnTriggerMapper
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.form_element.TableRowPresetMapper
|
||||||
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
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormOptionDto
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -28,6 +28,8 @@ class ApplicationFormVersionService(
|
|||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
|
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
|
||||||
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
|
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
|
||||||
|
private val tableRowPresetMapper: TableRowPresetMapper,
|
||||||
|
private val formOptionMapper: FormOptionMapper,
|
||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createVersion(
|
fun createVersion(
|
||||||
@@ -123,26 +125,22 @@ class ApplicationFormVersionService(
|
|||||||
title = element.title,
|
title = element.title,
|
||||||
description = element.description,
|
description = element.description,
|
||||||
type = element.type,
|
type = element.type,
|
||||||
options =
|
options = element.options.map { formOptionMapper.toFormOptionDto(it) },
|
||||||
element.options.map { option ->
|
visibilityConditions =
|
||||||
FormOptionDto(
|
element.visibilityConditions
|
||||||
value = option.value,
|
.map {
|
||||||
label = option.label,
|
visibilityConditionMapper
|
||||||
processingPurpose = option.processingPurpose,
|
.toFormElementVisibilityConditionDto(it)
|
||||||
employeeDataCategory = option.employeeDataCategory,
|
},
|
||||||
)
|
sectionSpawnTriggers =
|
||||||
},
|
element.sectionSpawnTriggers.map {
|
||||||
visibilityCondition =
|
|
||||||
element.visibilityCondition?.let {
|
|
||||||
visibilityConditionMapper.toFormElementVisibilityConditionDto(
|
|
||||||
it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
sectionSpawnTrigger =
|
|
||||||
element.sectionSpawnTrigger?.let {
|
|
||||||
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
|
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
|
||||||
},
|
},
|
||||||
isClonable = element.isClonable,
|
isClonable = element.isClonable,
|
||||||
|
tableRowPreset =
|
||||||
|
element.tableRowPreset?.let {
|
||||||
|
tableRowPresetMapper.toTableRowPresetDto(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -185,23 +183,23 @@ class ApplicationFormVersionService(
|
|||||||
formElementSubSection = subsection,
|
formElementSubSection = subsection,
|
||||||
options =
|
options =
|
||||||
elementSnapshot.options
|
elementSnapshot.options
|
||||||
.map { optionDto ->
|
.map { formOptionMapper.toFormOption(it) }
|
||||||
FormOption(
|
.toMutableList(),
|
||||||
value = optionDto.value,
|
visibilityConditions =
|
||||||
label = optionDto.label,
|
elementSnapshot.visibilityConditions
|
||||||
processingPurpose = optionDto.processingPurpose,
|
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
|
||||||
employeeDataCategory = optionDto.employeeDataCategory,
|
?.toMutableList()
|
||||||
)
|
?: mutableListOf(),
|
||||||
}.toMutableList(),
|
sectionSpawnTriggers =
|
||||||
visibilityCondition =
|
elementSnapshot.sectionSpawnTriggers
|
||||||
elementSnapshot.visibilityCondition?.let {
|
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
|
||||||
visibilityConditionMapper.toFormElementVisibilityCondition(it)
|
?.toMutableList()
|
||||||
},
|
?: mutableListOf(),
|
||||||
sectionSpawnTrigger =
|
|
||||||
elementSnapshot.sectionSpawnTrigger?.let {
|
|
||||||
spawnTriggerMapper.toSectionSpawnTrigger(it)
|
|
||||||
},
|
|
||||||
isClonable = elementSnapshot.isClonable ?: false,
|
isClonable = elementSnapshot.isClonable ?: false,
|
||||||
|
tableRowPreset =
|
||||||
|
elementSnapshot.tableRowPreset?.let {
|
||||||
|
tableRowPresetMapper.toTableRowPreset(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
subsection.formElements.add(element)
|
subsection.formElements.add(element)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.form_element
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
|
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementType
|
||||||
|
import jakarta.persistence.AttributeOverride
|
||||||
|
import jakarta.persistence.AttributeOverrides
|
||||||
import jakarta.persistence.CollectionTable
|
import jakarta.persistence.CollectionTable
|
||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.ElementCollection
|
import jakarta.persistence.ElementCollection
|
||||||
@@ -28,9 +30,25 @@ 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
|
@ElementCollection
|
||||||
var visibilityCondition: FormElementVisibilityCondition? = null,
|
@CollectionTable(name = "visibility_conditions", joinColumns = [JoinColumn(name = "form_element_id")])
|
||||||
@Embedded
|
var visibilityConditions: MutableList<FormElementVisibilityCondition> = mutableListOf(),
|
||||||
var sectionSpawnTrigger: SectionSpawnTrigger? = null,
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "section_spawn_triggers", joinColumns = [JoinColumn(name = "form_element_id")])
|
||||||
|
var sectionSpawnTriggers: MutableList<SectionSpawnTrigger> = mutableListOf(),
|
||||||
var isClonable: Boolean = false,
|
var isClonable: Boolean = false,
|
||||||
|
@Embedded
|
||||||
|
@AttributeOverrides(
|
||||||
|
AttributeOverride(name = "sourceTableReference", column = Column(name = "row_preset_source_table_ref")),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "filterCondition.sourceColumnIndex",
|
||||||
|
column = Column(name = "row_preset_filter_src_col_idx"),
|
||||||
|
),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "filterCondition.expectedValue",
|
||||||
|
column = Column(name = "row_preset_filter_expected_val"),
|
||||||
|
),
|
||||||
|
AttributeOverride(name = "filterCondition.operator", column = Column(name = "row_preset_filter_operator")),
|
||||||
|
)
|
||||||
|
var tableRowPreset: TableRowPreset? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class FormElementMapper(
|
|||||||
private val formOptionMapper: FormOptionMapper,
|
private val formOptionMapper: FormOptionMapper,
|
||||||
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
|
private val visibilityConditionMapper: FormElementVisibilityConditionMapper,
|
||||||
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
|
private val spawnTriggerMapper: SectionSpawnTriggerMapper,
|
||||||
|
private val tableRowPresetMapper: TableRowPresetMapper,
|
||||||
) {
|
) {
|
||||||
fun toFormElementDto(formElement: FormElement): FormElementDto =
|
fun toFormElementDto(formElement: FormElement): FormElementDto =
|
||||||
FormElementDto(
|
FormElementDto(
|
||||||
@@ -20,15 +21,19 @@ class FormElementMapper(
|
|||||||
formElementSubSectionId =
|
formElementSubSectionId =
|
||||||
formElement.formElementSubSection?.id
|
formElement.formElementSubSection?.id
|
||||||
?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
|
?: throw IllegalStateException("FormElementSubSection ID must not be null!"),
|
||||||
visibilityCondition =
|
visibilityConditions =
|
||||||
formElement.visibilityCondition?.let {
|
formElement.visibilityConditions.map {
|
||||||
visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
|
visibilityConditionMapper.toFormElementVisibilityConditionDto(it)
|
||||||
},
|
},
|
||||||
sectionSpawnTrigger =
|
sectionSpawnTriggers =
|
||||||
formElement.sectionSpawnTrigger?.let {
|
formElement.sectionSpawnTriggers.map {
|
||||||
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
|
spawnTriggerMapper.toSectionSpawnTriggerDto(it)
|
||||||
},
|
},
|
||||||
isClonable = formElement.isClonable,
|
isClonable = formElement.isClonable,
|
||||||
|
tableRowPreset =
|
||||||
|
formElement.tableRowPreset?.let {
|
||||||
|
tableRowPresetMapper.toTableRowPresetDto(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toFormElement(
|
fun toFormElement(
|
||||||
@@ -43,15 +48,21 @@ class FormElementMapper(
|
|||||||
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
||||||
type = formElement.type,
|
type = formElement.type,
|
||||||
formElementSubSection = formElementSubSection,
|
formElementSubSection = formElementSubSection,
|
||||||
visibilityCondition =
|
visibilityConditions =
|
||||||
formElement.visibilityCondition?.let {
|
formElement.visibilityConditions
|
||||||
visibilityConditionMapper.toFormElementVisibilityCondition(it)
|
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
|
||||||
},
|
?.toMutableList()
|
||||||
sectionSpawnTrigger =
|
?: mutableListOf(),
|
||||||
formElement.sectionSpawnTrigger?.let {
|
sectionSpawnTriggers =
|
||||||
spawnTriggerMapper.toSectionSpawnTrigger(it)
|
formElement.sectionSpawnTriggers
|
||||||
},
|
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
|
||||||
|
?.toMutableList()
|
||||||
|
?: mutableListOf(),
|
||||||
isClonable = formElement.isClonable ?: false,
|
isClonable = formElement.isClonable ?: false,
|
||||||
|
tableRowPreset =
|
||||||
|
formElement.tableRowPreset?.let {
|
||||||
|
tableRowPresetMapper.toTableRowPreset(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toNewFormElement(
|
fun toNewFormElement(
|
||||||
@@ -66,14 +77,20 @@ class FormElementMapper(
|
|||||||
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
||||||
type = formElement.type,
|
type = formElement.type,
|
||||||
formElementSubSection = formElementSubSection,
|
formElementSubSection = formElementSubSection,
|
||||||
visibilityCondition =
|
visibilityConditions =
|
||||||
formElement.visibilityCondition?.let {
|
formElement.visibilityConditions
|
||||||
visibilityConditionMapper.toFormElementVisibilityCondition(it)
|
?.map { visibilityConditionMapper.toFormElementVisibilityCondition(it) }
|
||||||
},
|
?.toMutableList()
|
||||||
sectionSpawnTrigger =
|
?: mutableListOf(),
|
||||||
formElement.sectionSpawnTrigger?.let {
|
sectionSpawnTriggers =
|
||||||
spawnTriggerMapper.toSectionSpawnTrigger(it)
|
formElement.sectionSpawnTriggers
|
||||||
},
|
?.map { spawnTriggerMapper.toSectionSpawnTrigger(it) }
|
||||||
|
?.toMutableList()
|
||||||
|
?: mutableListOf(),
|
||||||
isClonable = formElement.isClonable ?: false,
|
isClonable = formElement.isClonable ?: false,
|
||||||
|
tableRowPreset =
|
||||||
|
formElement.tableRowPreset?.let {
|
||||||
|
tableRowPresetMapper.toTableRowPreset(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package com.betriebsratkanzlei.legalconsenthub.form_element
|
|||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.EmployeeDataCategory
|
import com.betriebsratkanzlei.legalconsenthub_api.model.EmployeeDataCategory
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ProcessingPurpose
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ProcessingPurpose
|
||||||
|
import jakarta.persistence.AttributeOverride
|
||||||
|
import jakarta.persistence.AttributeOverrides
|
||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.Embeddable
|
import jakarta.persistence.Embeddable
|
||||||
|
import jakarta.persistence.Embedded
|
||||||
|
|
||||||
@Embeddable
|
@Embeddable
|
||||||
class FormOption(
|
class FormOption(
|
||||||
@@ -15,4 +18,21 @@ class FormOption(
|
|||||||
var processingPurpose: ProcessingPurpose,
|
var processingPurpose: ProcessingPurpose,
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var employeeDataCategory: EmployeeDataCategory,
|
var employeeDataCategory: EmployeeDataCategory,
|
||||||
|
@Embedded
|
||||||
|
@AttributeOverrides(
|
||||||
|
AttributeOverride(name = "sourceTableReference", column = Column(name = "col_config_source_table_ref")),
|
||||||
|
AttributeOverride(name = "sourceColumnIndex", column = Column(name = "col_config_source_col_idx")),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "filterCondition.sourceColumnIndex",
|
||||||
|
column = Column(name = "col_config_filter_src_col_idx"),
|
||||||
|
),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "filterCondition.expectedValue",
|
||||||
|
column = Column(name = "col_config_filter_expected_val"),
|
||||||
|
),
|
||||||
|
AttributeOverride(name = "filterCondition.operator", column = Column(name = "col_config_filter_operator")),
|
||||||
|
AttributeOverride(name = "isReadOnly", column = Column(name = "col_config_is_read_only")),
|
||||||
|
AttributeOverride(name = "isCheckbox", column = Column(name = "col_config_is_checkbox")),
|
||||||
|
)
|
||||||
|
var columnConfig: TableColumnConfig? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.FormOptionDto
|
|||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class FormOptionMapper {
|
class FormOptionMapper(
|
||||||
|
private val columnConfigMapper: TableColumnConfigMapper,
|
||||||
|
) {
|
||||||
fun toFormOptionDto(formOption: FormOption): FormOptionDto =
|
fun toFormOptionDto(formOption: FormOption): FormOptionDto =
|
||||||
FormOptionDto(
|
FormOptionDto(
|
||||||
value = formOption.value,
|
value = formOption.value,
|
||||||
label = formOption.label,
|
label = formOption.label,
|
||||||
processingPurpose = formOption.processingPurpose,
|
processingPurpose = formOption.processingPurpose,
|
||||||
employeeDataCategory = formOption.employeeDataCategory,
|
employeeDataCategory = formOption.employeeDataCategory,
|
||||||
|
columnConfig = formOption.columnConfig?.let { columnConfigMapper.toTableColumnConfigDto(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toFormOption(formOptionDto: FormOptionDto): FormOption =
|
fun toFormOption(formOptionDto: FormOptionDto): FormOption =
|
||||||
@@ -19,5 +22,6 @@ class FormOptionMapper {
|
|||||||
label = formOptionDto.label,
|
label = formOptionDto.label,
|
||||||
processingPurpose = formOptionDto.processingPurpose,
|
processingPurpose = formOptionDto.processingPurpose,
|
||||||
employeeDataCategory = formOptionDto.employeeDataCategory,
|
employeeDataCategory = formOptionDto.employeeDataCategory,
|
||||||
|
columnConfig = formOptionDto.columnConfig?.let { columnConfigMapper.toTableColumnConfig(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import jakarta.persistence.AttributeOverride
|
||||||
|
import jakarta.persistence.AttributeOverrides
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
import jakarta.persistence.Embedded
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class TableColumnConfig(
|
||||||
|
val sourceTableReference: String? = null,
|
||||||
|
val sourceColumnIndex: Int? = null,
|
||||||
|
@Embedded
|
||||||
|
val filterCondition: TableColumnFilter? = null,
|
||||||
|
@Embedded
|
||||||
|
@AttributeOverrides(
|
||||||
|
AttributeOverride(name = "constraintTableReference", column = Column(name = "row_constraint_table_reference")),
|
||||||
|
AttributeOverride(name = "constraintKeyColumnIndex", column = Column(name = "row_constraint_key_column_index")),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "constraintValueColumnIndex",
|
||||||
|
column = Column(name = "row_constraint_value_column_index"),
|
||||||
|
),
|
||||||
|
AttributeOverride(
|
||||||
|
name = "currentRowKeyColumnIndex",
|
||||||
|
column = Column(name = "row_constraint_current_row_key_column_index"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val rowConstraint: TableRowConstraint? = null,
|
||||||
|
val isReadOnly: Boolean = false,
|
||||||
|
val isMultipleAllowed: Boolean = false,
|
||||||
|
val isCheckbox: Boolean = false,
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnConfigDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TableColumnConfigMapper(
|
||||||
|
private val filterMapper: TableColumnFilterMapper,
|
||||||
|
private val rowConstraintMapper: TableRowConstraintMapper,
|
||||||
|
) {
|
||||||
|
fun toTableColumnConfigDto(config: TableColumnConfig): TableColumnConfigDto =
|
||||||
|
TableColumnConfigDto(
|
||||||
|
sourceTableReference = config.sourceTableReference,
|
||||||
|
sourceColumnIndex = config.sourceColumnIndex,
|
||||||
|
filterCondition = config.filterCondition?.let { filterMapper.toTableColumnFilterDto(it) },
|
||||||
|
rowConstraint = config.rowConstraint?.let { rowConstraintMapper.toTableRowConstraintDto(it) },
|
||||||
|
isReadOnly = config.isReadOnly,
|
||||||
|
isMultipleAllowed = config.isMultipleAllowed,
|
||||||
|
isCheckbox = config.isCheckbox,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toTableColumnConfig(dto: TableColumnConfigDto): TableColumnConfig =
|
||||||
|
TableColumnConfig(
|
||||||
|
sourceTableReference = dto.sourceTableReference,
|
||||||
|
sourceColumnIndex = dto.sourceColumnIndex,
|
||||||
|
filterCondition = dto.filterCondition?.let { filterMapper.toTableColumnFilter(it) },
|
||||||
|
rowConstraint = dto.rowConstraint?.let { rowConstraintMapper.toTableRowConstraint(it) },
|
||||||
|
isReadOnly = dto.isReadOnly ?: false,
|
||||||
|
isMultipleAllowed = dto.isMultipleAllowed ?: false,
|
||||||
|
isCheckbox = dto.isCheckbox ?: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
import jakarta.persistence.EnumType
|
||||||
|
import jakarta.persistence.Enumerated
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class TableColumnFilter(
|
||||||
|
val sourceColumnIndex: Int? = null,
|
||||||
|
val expectedValue: String? = null,
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
val operator: VisibilityConditionOperator = VisibilityConditionOperator.EQUALS,
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnFilterDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TableColumnFilterMapper {
|
||||||
|
fun toTableColumnFilterDto(filter: TableColumnFilter): TableColumnFilterDto =
|
||||||
|
TableColumnFilterDto(
|
||||||
|
sourceColumnIndex = filter.sourceColumnIndex,
|
||||||
|
expectedValue = filter.expectedValue,
|
||||||
|
operator = filter.operator.toDto(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toTableColumnFilter(dto: TableColumnFilterDto): TableColumnFilter =
|
||||||
|
TableColumnFilter(
|
||||||
|
sourceColumnIndex = dto.sourceColumnIndex,
|
||||||
|
expectedValue = dto.expectedValue,
|
||||||
|
operator = dto.operator?.toEntity() ?: VisibilityConditionOperator.EQUALS,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun VisibilityConditionOperator.toDto(): VisibilityConditionOperatorDto =
|
||||||
|
when (this) {
|
||||||
|
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 VisibilityConditionOperatorDto.toEntity(): VisibilityConditionOperator =
|
||||||
|
when (this) {
|
||||||
|
VisibilityConditionOperatorDto.EQUALS -> VisibilityConditionOperator.EQUALS
|
||||||
|
VisibilityConditionOperatorDto.NOT_EQUALS -> VisibilityConditionOperator.NOT_EQUALS
|
||||||
|
VisibilityConditionOperatorDto.IS_EMPTY -> VisibilityConditionOperator.IS_EMPTY
|
||||||
|
VisibilityConditionOperatorDto.IS_NOT_EMPTY -> VisibilityConditionOperator.IS_NOT_EMPTY
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class TableColumnMapping(
|
||||||
|
val sourceColumnIndex: Int,
|
||||||
|
val targetColumnIndex: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.TableColumnMappingDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TableColumnMappingMapper {
|
||||||
|
fun toTableColumnMappingDto(mapping: TableColumnMapping): TableColumnMappingDto =
|
||||||
|
TableColumnMappingDto(
|
||||||
|
sourceColumnIndex = mapping.sourceColumnIndex,
|
||||||
|
targetColumnIndex = mapping.targetColumnIndex,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toTableColumnMapping(dto: TableColumnMappingDto): TableColumnMapping =
|
||||||
|
TableColumnMapping(
|
||||||
|
sourceColumnIndex = dto.sourceColumnIndex ?: 0,
|
||||||
|
targetColumnIndex = dto.targetColumnIndex ?: 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class TableRowConstraint(
|
||||||
|
val constraintTableReference: String? = null,
|
||||||
|
val constraintKeyColumnIndex: Int? = null,
|
||||||
|
val constraintValueColumnIndex: Int? = null,
|
||||||
|
val currentRowKeyColumnIndex: Int? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowConstraintDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TableRowConstraintMapper {
|
||||||
|
fun toTableRowConstraintDto(constraint: TableRowConstraint): TableRowConstraintDto =
|
||||||
|
TableRowConstraintDto(
|
||||||
|
constraintTableReference = constraint.constraintTableReference,
|
||||||
|
constraintKeyColumnIndex = constraint.constraintKeyColumnIndex,
|
||||||
|
constraintValueColumnIndex = constraint.constraintValueColumnIndex,
|
||||||
|
currentRowKeyColumnIndex = constraint.currentRowKeyColumnIndex,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toTableRowConstraint(dto: TableRowConstraintDto): TableRowConstraint =
|
||||||
|
TableRowConstraint(
|
||||||
|
constraintTableReference = dto.constraintTableReference,
|
||||||
|
constraintKeyColumnIndex = dto.constraintKeyColumnIndex,
|
||||||
|
constraintValueColumnIndex = dto.constraintValueColumnIndex,
|
||||||
|
currentRowKeyColumnIndex = dto.currentRowKeyColumnIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import jakarta.persistence.CollectionTable
|
||||||
|
import jakarta.persistence.ElementCollection
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
import jakarta.persistence.Embedded
|
||||||
|
import jakarta.persistence.JoinColumn
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class TableRowPreset(
|
||||||
|
val sourceTableReference: String? = null,
|
||||||
|
@Embedded
|
||||||
|
val filterCondition: TableColumnFilter? = null,
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "table_column_mappings", joinColumns = [JoinColumn(name = "form_element_id")])
|
||||||
|
val columnMappings: MutableList<TableColumnMapping> = mutableListOf(),
|
||||||
|
val canAddRows: Boolean? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.form_element
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.TableRowPresetDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TableRowPresetMapper(
|
||||||
|
private val filterMapper: TableColumnFilterMapper,
|
||||||
|
private val mappingMapper: TableColumnMappingMapper,
|
||||||
|
) {
|
||||||
|
fun toTableRowPresetDto(preset: TableRowPreset): TableRowPresetDto =
|
||||||
|
TableRowPresetDto(
|
||||||
|
sourceTableReference = preset.sourceTableReference,
|
||||||
|
filterCondition = preset.filterCondition?.let { filterMapper.toTableColumnFilterDto(it) },
|
||||||
|
columnMappings = preset.columnMappings.map { mappingMapper.toTableColumnMappingDto(it) },
|
||||||
|
canAddRows = preset.canAddRows ?: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toTableRowPreset(dto: TableRowPresetDto): TableRowPreset =
|
||||||
|
TableRowPreset(
|
||||||
|
sourceTableReference = dto.sourceTableReference,
|
||||||
|
filterCondition = dto.filterCondition?.let { filterMapper.toTableColumnFilter(it) },
|
||||||
|
columnMappings =
|
||||||
|
dto.columnMappings?.map { mappingMapper.toTableColumnMapping(it) }?.toMutableList() ?: mutableListOf(),
|
||||||
|
canAddRows = dto.canAddRows,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -53,33 +53,43 @@ create table comment
|
|||||||
|
|
||||||
create table form_element_options
|
create table form_element_options
|
||||||
(
|
(
|
||||||
employee_data_category smallint not null check (employee_data_category between 0 and 3),
|
col_config_filter_src_col_idx integer,
|
||||||
processing_purpose smallint not null check (processing_purpose between 0 and 3),
|
col_config_is_checkbox boolean,
|
||||||
form_element_id uuid not null,
|
col_config_is_read_only boolean,
|
||||||
label varchar(255) not null,
|
col_config_source_col_idx integer,
|
||||||
option_value TEXT not null
|
employee_data_category smallint not null check (employee_data_category between 0 and 3),
|
||||||
|
is_multiple_allowed boolean,
|
||||||
|
processing_purpose smallint not null check (processing_purpose between 0 and 3),
|
||||||
|
row_constraint_current_row_key_column_index integer,
|
||||||
|
row_constraint_key_column_index integer,
|
||||||
|
row_constraint_value_column_index integer,
|
||||||
|
form_element_id uuid not null,
|
||||||
|
col_config_filter_expected_val varchar(255),
|
||||||
|
col_config_filter_operator varchar(255) check (col_config_filter_operator in
|
||||||
|
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY',
|
||||||
|
'IS_NOT_EMPTY')),
|
||||||
|
col_config_source_table_ref varchar(255),
|
||||||
|
label varchar(255) not null,
|
||||||
|
option_value TEXT not null,
|
||||||
|
row_constraint_table_reference varchar(255)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table form_element
|
create table form_element
|
||||||
(
|
(
|
||||||
form_element_order integer,
|
can_add_rows boolean,
|
||||||
is_clonable boolean not null,
|
form_element_order integer,
|
||||||
type smallint not null check (type between 0 and 7),
|
is_clonable boolean not null,
|
||||||
form_element_sub_section_id uuid not null,
|
row_preset_filter_src_col_idx integer,
|
||||||
id uuid not null,
|
type smallint not null check (type between 0 and 8),
|
||||||
description varchar(255),
|
form_element_sub_section_id uuid not null,
|
||||||
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')),
|
id uuid not null,
|
||||||
form_element_expected_value varchar(255),
|
description varchar(255),
|
||||||
form_element_operator varchar(255) check (form_element_operator in
|
reference varchar(255),
|
||||||
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
|
row_preset_filter_expected_val varchar(255),
|
||||||
reference varchar(255),
|
row_preset_filter_operator varchar(255) check (row_preset_filter_operator in
|
||||||
section_spawn_condition_type varchar(255) check (section_spawn_condition_type in ('SHOW', 'HIDE')),
|
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
|
||||||
section_spawn_expected_value varchar(255),
|
row_preset_source_table_ref varchar(255),
|
||||||
section_spawn_operator varchar(255) check (section_spawn_operator in
|
title varchar(255),
|
||||||
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
|
|
||||||
source_form_element_reference varchar(255),
|
|
||||||
template_reference varchar(255),
|
|
||||||
title varchar(255),
|
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,6 +132,33 @@ create table notification
|
|||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table section_spawn_triggers
|
||||||
|
(
|
||||||
|
form_element_id uuid not null,
|
||||||
|
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')),
|
||||||
|
template_reference varchar(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table table_column_mappings
|
||||||
|
(
|
||||||
|
source_column_index integer,
|
||||||
|
target_column_index integer,
|
||||||
|
form_element_id uuid not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table visibility_conditions
|
||||||
|
(
|
||||||
|
form_element_id uuid not null,
|
||||||
|
form_element_condition_type varchar(255) check (form_element_condition_type in ('SHOW', 'HIDE')),
|
||||||
|
form_element_expected_value varchar(255),
|
||||||
|
form_element_operator varchar(255) check (form_element_operator in
|
||||||
|
('EQUALS', 'NOT_EQUALS', 'IS_EMPTY', 'IS_NOT_EMPTY')),
|
||||||
|
source_form_element_reference varchar(255)
|
||||||
|
);
|
||||||
|
|
||||||
alter table if exists application_form
|
alter table if exists application_form
|
||||||
add constraint FKhtad5onoy2jknhtyfmx6cvvey
|
add constraint FKhtad5onoy2jknhtyfmx6cvvey
|
||||||
foreign key (created_by_id)
|
foreign key (created_by_id)
|
||||||
@@ -182,3 +219,18 @@ alter table if exists notification
|
|||||||
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
|
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
|
||||||
foreign key (recipient_id)
|
foreign key (recipient_id)
|
||||||
references app_user;
|
references app_user;
|
||||||
|
|
||||||
|
alter table if exists section_spawn_triggers
|
||||||
|
add constraint FK7lf0hf8cepm2o9nty147x2ahm
|
||||||
|
foreign key (form_element_id)
|
||||||
|
references form_element;
|
||||||
|
|
||||||
|
alter table if exists table_column_mappings
|
||||||
|
add constraint FK2t3a4fl5kqtqky39r7boqegf9
|
||||||
|
foreign key (form_element_id)
|
||||||
|
references form_element;
|
||||||
|
|
||||||
|
alter table if exists visibility_conditions
|
||||||
|
add constraint FK5xuf7bd179ogpq5a1m3g8q7jb
|
||||||
|
foreign key (form_element_id)
|
||||||
|
references form_element;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,12 @@
|
|||||||
\usepackage{xcolor}
|
\usepackage{xcolor}
|
||||||
\usepackage{tcolorbox}
|
\usepackage{tcolorbox}
|
||||||
\usepackage[normalem]{ulem}
|
\usepackage[normalem]{ulem}
|
||||||
|
\usepackage{tabularx}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
|
||||||
|
% Define column type for auto-wrapping text
|
||||||
|
\newcolumntype{Y}{>{\raggedright\arraybackslash}X}
|
||||||
|
|
||||||
\hypersetup{
|
\hypersetup{
|
||||||
colorlinks=true,
|
colorlinks=true,
|
||||||
@@ -73,12 +79,22 @@ Dieses Dokument enthält die Details der Betriebsvereinbarung "[[${applicationFo
|
|||||||
\textit{\small [[${element.description}]]}
|
\textit{\small [[${element.description}]]}
|
||||||
[/]
|
[/]
|
||||||
|
|
||||||
|
[# th:if="${element.isTable}"]
|
||||||
|
\vspace{0.5em}
|
||||||
|
\noindent
|
||||||
|
\small
|
||||||
|
[(${element.value})]
|
||||||
|
\normalsize
|
||||||
|
\vspace{0.5em}
|
||||||
|
[/]
|
||||||
|
[# th:if="${!element.isTable}"]
|
||||||
\begin{tcolorbox}[colback=gray!5, colframe=gray!20, arc=0mm, boxrule=0.5pt]
|
\begin{tcolorbox}[colback=gray!5, colframe=gray!20, arc=0mm, boxrule=0.5pt]
|
||||||
[[${element.value}]]
|
[[${element.value}]]
|
||||||
\end{tcolorbox}
|
\end{tcolorbox}
|
||||||
[/]
|
[/]
|
||||||
[/]
|
[/]
|
||||||
[/]
|
[/]
|
||||||
|
[/]
|
||||||
|
|
||||||
\vspace{3cm}
|
\vspace{3cm}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
:is="getResolvedComponent(formElementItem.formElement)"
|
:is="getResolvedComponent(formElementItem.formElement)"
|
||||||
:form-options="formElementItem.formElement.options"
|
:form-options="formElementItem.formElement.options"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
|
:all-form-elements="props.allFormElements"
|
||||||
|
:table-row-preset="formElementItem.formElement.tableRowPreset"
|
||||||
@update:form-options="updateFormOptions($event, formElementItem)"
|
@update:form-options="updateFormOptions($event, formElementItem)"
|
||||||
/>
|
/>
|
||||||
<div v-if="formElementItem.formElement.isClonable && !props.disabled" class="mt-3">
|
<div v-if="formElementItem.formElement.isClonable && !props.disabled" class="mt-3">
|
||||||
@@ -102,6 +104,7 @@ const props = defineProps<{
|
|||||||
visibilityMap: Map<string, boolean>
|
visibilityMap: Map<string, boolean>
|
||||||
applicationFormId?: string
|
applicationFormId?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
allFormElements?: FormElementDto[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
:visibility-map="visibilityMap"
|
:visibility-map="visibilityMap"
|
||||||
:application-form-id="applicationFormId"
|
:application-form-id="applicationFormId"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:all-form-elements="allFormElements"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(elements) =>
|
(elements) =>
|
||||||
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
handleFormElementUpdate(elements, getSubsectionKey(currentFormElementSection, sectionIndex, subsection))
|
||||||
@@ -107,9 +108,9 @@ const { cloneElement } = useClonableElements()
|
|||||||
const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
|
const previousVisibilityMap = ref<Map<string, boolean>>(new Map())
|
||||||
|
|
||||||
const allFormElements = computed(() => {
|
const allFormElements = computed(() => {
|
||||||
return props.formElementSections.flatMap(
|
return props.formElementSections
|
||||||
(section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? []
|
.filter((section) => section.isTemplate !== true)
|
||||||
)
|
.flatMap((section) => section.formElementSubSections?.flatMap((subsection) => subsection.formElements) ?? [])
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibilityMap = computed(() => {
|
const visibilityMap = computed(() => {
|
||||||
|
|||||||
@@ -1,8 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<UTable :data="tableData" :columns="tableColumns" class="w-full">
|
<UTable :data="tableData" :columns="tableColumns" class="w-full" :ui="{ td: 'p-2' }">
|
||||||
<template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
|
<template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
|
||||||
|
<!-- Column with cross-reference -->
|
||||||
|
<USelectMenu
|
||||||
|
v-if="hasColumnReference(col.colIndex) && !isColumnReadOnly(col.colIndex)"
|
||||||
|
:model-value="getCellValueForSelect(slotProps.row as TableRow<TableRowData>, col.key, col.colIndex)"
|
||||||
|
:items="getColumnOptions(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)"
|
||||||
|
:disabled="disabled"
|
||||||
|
:placeholder="$t('applicationForms.formElements.table.selectValue')"
|
||||||
|
:multiple="isColumnMultipleAllowed(col.colIndex)"
|
||||||
|
class="w-full min-w-32"
|
||||||
|
@update:model-value="
|
||||||
|
(val: string | string[]) =>
|
||||||
|
updateCellValue((slotProps.row as TableRow<TableRowData>).index, col.key, col.colIndex, val)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<!-- Read-only column -->
|
||||||
|
<span v-else-if="isColumnReadOnly(col.colIndex)" class="text-muted px-2 py-1">
|
||||||
|
{{ formatCellDisplay(slotProps.row as any, col.key, col.colIndex) }}
|
||||||
|
</span>
|
||||||
|
<!-- Checkbox column -->
|
||||||
|
<div v-else-if="isColumnCheckbox(col.colIndex)" class="flex justify-center">
|
||||||
|
<UCheckbox
|
||||||
|
:model-value="getCellValueForCheckbox(slotProps.row as TableRow<TableRowData>, col.key)"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:model-value="
|
||||||
|
(val: boolean | 'indeterminate') =>
|
||||||
|
updateCheckboxCell((slotProps.row as TableRow<TableRowData>).index, col.colIndex, val === true)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Regular text input -->
|
||||||
<UInput
|
<UInput
|
||||||
|
v-else
|
||||||
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
|
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="w-full min-w-32"
|
class="w-full min-w-32"
|
||||||
@@ -11,7 +42,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #actions-cell="{ row }">
|
<template v-if="canModifyRows" #actions-cell="{ row }">
|
||||||
<UButton
|
<UButton
|
||||||
v-if="!disabled"
|
v-if="!disabled"
|
||||||
icon="i-lucide-trash-2"
|
icon="i-lucide-trash-2"
|
||||||
@@ -28,26 +59,61 @@
|
|||||||
{{ $t('applicationForms.formElements.table.noData') }}
|
{{ $t('applicationForms.formElements.table.noData') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UButton v-if="!disabled" variant="outline" size="sm" leading-icon="i-lucide-plus" @click="addRow">
|
<UButton v-if="!disabled && canModifyRows" variant="outline" size="sm" leading-icon="i-lucide-plus" @click="addRow">
|
||||||
{{ $t('applicationForms.formElements.table.addRow') }}
|
{{ $t('applicationForms.formElements.table.addRow') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormOptionDto } from '~~/.api-client'
|
import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
|
||||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||||
|
import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
formOptions: FormOptionDto[]
|
formOptions: FormOptionDto[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
allFormElements?: FormElementDto[]
|
||||||
|
tableRowPreset?: TableRowPresetDto
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:formOptions', value: FormOptionDto[]): void
|
(e: 'update:formOptions', value: FormOptionDto[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type TableRowData = Record<string, string>
|
const { getReferencedColumnValues, getConstrainedColumnValues, applyRowPresets } = useTableCrossReferences()
|
||||||
|
|
||||||
|
const canModifyRows = computed(() => {
|
||||||
|
if (!props.tableRowPreset) return true
|
||||||
|
return props.tableRowPreset.canAddRows !== false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for changes in source table and apply row presets reactively
|
||||||
|
const sourceTableOptions = computed(() => {
|
||||||
|
if (!props.tableRowPreset?.sourceTableReference || !props.allFormElements) return null
|
||||||
|
const sourceTable = props.allFormElements.find(
|
||||||
|
(el) => el.reference === props.tableRowPreset?.sourceTableReference && el.type === 'TABLE'
|
||||||
|
)
|
||||||
|
return sourceTable?.options
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
sourceTableOptions,
|
||||||
|
() => {
|
||||||
|
if (!sourceTableOptions.value || !props.tableRowPreset || !props.allFormElements) return
|
||||||
|
|
||||||
|
const updatedOptions = applyRowPresets(props.tableRowPreset, props.formOptions, props.allFormElements)
|
||||||
|
|
||||||
|
const hasChanges = updatedOptions.some((opt, idx) => opt.value !== props.formOptions[idx]?.value)
|
||||||
|
if (hasChanges) {
|
||||||
|
emit('update:formOptions', updatedOptions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellValue = string | string[] | boolean
|
||||||
|
type TableRowData = Record<string, CellValue>
|
||||||
|
|
||||||
interface DataColumn {
|
interface DataColumn {
|
||||||
key: string
|
key: string
|
||||||
@@ -67,7 +133,8 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
|
|||||||
header: option.label || ''
|
header: option.label || ''
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (!props.disabled) {
|
// Only show actions column if not disabled AND rows can be modified
|
||||||
|
if (!props.disabled && canModifyRows.value) {
|
||||||
columns.push({
|
columns.push({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: ''
|
header: ''
|
||||||
@@ -80,10 +147,21 @@ const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
|
|||||||
const tableData = computed<TableRowData[]>(() => {
|
const tableData = computed<TableRowData[]>(() => {
|
||||||
if (props.formOptions.length === 0) return []
|
if (props.formOptions.length === 0) return []
|
||||||
|
|
||||||
const columnData: string[][] = props.formOptions.map((option) => {
|
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(option.value || '[]')
|
const parsed = JSON.parse(option.value || '[]')
|
||||||
return Array.isArray(parsed) ? parsed : []
|
if (!Array.isArray(parsed)) return []
|
||||||
|
|
||||||
|
// For multi-select columns, each cell value is already an array
|
||||||
|
// For checkbox columns, each cell value is a boolean
|
||||||
|
// For single-select columns, each cell value is a string
|
||||||
|
if (isColumnMultipleAllowed(colIndex)) {
|
||||||
|
return parsed.map((val: CellValue) => (Array.isArray(val) ? val : []))
|
||||||
|
}
|
||||||
|
if (isColumnCheckbox(colIndex)) {
|
||||||
|
return parsed.map((val: CellValue) => val === true)
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -94,8 +172,15 @@ const tableData = computed<TableRowData[]>(() => {
|
|||||||
const rows: TableRowData[] = []
|
const rows: TableRowData[] = []
|
||||||
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||||
const row: TableRowData = {}
|
const row: TableRowData = {}
|
||||||
props.formOptions.forEach((_, colIndex) => {
|
props.formOptions.forEach((option, colIndex) => {
|
||||||
row[`col_${colIndex}`] = columnData[colIndex]?.[rowIndex] ?? ''
|
const cellValue = columnData[colIndex]?.[rowIndex]
|
||||||
|
if (isColumnMultipleAllowed(colIndex)) {
|
||||||
|
row[`col_${colIndex}`] = Array.isArray(cellValue) ? cellValue : []
|
||||||
|
} else if (isColumnCheckbox(colIndex)) {
|
||||||
|
row[`col_${colIndex}`] = cellValue === true
|
||||||
|
} else {
|
||||||
|
row[`col_${colIndex}`] = typeof cellValue === 'string' ? cellValue : ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
rows.push(row)
|
rows.push(row)
|
||||||
}
|
}
|
||||||
@@ -103,8 +188,81 @@ const tableData = computed<TableRowData[]>(() => {
|
|||||||
return rows
|
return rows
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function hasColumnReference(colIndex: number): boolean {
|
||||||
|
const option = props.formOptions[colIndex]
|
||||||
|
return !!option?.columnConfig?.sourceTableReference
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColumnReadOnly(colIndex: number): boolean {
|
||||||
|
const option = props.formOptions[colIndex]
|
||||||
|
return option?.columnConfig?.isReadOnly === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColumnMultipleAllowed(colIndex: number): boolean {
|
||||||
|
const option = props.formOptions[colIndex]
|
||||||
|
return option?.columnConfig?.isMultipleAllowed === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isColumnCheckbox(colIndex: number): boolean {
|
||||||
|
const option = props.formOptions[colIndex]
|
||||||
|
return option?.columnConfig?.isCheckbox === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnOptions(colIndex: number, currentRowData?: TableRowData): string[] {
|
||||||
|
const option = props.formOptions[colIndex]
|
||||||
|
if (!option?.columnConfig || !props.allFormElements) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const { columnConfig } = option
|
||||||
|
const { rowConstraint } = columnConfig
|
||||||
|
|
||||||
|
// If row constraint is configured, filter values based on current row's key value
|
||||||
|
if (rowConstraint?.constraintTableReference && currentRowData) {
|
||||||
|
const currentRowAsRecord: Record<string, string> = {}
|
||||||
|
for (const [key, value] of Object.entries(currentRowData)) {
|
||||||
|
currentRowAsRecord[key] =
|
||||||
|
typeof value === 'string' ? value : Array.isArray(value) ? value.join(',') : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConstrainedColumnValues(
|
||||||
|
columnConfig,
|
||||||
|
currentRowAsRecord,
|
||||||
|
rowConstraint.constraintTableReference,
|
||||||
|
rowConstraint.constraintKeyColumnIndex ?? 0,
|
||||||
|
rowConstraint.constraintValueColumnIndex ?? 1,
|
||||||
|
props.allFormElements,
|
||||||
|
rowConstraint.currentRowKeyColumnIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getReferencedColumnValues(columnConfig, props.allFormElements)
|
||||||
|
}
|
||||||
|
|
||||||
function getCellValue(row: TableRow<TableRowData>, columnKey: string): string {
|
function getCellValue(row: TableRow<TableRowData>, columnKey: string): string {
|
||||||
return row.original[columnKey] ?? ''
|
const value = row.original[columnKey]
|
||||||
|
return typeof value === 'string' ? value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellValueForSelect(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string | string[] {
|
||||||
|
const value = row.original[columnKey]
|
||||||
|
if (isColumnMultipleAllowed(colIndex)) {
|
||||||
|
return Array.isArray(value) ? value : []
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellValueForCheckbox(row: TableRow<TableRowData>, columnKey: string): boolean {
|
||||||
|
const value = row.original[columnKey]
|
||||||
|
return value === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCellDisplay(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string {
|
||||||
|
const value = row.original[columnKey]
|
||||||
|
if (isColumnMultipleAllowed(colIndex) && Array.isArray(value)) {
|
||||||
|
return value.length > 0 ? value.join(', ') : '-'
|
||||||
|
}
|
||||||
|
return (typeof value === 'string' ? value : '') || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCell(rowIndex: number, columnKey: string, value: string) {
|
function updateCell(rowIndex: number, columnKey: string, value: string) {
|
||||||
@@ -113,7 +271,7 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
|
|||||||
const updatedOptions = props.formOptions.map((option, index) => {
|
const updatedOptions = props.formOptions.map((option, index) => {
|
||||||
if (index !== colIndex) return option
|
if (index !== colIndex) return option
|
||||||
|
|
||||||
let columnValues: string[]
|
let columnValues: CellValue[]
|
||||||
try {
|
try {
|
||||||
columnValues = JSON.parse(option.value || '[]')
|
columnValues = JSON.parse(option.value || '[]')
|
||||||
if (!Array.isArray(columnValues)) columnValues = []
|
if (!Array.isArray(columnValues)) columnValues = []
|
||||||
@@ -132,9 +290,11 @@ function updateCell(rowIndex: number, columnKey: string, value: string) {
|
|||||||
emit('update:formOptions', updatedOptions)
|
emit('update:formOptions', updatedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRow() {
|
function updateCellValue(rowIndex: number, columnKey: string, colIndex: number, value: string | string[]) {
|
||||||
const updatedOptions = props.formOptions.map((option) => {
|
const updatedOptions = props.formOptions.map((option, index) => {
|
||||||
let columnValues: string[]
|
if (index !== colIndex) return option
|
||||||
|
|
||||||
|
let columnValues: CellValue[]
|
||||||
try {
|
try {
|
||||||
columnValues = JSON.parse(option.value || '[]')
|
columnValues = JSON.parse(option.value || '[]')
|
||||||
if (!Array.isArray(columnValues)) columnValues = []
|
if (!Array.isArray(columnValues)) columnValues = []
|
||||||
@@ -142,7 +302,61 @@ function addRow() {
|
|||||||
columnValues = []
|
columnValues = []
|
||||||
}
|
}
|
||||||
|
|
||||||
columnValues.push('')
|
const isMultiple = isColumnMultipleAllowed(colIndex)
|
||||||
|
while (columnValues.length <= rowIndex) {
|
||||||
|
columnValues.push(isMultiple ? [] : '')
|
||||||
|
}
|
||||||
|
columnValues[rowIndex] = value
|
||||||
|
|
||||||
|
return { ...option, value: JSON.stringify(columnValues) }
|
||||||
|
})
|
||||||
|
|
||||||
|
emit('update:formOptions', updatedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean) {
|
||||||
|
const updatedOptions = props.formOptions.map((option, index) => {
|
||||||
|
if (index !== colIndex) return option
|
||||||
|
|
||||||
|
let columnValues: CellValue[]
|
||||||
|
try {
|
||||||
|
columnValues = JSON.parse(option.value || '[]')
|
||||||
|
if (!Array.isArray(columnValues)) columnValues = []
|
||||||
|
} catch {
|
||||||
|
columnValues = []
|
||||||
|
}
|
||||||
|
|
||||||
|
while (columnValues.length <= rowIndex) {
|
||||||
|
columnValues.push(false)
|
||||||
|
}
|
||||||
|
columnValues[rowIndex] = value
|
||||||
|
|
||||||
|
return { ...option, value: JSON.stringify(columnValues) }
|
||||||
|
})
|
||||||
|
|
||||||
|
emit('update:formOptions', updatedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow() {
|
||||||
|
const updatedOptions = props.formOptions.map((option, colIndex) => {
|
||||||
|
let columnValues: CellValue[]
|
||||||
|
try {
|
||||||
|
columnValues = JSON.parse(option.value || '[]')
|
||||||
|
if (!Array.isArray(columnValues)) columnValues = []
|
||||||
|
} catch {
|
||||||
|
columnValues = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multi-select columns, initialize with empty array
|
||||||
|
// For checkbox columns, initialize with false
|
||||||
|
// Otherwise empty string
|
||||||
|
let emptyValue: CellValue = ''
|
||||||
|
if (isColumnMultipleAllowed(colIndex)) {
|
||||||
|
emptyValue = []
|
||||||
|
} else if (isColumnCheckbox(colIndex)) {
|
||||||
|
emptyValue = false
|
||||||
|
}
|
||||||
|
columnValues.push(emptyValue)
|
||||||
|
|
||||||
return { ...option, value: JSON.stringify(columnValues) }
|
return { ...option, value: JSON.stringify(columnValues) }
|
||||||
})
|
})
|
||||||
@@ -152,7 +366,7 @@ function addRow() {
|
|||||||
|
|
||||||
function removeRow(rowIndex: number) {
|
function removeRow(rowIndex: number) {
|
||||||
const updatedOptions = props.formOptions.map((option) => {
|
const updatedOptions = props.formOptions.map((option) => {
|
||||||
let columnValues: string[]
|
let columnValues: CellValue[]
|
||||||
try {
|
try {
|
||||||
columnValues = JSON.parse(option.value || '[]')
|
columnValues = JSON.parse(option.value || '[]')
|
||||||
if (!Array.isArray(columnValues)) columnValues = []
|
if (!Array.isArray(columnValues)) columnValues = []
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export { useUser } from './user/useUser'
|
|||||||
export { useUserApi } from './user/useUserApi'
|
export { useUserApi } from './user/useUserApi'
|
||||||
export { useSectionSpawning } from './useSectionSpawning'
|
export { useSectionSpawning } from './useSectionSpawning'
|
||||||
export { useClonableElements } from './useClonableElements'
|
export { useClonableElements } from './useClonableElements'
|
||||||
|
export { useTableCrossReferences } from './useTableCrossReferences'
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import type { FormElementDto, VisibilityConditionOperator } from '~~/.api-client'
|
import type { FormElementDto, FormElementVisibilityCondition, VisibilityConditionOperator } from '~~/.api-client'
|
||||||
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client'
|
import { VisibilityConditionOperator as VCOperator, VisibilityConditionType as VCType } from '~~/.api-client'
|
||||||
|
|
||||||
export function useFormElementVisibility() {
|
export function useFormElementVisibility() {
|
||||||
|
/**
|
||||||
|
* Evaluates visibility for all form elements based on their visibility conditions.
|
||||||
|
* Returns a map of element key (id or reference) to visibility status.
|
||||||
|
*/
|
||||||
function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> {
|
function evaluateFormElementVisibility(allFormElements: FormElementDto[]): Map<string, boolean> {
|
||||||
const formElementsByRef = buildFormElementsMap(allFormElements)
|
const formElementsByRef = buildFormElementsMap(allFormElements)
|
||||||
const visibilityMap = new Map<string, boolean>()
|
const visibilityMap = new Map<string, boolean>()
|
||||||
|
|
||||||
allFormElements.forEach((element) => {
|
allFormElements.forEach((element) => {
|
||||||
const isVisible = isElementVisible(element, formElementsByRef, visibilityMap)
|
const isVisible = isElementVisible(element, formElementsByRef)
|
||||||
const key = element.id || element.reference
|
const key = element.id || element.reference
|
||||||
if (key) {
|
if (key) {
|
||||||
visibilityMap.set(key, isVisible)
|
visibilityMap.set(key, isVisible)
|
||||||
@@ -27,24 +31,33 @@ export function useFormElementVisibility() {
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
function isElementVisible(
|
/**
|
||||||
element: FormElementDto,
|
* Evaluates if an element is visible based on its visibility conditions.
|
||||||
formElementsByRef: Map<string, FormElementDto>,
|
* Multiple conditions use AND logic - all conditions must be met for the element to be visible.
|
||||||
_visibilityMap: Map<string, boolean>
|
*/
|
||||||
): boolean {
|
function isElementVisible(element: FormElementDto, formElementsByRef: Map<string, FormElementDto>): boolean {
|
||||||
if (!element.visibilityCondition) {
|
const conditions = element.visibilityConditions
|
||||||
|
if (!conditions || conditions.length === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const condition = element.visibilityCondition
|
// All conditions must be met (AND logic)
|
||||||
|
return conditions.every((condition) => evaluateSingleCondition(condition, formElementsByRef))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a single visibility condition against the form state.
|
||||||
|
*/
|
||||||
|
function evaluateSingleCondition(
|
||||||
|
condition: FormElementVisibilityCondition,
|
||||||
|
formElementsByRef: Map<string, FormElementDto>
|
||||||
|
): boolean {
|
||||||
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference)
|
const sourceElement = formElementsByRef.get(condition.sourceFormElementReference)
|
||||||
if (!sourceElement) {
|
if (!sourceElement) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceValue = getFormElementValue(sourceElement)
|
const sourceValue = getFormElementValue(sourceElement)
|
||||||
|
|
||||||
const operator = condition.formElementOperator || VCOperator.Equals
|
const operator = condition.formElementOperator || VCOperator.Equals
|
||||||
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
|
const conditionMet = evaluateCondition(sourceValue, condition.formElementExpectedValue, operator)
|
||||||
|
|
||||||
@@ -61,20 +74,15 @@ export function useFormElementVisibility() {
|
|||||||
expectedValue: string,
|
expectedValue: string,
|
||||||
operator: VisibilityConditionOperator
|
operator: VisibilityConditionOperator
|
||||||
): boolean {
|
): boolean {
|
||||||
let result: boolean
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case VCOperator.Equals:
|
case VCOperator.Equals:
|
||||||
result = actualValue.toLowerCase() === expectedValue.toLowerCase()
|
return actualValue.toLowerCase() === expectedValue.toLowerCase()
|
||||||
return result
|
|
||||||
case VCOperator.NotEquals:
|
case VCOperator.NotEquals:
|
||||||
result = actualValue.toLowerCase() !== expectedValue.toLowerCase()
|
return actualValue.toLowerCase() !== expectedValue.toLowerCase()
|
||||||
return result
|
|
||||||
case VCOperator.IsEmpty:
|
case VCOperator.IsEmpty:
|
||||||
result = actualValue === ''
|
return actualValue === ''
|
||||||
return result
|
|
||||||
case VCOperator.IsNotEmpty:
|
case VCOperator.IsNotEmpty:
|
||||||
result = actualValue !== ''
|
return actualValue !== ''
|
||||||
return result
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,54 @@ export function useSectionSpawning() {
|
|||||||
let resultSections = sections
|
let resultSections = sections
|
||||||
|
|
||||||
for (const formElement of updatedFormElements) {
|
for (const formElement of updatedFormElements) {
|
||||||
if (!formElement.sectionSpawnTrigger || !formElement.reference) {
|
const triggers = formElement.sectionSpawnTriggers
|
||||||
|
if (!triggers || triggers.length === 0 || !formElement.reference) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract trigger configuration and current element value
|
|
||||||
const trigger = formElement.sectionSpawnTrigger
|
|
||||||
const triggerValue = getFormElementValue(formElement)
|
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:
|
// Process each trigger independently
|
||||||
// 1. Condition met but no section spawned yet → create new section
|
for (const trigger of triggers) {
|
||||||
if (shouldSpawn && existingSpawnedSections.length === 0) {
|
resultSections = processSingleTrigger(resultSections, formElement, trigger, triggerValue)
|
||||||
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
|
return resultSections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processSingleTrigger(
|
||||||
|
sections: FormElementSectionDto[],
|
||||||
|
formElement: FormElementDto,
|
||||||
|
trigger: SectionSpawnTriggerDto,
|
||||||
|
triggerValue: string
|
||||||
|
): FormElementSectionDto[] {
|
||||||
|
let resultSections = sections
|
||||||
|
const shouldSpawn = shouldSpawnSection(trigger, triggerValue)
|
||||||
|
// Find existing spawned section for this specific trigger (by template reference)
|
||||||
|
const existingSpawnedSection = findSpawnedSectionForTrigger(
|
||||||
|
resultSections,
|
||||||
|
formElement.reference!,
|
||||||
|
trigger.templateReference
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle three spawn states:
|
||||||
|
// 1. Condition met but no section spawned yet → create new section
|
||||||
|
if (shouldSpawn && !existingSpawnedSection) {
|
||||||
|
resultSections = spawnNewSection(resultSections, formElement, trigger, triggerValue)
|
||||||
|
}
|
||||||
|
// 2. Condition no longer met but section exists → remove spawned section
|
||||||
|
else if (!shouldSpawn && existingSpawnedSection) {
|
||||||
|
resultSections = removeSpawnedSectionForTrigger(resultSections, formElement.reference!, trigger.templateReference)
|
||||||
|
}
|
||||||
|
// 3. Condition still met and section exists → update section titles if value changed
|
||||||
|
else if (shouldSpawn && existingSpawnedSection && triggerValue) {
|
||||||
|
resultSections = updateSpawnedSectionTitles(resultSections, formElement.reference!, trigger, triggerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultSections
|
||||||
|
}
|
||||||
|
|
||||||
function spawnNewSection(
|
function spawnNewSection(
|
||||||
sections: FormElementSectionDto[],
|
sections: FormElementSectionDto[],
|
||||||
element: FormElementDto,
|
element: FormElementDto,
|
||||||
@@ -94,8 +113,30 @@ export function useSectionSpawning() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeSpawnedSections(sections: FormElementSectionDto[], elementReference: string): FormElementSectionDto[] {
|
function findSpawnedSectionForTrigger(
|
||||||
return sections.filter((section) => section.spawnedFromElementReference !== elementReference || section.isTemplate)
|
sections: FormElementSectionDto[],
|
||||||
|
elementReference: string,
|
||||||
|
templateReference: string
|
||||||
|
): FormElementSectionDto | undefined {
|
||||||
|
return sections.find(
|
||||||
|
(section) =>
|
||||||
|
!section.isTemplate &&
|
||||||
|
section.spawnedFromElementReference === elementReference &&
|
||||||
|
section.templateReference === templateReference
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSpawnedSectionForTrigger(
|
||||||
|
sections: FormElementSectionDto[],
|
||||||
|
elementReference: string,
|
||||||
|
templateReference: string
|
||||||
|
): FormElementSectionDto[] {
|
||||||
|
return sections.filter(
|
||||||
|
(section) =>
|
||||||
|
section.isTemplate ||
|
||||||
|
section.spawnedFromElementReference !== elementReference ||
|
||||||
|
section.templateReference !== templateReference
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnSectionFromTemplate(
|
function spawnSectionFromTemplate(
|
||||||
@@ -146,13 +187,6 @@ export function useSectionSpawning() {
|
|||||||
return trigger.sectionSpawnConditionType === VisibilityConditionType.Show ? isConditionMet : !isConditionMet
|
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(
|
function findTemplateSection(
|
||||||
sections: FormElementSectionDto[],
|
sections: FormElementSectionDto[],
|
||||||
templateReference: string
|
templateReference: string
|
||||||
|
|||||||
269
legalconsenthub/app/composables/useTableCrossReferences.ts
Normal file
269
legalconsenthub/app/composables/useTableCrossReferences.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import type {
|
||||||
|
FormElementDto,
|
||||||
|
FormOptionDto,
|
||||||
|
TableColumnConfigDto,
|
||||||
|
TableColumnFilterDto,
|
||||||
|
TableRowPresetDto
|
||||||
|
} from '~~/.api-client'
|
||||||
|
import { VisibilityConditionOperator as VCOperator } from '~~/.api-client'
|
||||||
|
|
||||||
|
export function useTableCrossReferences() {
|
||||||
|
// Get available values for a column that references another table's column
|
||||||
|
function getReferencedColumnValues(
|
||||||
|
columnConfig: TableColumnConfigDto | undefined,
|
||||||
|
allFormElements: FormElementDto[]
|
||||||
|
): string[] {
|
||||||
|
if (!columnConfig?.sourceTableReference || columnConfig.sourceColumnIndex === undefined) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTable = findTableElement(columnConfig.sourceTableReference, allFormElements)
|
||||||
|
if (!sourceTable) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceColumn = sourceTable.options[columnConfig.sourceColumnIndex]
|
||||||
|
if (!sourceColumn) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnValues = parseColumnValues(sourceColumn.value)
|
||||||
|
|
||||||
|
// Apply filter if present
|
||||||
|
if (columnConfig.filterCondition) {
|
||||||
|
return filterColumnValues(columnValues, columnConfig.filterCondition, sourceTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnValues.filter((v) => v.trim() !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filtered values based on constraints from another table
|
||||||
|
// Used for cases like "Permission-ID can only use permissions allowed for the selected role"
|
||||||
|
function getConstrainedColumnValues(
|
||||||
|
columnConfig: TableColumnConfigDto | undefined,
|
||||||
|
currentRowData: Record<string, string>,
|
||||||
|
constraintTableReference: string,
|
||||||
|
constraintKeyColumnIndex: number,
|
||||||
|
constraintValueColumnIndex: number,
|
||||||
|
allFormElements: FormElementDto[],
|
||||||
|
currentRowKeyColumnIndex?: number
|
||||||
|
): string[] {
|
||||||
|
if (!columnConfig?.sourceTableReference) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraintTable = findTableElement(constraintTableReference, allFormElements)
|
||||||
|
if (!constraintTable) {
|
||||||
|
// No constraint found, return all values from source table column
|
||||||
|
return getReferencedColumnValues(columnConfig, allFormElements)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookupColumnIndex = currentRowKeyColumnIndex ?? constraintKeyColumnIndex
|
||||||
|
const keyValue = currentRowData[`col_${lookupColumnIndex}`]
|
||||||
|
if (!keyValue) {
|
||||||
|
// No key value to look up, return all values from source table column
|
||||||
|
return getReferencedColumnValues(columnConfig, allFormElements)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedValuesRaw = getAllowedValuesFromConstraintTable(
|
||||||
|
constraintTable,
|
||||||
|
keyValue,
|
||||||
|
constraintKeyColumnIndex,
|
||||||
|
constraintValueColumnIndex
|
||||||
|
)
|
||||||
|
const allowedValues = allowedValuesRaw.flatMap((v) => (typeof v === 'boolean' ? String(v) : v))
|
||||||
|
|
||||||
|
// If no allowed values found, fall back to all values from source table
|
||||||
|
if (allowedValues.length === 0) {
|
||||||
|
return getReferencedColumnValues(columnConfig, allFormElements)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply row presets from a source table based on filter conditions
|
||||||
|
function applyRowPresets(
|
||||||
|
tableRowPreset: TableRowPresetDto | undefined,
|
||||||
|
targetOptions: FormOptionDto[],
|
||||||
|
allFormElements: FormElementDto[]
|
||||||
|
): FormOptionDto[] {
|
||||||
|
if (!tableRowPreset?.sourceTableReference) {
|
||||||
|
return targetOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceTable = findTableElement(tableRowPreset.sourceTableReference, allFormElements)
|
||||||
|
if (!sourceTable) {
|
||||||
|
return targetOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get source table data
|
||||||
|
const sourceData = parseTableData(sourceTable.options)
|
||||||
|
|
||||||
|
// Filter rows based on filter condition
|
||||||
|
const filteredRows = tableRowPreset.filterCondition
|
||||||
|
? filterTableRows(sourceData, tableRowPreset.filterCondition, sourceTable.options)
|
||||||
|
: sourceData
|
||||||
|
|
||||||
|
// Apply column mappings to create preset rows in target
|
||||||
|
const columnMappings = tableRowPreset.columnMappings || []
|
||||||
|
const presetRowCount = filteredRows.length
|
||||||
|
|
||||||
|
return targetOptions.map((option, targetColIndex) => {
|
||||||
|
const mapping = columnMappings.find((m) => m.targetColumnIndex === targetColIndex)
|
||||||
|
|
||||||
|
// For mapped columns, use values from source
|
||||||
|
if (mapping && mapping.sourceColumnIndex !== undefined) {
|
||||||
|
const sourceColIndex = mapping.sourceColumnIndex
|
||||||
|
const presetValues = filteredRows.map((row) => String(row[sourceColIndex] ?? ''))
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
value: JSON.stringify(presetValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-mapped columns, ensure we have the right number of rows
|
||||||
|
const existingValues = parseColumnValues(option.value)
|
||||||
|
const isCheckboxColumn = option.columnConfig?.isCheckbox === true
|
||||||
|
|
||||||
|
// Pad or trim to match preset row count
|
||||||
|
const adjustedValues: (string | boolean)[] = []
|
||||||
|
for (let i = 0; i < presetRowCount; i++) {
|
||||||
|
if (i < existingValues.length && existingValues[i] !== undefined) {
|
||||||
|
adjustedValues.push(existingValues[i]!)
|
||||||
|
} else {
|
||||||
|
// Initialize new rows with appropriate default
|
||||||
|
adjustedValues.push(isCheckboxColumn ? false : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
value: JSON.stringify(adjustedValues)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTableElement(reference: string, allFormElements: FormElementDto[]): FormElementDto | undefined {
|
||||||
|
return allFormElements.find((el) => el.reference === reference && el.type === 'TABLE')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseColumnValues(jsonValue: string | undefined): string[] {
|
||||||
|
if (!jsonValue) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonValue)
|
||||||
|
return Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTableData(options: FormOptionDto[]): (string | boolean)[][] {
|
||||||
|
const columnData = options.map((opt) => parseColumnValuesWithTypes(opt.value))
|
||||||
|
const rowCount = Math.max(...columnData.map((col) => col.length), 0)
|
||||||
|
|
||||||
|
const rows: (string | boolean)[][] = []
|
||||||
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||||
|
const row = columnData.map((col) => col[rowIndex] ?? '')
|
||||||
|
rows.push(row)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseColumnValuesWithTypes(jsonValue: string | undefined): (string | boolean)[] {
|
||||||
|
if (!jsonValue) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonValue)
|
||||||
|
return Array.isArray(parsed) ? parsed : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterColumnValues(
|
||||||
|
values: string[],
|
||||||
|
filterCondition: TableColumnFilterDto,
|
||||||
|
sourceTable: FormElementDto
|
||||||
|
): string[] {
|
||||||
|
if (filterCondition.sourceColumnIndex === undefined) {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterColumn = sourceTable.options[filterCondition.sourceColumnIndex]
|
||||||
|
if (!filterColumn) {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterColumnValues = parseColumnValues(filterColumn.value)
|
||||||
|
|
||||||
|
return values.filter((_, index) => {
|
||||||
|
const filterValue = filterColumnValues[index] || ''
|
||||||
|
return evaluateFilterCondition(filterValue, filterCondition)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTableRows(
|
||||||
|
rows: (string | boolean)[][],
|
||||||
|
filterCondition: TableColumnFilterDto,
|
||||||
|
_options: FormOptionDto[]
|
||||||
|
): (string | boolean)[][] {
|
||||||
|
if (filterCondition.sourceColumnIndex === undefined) {
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const filterValue = row[filterCondition.sourceColumnIndex!] ?? ''
|
||||||
|
return evaluateFilterCondition(filterValue, filterCondition)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFilterCondition(actualValue: string | boolean, filterCondition: TableColumnFilterDto): boolean {
|
||||||
|
const expectedValue = filterCondition.expectedValue || ''
|
||||||
|
const operator = filterCondition.operator || VCOperator.Equals
|
||||||
|
|
||||||
|
// Handle boolean values (from checkbox columns)
|
||||||
|
const normalizedActual = typeof actualValue === 'boolean' ? String(actualValue) : actualValue
|
||||||
|
|
||||||
|
switch (operator) {
|
||||||
|
case VCOperator.Equals:
|
||||||
|
return normalizedActual.toLowerCase() === expectedValue.toLowerCase()
|
||||||
|
case VCOperator.NotEquals:
|
||||||
|
return normalizedActual.toLowerCase() !== expectedValue.toLowerCase()
|
||||||
|
case VCOperator.IsEmpty:
|
||||||
|
return normalizedActual.trim() === ''
|
||||||
|
case VCOperator.IsNotEmpty:
|
||||||
|
return normalizedActual.trim() !== ''
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllowedValuesFromConstraintTable(
|
||||||
|
constraintTable: FormElementDto,
|
||||||
|
keyValue: string,
|
||||||
|
keyColumnIndex: number,
|
||||||
|
valueColumnIndex: number
|
||||||
|
): (string | boolean | string[])[] {
|
||||||
|
const tableData = parseTableData(constraintTable.options)
|
||||||
|
const allowedValues: (string | boolean | string[])[] = []
|
||||||
|
|
||||||
|
tableData.forEach((row) => {
|
||||||
|
const keyCell = row[keyColumnIndex]
|
||||||
|
const keyCellStr = Array.isArray(keyCell) ? keyCell[0] : typeof keyCell === 'boolean' ? String(keyCell) : keyCell
|
||||||
|
|
||||||
|
if (keyCellStr?.toLowerCase() === keyValue.toLowerCase()) {
|
||||||
|
const value = row[valueColumnIndex]
|
||||||
|
if (value !== undefined && !allowedValues.includes(value)) {
|
||||||
|
allowedValues.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return allowedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getReferencedColumnValues,
|
||||||
|
getConstrainedColumnValues,
|
||||||
|
applyRowPresets
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"addRow": "Zeile hinzufügen",
|
"addRow": "Zeile hinzufügen",
|
||||||
"removeRow": "Zeile entfernen",
|
"removeRow": "Zeile entfernen",
|
||||||
"emptyValue": "Keine Eingabe",
|
"emptyValue": "Keine Eingabe",
|
||||||
"noData": "Keine Daten vorhanden"
|
"noData": "Keine Daten vorhanden",
|
||||||
|
"selectValue": "Wert auswählen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"addRow": "Add row",
|
"addRow": "Add row",
|
||||||
"removeRow": "Remove row",
|
"removeRow": "Remove row",
|
||||||
"emptyValue": "No input",
|
"emptyValue": "No input",
|
||||||
"noData": "No data available"
|
"noData": "No data available",
|
||||||
|
"selectValue": "Select value"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
Reference in New Issue
Block a user