feat: Cleanup old HTML and non-versioned PDF export

This commit is contained in:
2025-12-26 10:46:18 +01:00
parent 896902f590
commit 59e89662cf
4 changed files with 0 additions and 495 deletions

View File

@@ -51,16 +51,6 @@ class ApplicationFormController(
),
)
@PreAuthorize(
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
)
override fun getApplicationFormHtml(id: UUID): ResponseEntity<String> {
val applicationForm = applicationFormService.getApplicationFormById(id)
return ResponseEntity.ok(
applicationFormFormatService.generateHtml(applicationForm),
)
}
@PreAuthorize(
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')",
)

View File

@@ -7,11 +7,6 @@ import com.betriebsratkanzlei.legalconsenthub.application_form.export.latex.Late
import com.betriebsratkanzlei.legalconsenthub.application_form.export.latex.LatexSection
import com.betriebsratkanzlei.legalconsenthub.application_form.export.latex.LatexSubSection
import com.betriebsratkanzlei.legalconsenthub.application_form.export.latex.RichTextToLatexConverter
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSubSection
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionOperator
import com.betriebsratkanzlei.legalconsenthub.form_element.VisibilityConditionType
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSectionSnapshotDto
import com.betriebsratkanzlei.legalconsenthub_api.model.FormElementSnapshotDto
@@ -23,7 +18,6 @@ import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.UUID
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionOperator as VisibilityConditionOperatorDto
import com.betriebsratkanzlei.legalconsenthub_api.model.VisibilityConditionType as VisibilityConditionTypeDto
@@ -33,11 +27,6 @@ class ApplicationFormFormatService(
private val richTextToLatexConverter: RichTextToLatexConverter,
private val pdfRenderer: LatexPdfRenderer,
) {
fun generatePdf(applicationForm: ApplicationForm): ByteArray {
val latexContent = generateLatex(applicationForm)
return pdfRenderer.render(latexContent)
}
fun generatePdf(
snapshot: ApplicationFormSnapshotDto,
createdAt: Instant?,
@@ -46,17 +35,6 @@ class ApplicationFormFormatService(
return pdfRenderer.render(latexContent)
}
fun generateLatex(applicationForm: ApplicationForm): String {
val filteredForm = filterVisibleElements(applicationForm)
val exportModel = buildLatexExportModel(filteredForm)
val context =
Context().apply {
setVariable("applicationForm", exportModel)
}
return templateEngine.process("application_form_latex_template", context)
}
fun generateLatex(
snapshot: ApplicationFormSnapshotDto,
createdAt: Instant?,
@@ -71,50 +49,6 @@ class ApplicationFormFormatService(
return templateEngine.process("application_form_latex_template", context)
}
fun generateHtml(applicationForm: ApplicationForm): String {
val filteredForm = filterVisibleElements(applicationForm)
val context =
Context().apply {
setVariable("applicationForm", filteredForm)
}
return templateEngine.process("application_form_template", context)
}
private fun buildLatexExportModel(applicationForm: ApplicationForm): LatexExportModel {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
return LatexExportModel(
id = applicationForm.id,
name = LatexEscaper.escape(applicationForm.name),
organizationId = LatexEscaper.escape(applicationForm.organizationId),
employer = LatexEscaper.escape("Arbeitgeber der Organisation ${applicationForm.organizationId}"),
worksCouncil = LatexEscaper.escape("Betriebsrat der Organisation ${applicationForm.organizationId}"),
createdAt = applicationForm.createdAt?.atZone(ZoneId.of("Europe/Berlin"))?.format(dateFormatter) ?: "",
sections =
applicationForm.formElementSections.map { section ->
LatexSection(
title = LatexEscaper.escape(section.title),
description = LatexEscaper.escape(section.description),
subsections =
section.formElementSubSections.map { subsection ->
LatexSubSection(
title = LatexEscaper.escape(subsection.title),
subtitle = LatexEscaper.escape(subsection.subtitle),
elements =
subsection.formElements.map { element ->
LatexFormElement(
title = LatexEscaper.escape(element.title),
description = LatexEscaper.escape(element.description),
value = renderElementValue(element),
)
},
)
},
)
},
)
}
private fun buildLatexExportModel(
snapshot: ApplicationFormSnapshotDto,
createdAt: Instant?,
@@ -153,48 +87,6 @@ class ApplicationFormFormatService(
)
}
private fun renderElementValue(element: FormElement): String =
when (element.type.name) {
"TEXTFIELD", "TEXTAREA" -> {
val value = element.options.firstOrNull()?.value
if (value.isNullOrBlank()) "Keine Eingabe" else LatexEscaper.escape(value)
}
"DATE" -> {
val value = element.options.firstOrNull()?.value
if (value.isNullOrBlank()) {
"Kein Datum ausgewählt"
} else {
try {
LocalDate.parse(value).format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} catch (e: Exception) {
LatexEscaper.escape(value)
}
}
}
"RICH_TEXT" -> {
val value = element.options.firstOrNull()?.value
if (value.isNullOrBlank()) "Keine Eingabe" else richTextToLatexConverter.convertToLatex(value)
}
"SELECT", "CHECKBOX" -> {
val selected = element.options.filter { it.value == "true" }.map { it.label }
if (selected.isEmpty()) {
"Keine Auswahl getroffen"
} else {
selected.joinToString(
", ",
) { LatexEscaper.escape(it) }
}
}
"RADIOBUTTON" -> {
val selected = element.options.firstOrNull { it.value == "true" }?.label
if (selected == null) "Keine Auswahl getroffen" else LatexEscaper.escape(selected)
}
"SWITCH" -> {
if (element.options.any { it.value == "true" }) "Ja" else "Nein"
}
else -> "Keine Auswahl getroffen"
}
private fun renderElementValue(element: FormElementSnapshotDto): String =
when (element.type.name) {
"TEXTFIELD", "TEXTAREA" -> {
@@ -237,66 +129,6 @@ class ApplicationFormFormatService(
else -> "Keine Auswahl getroffen"
}
private fun filterVisibleElements(applicationForm: ApplicationForm): ApplicationForm {
val allElements = collectAllFormElements(applicationForm)
val formElementsByRef = buildFormElementsByRefMap(allElements)
val visibilityMap = evaluateVisibility(allElements, formElementsByRef)
val filteredSections =
applicationForm.formElementSections
.filter { !it.isTemplate }
.mapNotNull { section ->
val filteredSubSections =
section.formElementSubSections
.mapNotNull { subsection ->
val filteredElements =
subsection.formElements.filter { element ->
visibilityMap[element.id] ?: true
}
if (filteredElements.isEmpty()) {
null
} else {
FormElementSubSection(
id = subsection.id,
title = subsection.title,
subtitle = subsection.subtitle,
formElements = filteredElements.toMutableList(),
formElementSection = null,
)
}
}
if (filteredSubSections.isEmpty()) {
null
} else {
FormElementSection(
id = section.id,
title = section.title,
shortTitle = section.shortTitle,
description = section.description,
isTemplate = section.isTemplate,
templateReference = section.templateReference,
titleTemplate = section.titleTemplate,
spawnedFromElementReference = section.spawnedFromElementReference,
formElementSubSections = filteredSubSections.toMutableList(),
applicationForm = null,
)
}
}
return ApplicationForm(
id = applicationForm.id,
name = applicationForm.name,
status = applicationForm.status,
createdBy = applicationForm.createdBy,
lastModifiedBy = applicationForm.lastModifiedBy,
createdAt = applicationForm.createdAt,
modifiedAt = applicationForm.modifiedAt,
isTemplate = applicationForm.isTemplate,
organizationId = applicationForm.organizationId,
formElementSections = filteredSections.toMutableList(),
)
}
private fun filterVisibleElements(snapshot: ApplicationFormSnapshotDto): ApplicationFormSnapshotDto {
val allElements = collectAllFormElements(snapshot)
val formElementsByRef = buildSnapshotFormElementsByRefMap(allElements)
@@ -319,21 +151,11 @@ class ApplicationFormFormatService(
return snapshot.copy(sections = filteredSections)
}
private fun collectAllFormElements(applicationForm: ApplicationForm): List<FormElement> =
applicationForm.formElementSections
.flatMap { it.formElementSubSections }
.flatMap { it.formElements }
private fun collectAllFormElements(snapshot: ApplicationFormSnapshotDto): List<FormElementSnapshotDto> =
snapshot.sections
.flatMap(FormElementSectionSnapshotDto::subsections)
.flatMap(FormElementSubSectionSnapshotDto::elements)
private fun buildFormElementsByRefMap(allElements: List<FormElement>): Map<String, FormElement> =
allElements
.mapNotNull { elem -> elem.reference?.let { it to elem } }
.toMap()
private fun buildSnapshotFormElementsByRefMap(
allElements: List<FormElementSnapshotDto>,
): Map<String, FormElementSnapshotDto> =
@@ -341,38 +163,6 @@ class ApplicationFormFormatService(
.mapNotNull { elem -> elem.reference?.let { it to elem } }
.toMap()
private fun evaluateVisibility(
allElements: List<FormElement>,
formElementsByRef: Map<String, FormElement>,
): Map<UUID?, Boolean> {
val visibilityMap = mutableMapOf<UUID?, Boolean>()
allElements.forEach { element ->
visibilityMap[element.id] = isElementVisible(element, formElementsByRef)
}
return visibilityMap
}
private fun isElementVisible(
element: FormElement,
formElementsByRef: Map<String, FormElement>,
): Boolean {
val condition = element.visibilityCondition ?: return true
// Don't show if source element is missing
val sourceElement = formElementsByRef[condition.sourceFormElementReference] ?: return false
val sourceValue = getFormElementValue(sourceElement)
val conditionMet =
evaluateCondition(sourceValue, condition.formElementExpectedValue, condition.formElementOperator)
return when (condition.formElementConditionType) {
VisibilityConditionType.SHOW -> conditionMet
VisibilityConditionType.HIDE -> !conditionMet
}
}
private fun isElementVisible(
element: FormElementSnapshotDto,
formElementsByRef: Map<String, FormElementSnapshotDto>,
@@ -391,17 +181,6 @@ class ApplicationFormFormatService(
}
}
private fun getFormElementValue(element: FormElement): String =
when (element.type.name) {
"SELECT",
"RADIOBUTTON",
-> element.options.firstOrNull { it.value == "true" }?.label ?: ""
"CHECKBOX",
"SWITCH",
-> if (element.options.any { it.value == "true" }) "true" else "false"
else -> element.options.firstOrNull()?.value ?: ""
}
private fun getFormElementValue(element: FormElementSnapshotDto): String =
when (element.type.name) {
"SELECT",
@@ -413,18 +192,6 @@ class ApplicationFormFormatService(
else -> element.options.firstOrNull()?.value ?: ""
}
private fun evaluateCondition(
actualValue: String,
expectedValue: String,
operator: VisibilityConditionOperator,
): Boolean =
when (operator) {
VisibilityConditionOperator.EQUALS -> actualValue.equals(expectedValue, ignoreCase = true)
VisibilityConditionOperator.NOT_EQUALS -> !actualValue.equals(expectedValue, ignoreCase = true)
VisibilityConditionOperator.IS_EMPTY -> actualValue.isEmpty()
VisibilityConditionOperator.IS_NOT_EMPTY -> actualValue.isNotEmpty()
}
private fun evaluateCondition(
actualValue: String,
expectedValue: String,

View File

@@ -1,223 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title th:text="${applicationForm.name}"></title>
<style>
body {
font-family: 'Times New Roman', Times, serif;
line-height: 1.4;
margin: 40px;
color: #333;
}
header {
text-align: center;
margin-bottom: 40px;
}
header img.logo {
max-height: 80px;
margin-bottom: 10px;
}
header h1 {
font-size: 1.8em;
letter-spacing: 1px;
margin: 0;
}
.meta {
margin-bottom: 30px;
}
.meta .field {
margin-bottom: 5px;
}
.meta label {
font-weight: bold;
display: inline-block;
width: 150px;
}
.contract-section {
margin-bottom: 25px;
}
.contract-section h2 {
font-size: 1.3em;
border-bottom: 1px solid #aaa;
padding-bottom: 5px;
margin-bottom: 15px;
}
.form-section {
margin-bottom: 30px;
}
.form-section h3 {
font-size: 1.2em;
color: #444;
margin-bottom: 10px;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.form-section p.description {
font-style: italic;
color: #666;
margin: 5px 0 15px;
}
.form-subsection {
margin-left: 10px;
margin-bottom: 20px;
}
.form-subsection h4 {
font-size: 1.15em;
color: #555;
margin-bottom: 8px;
}
.form-subsection p.description {
font-style: italic;
color: #666;
margin: 5px 0 10px;
}
.form-element {
margin-bottom: 20px;
margin-left: 20px;
}
.form-element h5 {
font-size: 1.05em;
margin-bottom: 5px;
}
.form-element p.description {
font-style: italic;
color: #666;
margin: 5px 0 10px;
}
footer {
text-align: center;
font-size: 0.9em;
color: #666;
margin-top: 50px;
}
</style>
</head>
<body>
<header>
<h1 th:text="${applicationForm.name}"></h1>
</header>
<div class="meta">
<div class="field">
<label>ID:</label>
<span th:text="${applicationForm.id}"></span>
</div>
<div class="field">
<label>Erstellt von:</label>
<span th:text="${applicationForm.createdBy.name}"></span>
</div>
<div class="field">
<label>Erstellt am:</label>
<span th:text="${#temporals.format(applicationForm.createdAt, 'dd.MM.yyyy')}"></span>
</div>
<div class="field">
<label>Organisation:</label>
<span th:text="${applicationForm.organizationId}"></span>
</div>
</div>
<div class="contract-section">
<h2>Formularelemente</h2>
<div th:each="section : ${applicationForm.formElementSections}" class="form-section">
<h3 th:text="${section.title}"></h3>
<p class="description" th:if="${section.description}" th:text="${section.description}"></p>
<div th:each="subsection : ${section.formElementSubSections}" class="form-subsection">
<h4 th:text="${subsection.title}"></h4>
<p class="description" th:if="${subsection.subtitle}" th:text="${subsection.subtitle}"></p>
<div th:each="elem : ${subsection.formElements}" class="form-element">
<h5 th:text="${elem.title}"></h5>
<p class="description" th:if="${elem.description}" th:text="${elem.description}"></p>
<div th:switch="${elem.type.name()}">
<div th:case="'TEXTFIELD'">
<div th:each="option : ${elem.options}">
<p th:if="${!option.value.isEmpty()}" th:text="${option.value}"></p>
</div>
<p th:if="${elem.options.isEmpty() || elem.options.?[!value.isEmpty()].isEmpty()}">Keine Eingabe</p>
</div>
<div th:case="'TEXTAREA'">
<div th:each="option : ${elem.options}">
<p th:if="${!option.value.isEmpty()}" th:text="${option.value}"></p>
</div>
<p th:if="${elem.options.isEmpty() || elem.options.?[!value.isEmpty()].isEmpty()}">Keine Eingabe</p>
</div>
<div th:case="'DATE'">
<div th:each="option : ${elem.options}">
<p th:if="${!option.value.isEmpty()}"
th:with="dateValue=${#temporals.createDate(option.value, 'yyyy-MM-dd')}"
th:text="${#temporals.format(dateValue, 'dd.MM.yyyy')}"></p>
</div>
<p th:if="${elem.options.isEmpty() || elem.options.?[!value.isEmpty()].isEmpty()}">Kein Datum ausgewählt</p>
</div>
<div th:case="'RICH_TEXT'">
<div th:each="option : ${elem.options}">
<div th:if="${!option.value.isEmpty()}" th:utext="${option.value}"></div>
</div>
<p th:if="${elem.options.isEmpty() || elem.options.?[!value.isEmpty()].isEmpty()}">Keine Eingabe</p>
</div>
<div th:case="'SELECT'">
<ul>
<li th:each="option : ${elem.options}" th:if="${option.value == 'true'}" th:text="${option.label}"></li>
</ul>
<p th:if="${elem.options.?[value == 'true'].isEmpty()}">Keine Auswahl getroffen</p>
</div>
<div th:case="'RADIOBUTTON'">
<p th:each="option : ${elem.options}" th:if="${option.value == 'true'}" th:text="${option.label}"></p>
<p th:if="${elem.options.?[value == 'true'].isEmpty()}">Keine Auswahl getroffen</p>
</div>
<div th:case="'CHECKBOX'">
<ul>
<li th:each="option : ${elem.options}" th:if="${option.value == 'true'}" th:text="${option.label}"></li>
</ul>
<p th:if="${elem.options.?[value == 'true'].isEmpty()}">Keine Auswahl getroffen</p>
</div>
<div th:case="'SWITCH'">
<p th:if="${elem.options.?[value == 'true'].isEmpty()}">Nein</p>
<p th:if="${!elem.options.?[value == 'true'].isEmpty()}">Ja</p>
</div>
<div th:case="*">
<ul>
<li th:each="option : ${elem.options}" th:if="${option.value == 'true'}" th:text="${option.label}"></li>
</ul>
<p th:if="${elem.options.?[value == 'true'].isEmpty()}">Keine Auswahl getroffen</p>
</div>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>Dieses Dokument wurde automatisch erzeugt und ist ohne Unterschrift gültig.</p>
</footer>
</body>
</html>