From d553668893978205ba8ac07fe19bb9e5f18a61bd Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sun, 25 May 2025 10:35:18 +0200 Subject: [PATCH] feat(fullstack): Add title and description to form element, add HTML and PDF endpoints for application form --- .../api/legalconsenthub.yml | 67 ++++++++++ legalconsenthub-backend/build.gradle | 10 ++ .../ApplicationFormController.kt | 22 ++++ .../ApplicationFormFormatService.kt | 31 +++++ .../form_element/FormElement.kt | 6 +- .../form_element/FormElementMapper.kt | 6 + .../templates/application_form_template.html | 119 ++++++++++++++++++ legalconsenthub/components/FormEngine.vue | 2 + testdata.json | 10 ++ 9 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt create mode 100644 legalconsenthub-backend/src/main/resources/templates/application_form_template.html diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index af782cb..dfce7d7 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -134,6 +134,65 @@ paths: "503": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + /application-forms/{id}/pdf: + get: + summary: Returns the application form rendered as PDF + operationId: getApplicationFormPdf + tags: + - application-form + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Application form as PDF + content: + application/pdf: + schema: + type: string + format: binary + "400": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest" + "401": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "500": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + "503": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + + /application-forms/{id}/html: + get: + summary: Returns the application form rendered as HTML + operationId: getApplicationFormHtml + tags: + - application-form + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Application form as HTML + content: + text/html: + schema: + type: string + "400": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest" + "401": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "500": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + "503": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + ####### Application Form Templates ####### /application-form-templates: get: @@ -684,6 +743,10 @@ components: applicationFormId: type: string format: uuid + title: + type: string + description: + type: string options: type: array items: @@ -697,6 +760,10 @@ components: - options - type properties: + title: + type: string + description: + type: string options: type: array items: diff --git a/legalconsenthub-backend/build.gradle b/legalconsenthub-backend/build.gradle index 9f9d09c..5ca194c 100644 --- a/legalconsenthub-backend/build.gradle +++ b/legalconsenthub-backend/build.gradle @@ -20,6 +20,10 @@ repositories { mavenCentral() } +ext { + openHtmlVersion = '1.0.10' +} + dependencies { implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' @@ -31,6 +35,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation "com.openhtmltopdf:openhtmltopdf-core:$openHtmlVersion" + implementation "com.openhtmltopdf:openhtmltopdf-pdfbox:$openHtmlVersion" + implementation "com.openhtmltopdf:openhtmltopdf-java2d:$openHtmlVersion" + implementation "com.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlVersion" + implementation "com.openhtmltopdf:openhtmltopdf-svg-support:$openHtmlVersion" runtimeOnly 'com.h2database:h2' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt index 340dd8a..35ade87 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormController.kt @@ -4,6 +4,10 @@ import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RestController import java.util.UUID @@ -13,6 +17,7 @@ class ApplicationFormController( val applicationFormService: ApplicationFormService, val pagedApplicationFormMapper: PagedApplicationFormMapper, val applicationFormMapper: ApplicationFormMapper, + val applicationFormFormatService: ApplicationFormFormatService ) : ApplicationFormApi { override fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ResponseEntity { @@ -40,6 +45,23 @@ class ApplicationFormController( ) } + override fun getApplicationFormHtml(id: UUID): ResponseEntity { + val applicationForm = applicationFormService.getApplicationFormById(id) + return ResponseEntity.ok( + applicationFormFormatService.generateHtml(applicationForm) + ) + } + + override fun getApplicationFormPdf(id: UUID): ResponseEntity { + val applicationForm = applicationFormService.getApplicationFormById(id) + val pdfBytes = applicationFormFormatService.generatePdf(applicationForm) + val resource = ByteArrayResource(pdfBytes) + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"form-$id.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(resource) + } + override fun updateApplicationForm( id: UUID, applicationFormDto: ApplicationFormDto 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 new file mode 100644 index 0000000..7721804 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormFormatService.kt @@ -0,0 +1,31 @@ +package com.betriebsratkanzlei.legalconsenthub.application_form + +import org.springframework.stereotype.Service +import org.thymeleaf.TemplateEngine +import org.thymeleaf.context.Context +import java.io.ByteArrayOutputStream +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder + +@Service +class ApplicationFormFormatService( + private val templateEngine: TemplateEngine +) { + fun generatePdf(applicationForm: ApplicationForm): ByteArray { + val htmlContent = generateHtml(applicationForm) + + val outputStream = ByteArrayOutputStream() + PdfRendererBuilder().useFastMode() + .withHtmlContent(htmlContent, null) + .toStream(outputStream) + .run() + + return outputStream.toByteArray() + } + + fun generateHtml(applicationForm: ApplicationForm): String { + val context = Context().apply { + setVariable("applicationForm", applicationForm) + } + return templateEngine.process("application_form_template", context) + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElement.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElement.kt index b57f329..4164067 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElement.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElement.kt @@ -17,7 +17,7 @@ import jakarta.persistence.OneToMany import java.util.UUID; @Entity -class FormElement ( +class FormElement( @Id @GeneratedValue var id: UUID? = null, @@ -26,6 +26,10 @@ class FormElement ( @JoinColumn(name = "application_form_id", nullable = false) var applicationForm: ApplicationForm? = null, + var title: String? = null, + + var description: String? = null, + @ElementCollection @CollectionTable(name = "form_element_options", joinColumns = [JoinColumn(name = "form_element_id")]) var options: MutableList = mutableListOf(), diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElementMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElementMapper.kt index 4a8c466..1891a4a 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElementMapper.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/form_element/FormElementMapper.kt @@ -15,6 +15,8 @@ class FormElementMapper( fun toFormElementDto(formElement: FormElement): FormElementDto { return FormElementDto( id = formElement.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"), + title = formElement.title, + description = formElement.description, options = formElement.options.map { formOptionMapper.toFormOptionDto(it) }, type = formElement.type, applicationFormId = formElement.applicationForm?.id @@ -28,6 +30,8 @@ class FormElementMapper( return FormElement( id = formElement.id, + title = formElement.title, + description = formElement.description, options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), type = formElement.type, applicationForm = applicationForm @@ -37,6 +41,8 @@ class FormElementMapper( fun toFormElement(formElement: CreateFormElementDto, applicationForm: ApplicationForm): FormElement { return FormElement( id = null, + title = formElement.title, + description = formElement.description, options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(), type = formElement.type, applicationForm = applicationForm diff --git a/legalconsenthub-backend/src/main/resources/templates/application_form_template.html b/legalconsenthub-backend/src/main/resources/templates/application_form_template.html new file mode 100644 index 0000000..5081671 --- /dev/null +++ b/legalconsenthub-backend/src/main/resources/templates/application_form_template.html @@ -0,0 +1,119 @@ + + + + + + + + +
+

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Formularelemente

+
+

+

+ +
    +
  • +
+

Keine Auswahl getroffen

+
+
+ +
+

Dieses Dokument wurde automatisch erzeugt und ist ohne Unterschrift gültig.

+
+ + diff --git a/legalconsenthub/components/FormEngine.vue b/legalconsenthub/components/FormEngine.vue index d6905f9..e250ba2 100644 --- a/legalconsenthub/components/FormEngine.vue +++ b/legalconsenthub/components/FormEngine.vue @@ -1,6 +1,8 @@