From be9d2ec9d78ac7e8ca6bec1f4b7fa8e53863d092 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sat, 27 Dec 2025 17:32:16 +0100 Subject: [PATCH] feat: Add TheTable form element --- CLAUDE.md | 59 +++++- api/legalconsenthub.yml | 1 + .../ApplicationFormFormatService.kt | 46 +++++ legalconsenthub/app/components/FormEngine.vue | 2 + .../app/components/formelements/TheTable.vue | 170 ++++++++++++++++++ legalconsenthub/i18n/locales/de.json | 8 +- legalconsenthub/i18n/locales/en.json | 8 +- 7 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 legalconsenthub/app/components/formelements/TheTable.vue diff --git a/CLAUDE.md b/CLAUDE.md index 898ea81..4ef466b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ Application Form └── Form Elements (FormElement) ├── id (UUID - generated by backend) ├── reference (string - custom key like "art_der_massnahme") - ├── type (SELECT, CHECKBOX, RADIOBUTTON, TEXTFIELD, TEXTAREA, SWITCH, RICH_TEXT, DATE) + ├── type (SELECT, CHECKBOX, RADIOBUTTON, TEXTFIELD, TEXTAREA, SWITCH, RICH_TEXT, DATE, TABLE) ├── title, description ├── options (FormOption[]) │ ├── value, label @@ -369,6 +369,62 @@ Form elements can trigger the dynamic creation of full sections based on user in - `useSectionSpawning`: Handles spawn trigger evaluation and template cloning - `useClonableElements`: Handles element cloning with reference generation and deep cloning +### 12. Table Form Element + +The TABLE form element type provides editable tabular data input with dynamic row management. + +**Data Structure**: + +Table data is stored using the existing `FormOptionDto` array: +- Each option represents a **column** +- `option.label` = column header +- `option.value` = JSON array of cell values for that column (e.g., `'["row1", "row2"]'`) +- Row count is determined by the JSON array length + +**Example Configuration (YAML)**: + +```yaml +- reference: module_overview_table + title: Module Overview + type: TABLE + options: + - value: '[]' + label: Modulname + processingPurpose: SYSTEM_OPERATION + employeeDataCategory: NON_CRITICAL + - value: '[]' + label: Beschreibung + processingPurpose: SYSTEM_OPERATION + employeeDataCategory: NON_CRITICAL + - value: '[]' + label: Verantwortlicher + processingPurpose: BUSINESS_PROCESS + employeeDataCategory: REVIEW_REQUIRED +``` + +**Frontend Behavior**: + +- Uses Nuxt UI `UTable` component with custom cell slots +- Each cell renders a `UInput` for text entry +- Users can dynamically add/remove rows +- Component: `legalconsenthub/app/components/formelements/TheTable.vue` + +**Backend Behavior**: + +- PDF export renders table data as a LaTeX tabular environment +- Empty cells display "-" placeholder +- Handled in `ApplicationFormFormatService.renderTableValue()` + +**Versioning**: + +- Table data is fully versioned as part of form snapshots +- Column structure (headers) and row data are both preserved + +**Future Extensibility**: + +- Designed to support other cell types (dropdown, date picker) in future iterations +- Column type configuration can be added via option metadata + --- ## Project Structure @@ -749,6 +805,7 @@ act -n - Verify these render correctly (with saved values): - `TEXTFIELD`, `TEXTAREA`, `DATE`, `RICH_TEXT` - `SELECT`, `RADIOBUTTON`, `CHECKBOX`, `SWITCH` + - `TABLE` (verify rows/columns render as LaTeX tabular) - If you changed backend filtering or templating, ensure: - Template sections (`isTemplate=true`) remain excluded from export - Spawned sections (`isTemplate=false`, `spawnedFromElementReference` set) are included diff --git a/api/legalconsenthub.yml b/api/legalconsenthub.yml index 8219c93..359e534 100644 --- a/api/legalconsenthub.yml +++ b/api/legalconsenthub.yml @@ -1456,6 +1456,7 @@ components: - SWITCH - RICH_TEXT - DATE + - TABLE FormElementVisibilityCondition: type: object diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt index d7e07b6..68c3612 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt @@ -11,6 +11,8 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotD import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSubSectionSnapshotDto +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.stereotype.Service import org.thymeleaf.TemplateEngine import org.thymeleaf.context.Context @@ -126,9 +128,53 @@ class ApplicationFormFormatService( "SWITCH" -> { if (element.options.any { it.value == "true" }) "Ja" else "Nein" } + "TABLE" -> { + renderTableValue(element) + } else -> "Keine Auswahl getroffen" } + private fun renderTableValue(element: FormElementSnapshotDto): String { + if (element.options.isEmpty()) return "Keine Daten" + + val objectMapper = jacksonObjectMapper() + val headers = element.options.map { LatexEscaper.escape(it.label ?: "") } + val columnData = + element.options.map { option -> + try { + val typeRef = object : TypeReference>() {} + objectMapper.readValue(option.value ?: "[]", typeRef) + } catch (e: Exception) { + emptyList() + } + } + + val rowCount = columnData.maxOfOrNull { col -> col.size } ?: 0 + if (rowCount == 0) return "Keine Daten" + + val columnSpec = headers.joinToString(" | ") { "l" } + val headerRow = headers.joinToString(" & ") + val dataRows = + (0 until rowCount).map { rowIndex -> + columnData.joinToString(" & ") { col: List -> + val value = col.getOrNull(rowIndex) ?: "" + if (value.isBlank()) "-" else LatexEscaper.escape(value) + } + } + + return buildString { + appendLine("\\begin{tabular}{$columnSpec}") + appendLine("\\hline") + appendLine("$headerRow \\\\") + appendLine("\\hline") + dataRows.forEach { row: String -> + appendLine("$row \\\\") + } + appendLine("\\hline") + appendLine("\\end{tabular}") + } + } + private fun filterVisibleElements(snapshot: ApplicationFormSnapshotDto): ApplicationFormSnapshotDto { val allElements = collectAllFormElements(snapshot) val formElementsByRef = buildSnapshotFormElementsByRefMap(allElements) diff --git a/legalconsenthub/app/components/FormEngine.vue b/legalconsenthub/app/components/FormEngine.vue index b5630bf..0bdc496 100644 --- a/legalconsenthub/app/components/FormEngine.vue +++ b/legalconsenthub/app/components/FormEngine.vue @@ -166,6 +166,8 @@ function getResolvedComponent(formElement: FormElementDto) { return resolveComponent('TheEditor') case 'DATE': return resolveComponent('TheDate') + case 'TABLE': + return resolveComponent('TheTable') default: return resolveComponent('Unimplemented') } diff --git a/legalconsenthub/app/components/formelements/TheTable.vue b/legalconsenthub/app/components/formelements/TheTable.vue new file mode 100644 index 0000000..0464c74 --- /dev/null +++ b/legalconsenthub/app/components/formelements/TheTable.vue @@ -0,0 +1,170 @@ + + + diff --git a/legalconsenthub/i18n/locales/de.json b/legalconsenthub/i18n/locales/de.json index e2e399a..fa70f82 100644 --- a/legalconsenthub/i18n/locales/de.json +++ b/legalconsenthub/i18n/locales/de.json @@ -23,7 +23,13 @@ "title": "Titel", "text": "Text", "unimplemented": "Element nicht implementiert:", - "richTextPlaceholder": "Schreiben Sie hier Ihre Ergänzungen..." + "richTextPlaceholder": "Schreiben Sie hier Ihre Ergänzungen...", + "table": { + "addRow": "Zeile hinzufügen", + "removeRow": "Zeile entfernen", + "emptyValue": "Keine Eingabe", + "noData": "Keine Daten vorhanden" + } }, "status": { "draft": "Entwurf", diff --git a/legalconsenthub/i18n/locales/en.json b/legalconsenthub/i18n/locales/en.json index 73eed1c..b3b10ca 100644 --- a/legalconsenthub/i18n/locales/en.json +++ b/legalconsenthub/i18n/locales/en.json @@ -23,7 +23,13 @@ "title": "Title", "text": "Text", "unimplemented": "Element unimplemented:", - "richTextPlaceholder": "Write your additions here..." + "richTextPlaceholder": "Write your additions here...", + "table": { + "addRow": "Add row", + "removeRow": "Remove row", + "emptyValue": "No input", + "noData": "No data available" + } }, "status": { "draft": "Draft",