feat(fullstack): Add title and description to form element, add HTML and PDF endpoints for application form
This commit is contained in:
@@ -134,6 +134,65 @@ paths:
|
|||||||
"503":
|
"503":
|
||||||
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
$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 #######
|
||||||
/application-form-templates:
|
/application-form-templates:
|
||||||
get:
|
get:
|
||||||
@@ -684,6 +743,10 @@ components:
|
|||||||
applicationFormId:
|
applicationFormId:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
options:
|
options:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -697,6 +760,10 @@ components:
|
|||||||
- options
|
- options
|
||||||
- type
|
- type
|
||||||
properties:
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
options:
|
options:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
openHtmlVersion = '1.0.10'
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
|
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect'
|
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-oauth2-resource-server'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
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'
|
runtimeOnly 'com.h2database:h2'
|
||||||
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import com.betriebsratkanzlei.legalconsenthub_api.api.ApplicationFormApi
|
|||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedApplicationFormDto
|
||||||
|
import org.springframework.core.io.ByteArrayResource
|
||||||
|
import org.springframework.core.io.Resource
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -13,6 +17,7 @@ class ApplicationFormController(
|
|||||||
val applicationFormService: ApplicationFormService,
|
val applicationFormService: ApplicationFormService,
|
||||||
val pagedApplicationFormMapper: PagedApplicationFormMapper,
|
val pagedApplicationFormMapper: PagedApplicationFormMapper,
|
||||||
val applicationFormMapper: ApplicationFormMapper,
|
val applicationFormMapper: ApplicationFormMapper,
|
||||||
|
val applicationFormFormatService: ApplicationFormFormatService
|
||||||
) : ApplicationFormApi {
|
) : ApplicationFormApi {
|
||||||
|
|
||||||
override fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ResponseEntity<ApplicationFormDto> {
|
override fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ResponseEntity<ApplicationFormDto> {
|
||||||
@@ -40,6 +45,23 @@ class ApplicationFormController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getApplicationFormHtml(id: UUID): ResponseEntity<String> {
|
||||||
|
val applicationForm = applicationFormService.getApplicationFormById(id)
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
applicationFormFormatService.generateHtml(applicationForm)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApplicationFormPdf(id: UUID): ResponseEntity<Resource> {
|
||||||
|
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(
|
override fun updateApplicationForm(
|
||||||
id: UUID,
|
id: UUID,
|
||||||
applicationFormDto: ApplicationFormDto
|
applicationFormDto: ApplicationFormDto
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import jakarta.persistence.OneToMany
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class FormElement (
|
class FormElement(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
var id: UUID? = null,
|
var id: UUID? = null,
|
||||||
@@ -26,6 +26,10 @@ class FormElement (
|
|||||||
@JoinColumn(name = "application_form_id", nullable = false)
|
@JoinColumn(name = "application_form_id", nullable = false)
|
||||||
var applicationForm: ApplicationForm? = null,
|
var applicationForm: ApplicationForm? = null,
|
||||||
|
|
||||||
|
var title: String? = null,
|
||||||
|
|
||||||
|
var description: String? = null,
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@CollectionTable(name = "form_element_options", joinColumns = [JoinColumn(name = "form_element_id")])
|
@CollectionTable(name = "form_element_options", joinColumns = [JoinColumn(name = "form_element_id")])
|
||||||
var options: MutableList<FormOption> = mutableListOf(),
|
var options: MutableList<FormOption> = mutableListOf(),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class FormElementMapper(
|
|||||||
fun toFormElementDto(formElement: FormElement): FormElementDto {
|
fun toFormElementDto(formElement: FormElement): FormElementDto {
|
||||||
return FormElementDto(
|
return FormElementDto(
|
||||||
id = formElement.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
|
id = formElement.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
|
||||||
|
title = formElement.title,
|
||||||
|
description = formElement.description,
|
||||||
options = formElement.options.map { formOptionMapper.toFormOptionDto(it) },
|
options = formElement.options.map { formOptionMapper.toFormOptionDto(it) },
|
||||||
type = formElement.type,
|
type = formElement.type,
|
||||||
applicationFormId = formElement.applicationForm?.id
|
applicationFormId = formElement.applicationForm?.id
|
||||||
@@ -28,6 +30,8 @@ class FormElementMapper(
|
|||||||
|
|
||||||
return FormElement(
|
return FormElement(
|
||||||
id = formElement.id,
|
id = formElement.id,
|
||||||
|
title = formElement.title,
|
||||||
|
description = formElement.description,
|
||||||
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
||||||
type = formElement.type,
|
type = formElement.type,
|
||||||
applicationForm = applicationForm
|
applicationForm = applicationForm
|
||||||
@@ -37,6 +41,8 @@ class FormElementMapper(
|
|||||||
fun toFormElement(formElement: CreateFormElementDto, applicationForm: ApplicationForm): FormElement {
|
fun toFormElement(formElement: CreateFormElementDto, applicationForm: ApplicationForm): FormElement {
|
||||||
return FormElement(
|
return FormElement(
|
||||||
id = null,
|
id = null,
|
||||||
|
title = formElement.title,
|
||||||
|
description = formElement.description,
|
||||||
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
options = formElement.options.map { formOptionMapper.toFormOption(it) }.toMutableList(),
|
||||||
type = formElement.type,
|
type = formElement.type,
|
||||||
applicationForm = applicationForm
|
applicationForm = applicationForm
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<!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-element {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-element h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
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="elem : ${applicationForm.formElements}" class="form-element">
|
||||||
|
<h3 th:text="${elem.title}"></h3>
|
||||||
|
<p class="description" th:if="${elem.description}" th:text="${elem.description}"></p>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Dieses Dokument wurde automatisch erzeugt und ist ohne Unterschrift gültig.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id">
|
<template v-for="(formElement, index) in props.modelValue" :key="formElement.id">
|
||||||
<div class="group py-3 lg:py-4">
|
<div class="group py-3 lg:py-4">
|
||||||
|
<p v-if="formElement.title" class="font-semibold">{{ formElement.title }}</p>
|
||||||
|
<p v-if="formElement.description" class="text-dimmed pb-3">{{ formElement.description }}</p>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<component
|
<component
|
||||||
:is="getResolvedComponent(formElement)"
|
:is="getResolvedComponent(formElement)"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"lastModifiedBy": "Denis",
|
"lastModifiedBy": "Denis",
|
||||||
"formElements": [
|
"formElements": [
|
||||||
{
|
{
|
||||||
|
"title": "Zustimmung erforderlich",
|
||||||
|
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"value": "false",
|
"value": "false",
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
"type": "SWITCH"
|
"type": "SWITCH"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"title": "Zustimmung erforderlich",
|
||||||
|
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"value": "false",
|
"value": "false",
|
||||||
@@ -27,6 +31,8 @@
|
|||||||
"type": "SWITCH"
|
"type": "SWITCH"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"title": "Zustimmung erforderlich",
|
||||||
|
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"value": "false",
|
"value": "false",
|
||||||
@@ -38,6 +44,8 @@
|
|||||||
"type": "CHECKBOX"
|
"type": "CHECKBOX"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"title": "Eine weitere Zustimmung erforderlich",
|
||||||
|
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"value": "false",
|
"value": "false",
|
||||||
@@ -55,6 +63,8 @@
|
|||||||
"type": "SELECT"
|
"type": "SELECT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"title": "Eine weitere Zustimmung erforderlich",
|
||||||
|
"description": "Bitte wählen Sie eine Option aus, um fortzufahren.",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"value": "false",
|
"value": "false",
|
||||||
|
|||||||
Reference in New Issue
Block a user