From 36132a3bef5eb505a06c8c0da49a7bc46ef7e177 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sun, 8 Feb 2026 18:21:07 +0100 Subject: [PATCH] feat(fullstack): Add contact form --- api/legalconsenthub.yml | 39 +++++++ .../contact/ContactController.kt | 23 ++++ .../legalconsenthub/contact/ContactService.kt | 58 +++++++++ .../templates/email/contact_message.html | 35 ++++++ .../app/components/ContactEditor.vue | 58 +++++++++ .../app/composables/contact/useContact.ts | 13 +++ .../app/composables/contact/useContactApi.ts | 24 ++++ legalconsenthub/app/composables/index.ts | 2 + legalconsenthub/app/layouts/default.vue | 15 +-- legalconsenthub/app/pages/contact.vue | 110 ++++++++++++++++++ legalconsenthub/i18n/locales/de.json | 27 +++++ legalconsenthub/i18n/locales/en.json | 27 +++++ 12 files changed, 420 insertions(+), 11 deletions(-) create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactController.kt create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactService.kt create mode 100644 legalconsenthub-backend/src/main/resources/templates/email/contact_message.html create mode 100644 legalconsenthub/app/components/ContactEditor.vue create mode 100644 legalconsenthub/app/composables/contact/useContact.ts create mode 100644 legalconsenthub/app/composables/contact/useContactApi.ts create mode 100644 legalconsenthub/app/pages/contact.vue diff --git a/api/legalconsenthub.yml b/api/legalconsenthub.yml index abf0646..d058709 100644 --- a/api/legalconsenthub.yml +++ b/api/legalconsenthub.yml @@ -1169,6 +1169,29 @@ paths: "500": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + ####### Contact ####### + /contact: + post: + summary: Send a contact message + operationId: sendContactMessage + tags: + - contact + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ContactMessageDto" + responses: + "204": + description: Contact message sent successfully + "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" + /application-forms/{applicationFormId}/files: parameters: - name: applicationFormId @@ -2044,6 +2067,22 @@ components: - WARNING - ERROR + ####### Contact DTOs ####### + ContactMessageDto: + type: object + required: + - subject + - message + properties: + subject: + type: string + minLength: 1 + description: Subject of the contact message + message: + type: string + minLength: 1 + description: HTML body of the contact message + ####### File Upload DTOs ####### UploadedFileDto: type: object diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactController.kt new file mode 100644 index 0000000..0e68f3c --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactController.kt @@ -0,0 +1,23 @@ +package com.betriebsratkanzlei.legalconsenthub.contact + +import com.betriebsratkanzlei.legalconsenthub_api.api.ContactApi +import com.betriebsratkanzlei.legalconsenthub_api.model.ContactMessageDto +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.RestController + +@RestController +class ContactController( + private val contactService: ContactService, +) : ContactApi { + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun sendContactMessage(contactMessageDto: ContactMessageDto): ResponseEntity { + contactService.sendContactMessage( + subject = contactMessageDto.subject, + message = contactMessageDto.message, + ) + return ResponseEntity.noContent().build() + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactService.kt new file mode 100644 index 0000000..875380d --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/contact/ContactService.kt @@ -0,0 +1,58 @@ +package com.betriebsratkanzlei.legalconsenthub.contact + +import com.betriebsratkanzlei.legalconsenthub.email.EmailService +import com.betriebsratkanzlei.legalconsenthub.user.UserService +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.thymeleaf.TemplateEngine +import org.thymeleaf.context.Context + +@Service +class ContactService( + private val emailService: EmailService, + private val userService: UserService, + private val templateEngine: TemplateEngine, +) { + private val logger = LoggerFactory.getLogger(ContactService::class.java) + + companion object { + private const val CONTACT_EMAIL = "kontakt@gremiumhub.de" + } + + fun sendContactMessage( + subject: String, + message: String, + ) { + val currentUser = userService.getCurrentUser() + + val emailBody = + buildContactEmail( + senderName = currentUser.name, + senderEmail = currentUser.email ?: "Keine E-Mail angegeben", + subject = subject, + message = message, + ) + + emailService.sendEmail( + to = CONTACT_EMAIL, + subject = "Kontaktanfrage: $subject", + body = emailBody, + ) + + logger.info("Contact message sent from user ${currentUser.keycloakId} with subject: $subject") + } + + private fun buildContactEmail( + senderName: String, + senderEmail: String, + subject: String, + message: String, + ): String { + val context = Context() + context.setVariable("senderName", senderName) + context.setVariable("senderEmail", senderEmail) + context.setVariable("subject", subject) + context.setVariable("message", message) + return templateEngine.process("email/contact_message", context) + } +} diff --git a/legalconsenthub-backend/src/main/resources/templates/email/contact_message.html b/legalconsenthub-backend/src/main/resources/templates/email/contact_message.html new file mode 100644 index 0000000..efd8434 --- /dev/null +++ b/legalconsenthub-backend/src/main/resources/templates/email/contact_message.html @@ -0,0 +1,35 @@ + + + + + + + +
+
+

Neue Kontaktanfrage

+
+
+

Eine neue Kontaktanfrage wurde über GremiumHub gesendet:

+
    +
  • Absender:
  • +
  • E-Mail:
  • +
  • Betreff:
  • +
+

Nachricht:

+
+
+ +
+ + diff --git a/legalconsenthub/app/components/ContactEditor.vue b/legalconsenthub/app/components/ContactEditor.vue new file mode 100644 index 0000000..4c033f8 --- /dev/null +++ b/legalconsenthub/app/components/ContactEditor.vue @@ -0,0 +1,58 @@ + + + diff --git a/legalconsenthub/app/composables/contact/useContact.ts b/legalconsenthub/app/composables/contact/useContact.ts new file mode 100644 index 0000000..69e5bdb --- /dev/null +++ b/legalconsenthub/app/composables/contact/useContact.ts @@ -0,0 +1,13 @@ +import { useContactApi } from './useContactApi' + +export function useContact() { + const { sendContactMessage: sendContactMessageApi } = useContactApi() + + async function sendContactMessage(subject: string, message: string): Promise { + return sendContactMessageApi({ subject, message }) + } + + return { + sendContactMessage + } +} diff --git a/legalconsenthub/app/composables/contact/useContactApi.ts b/legalconsenthub/app/composables/contact/useContactApi.ts new file mode 100644 index 0000000..69157b4 --- /dev/null +++ b/legalconsenthub/app/composables/contact/useContactApi.ts @@ -0,0 +1,24 @@ +import { ContactApi, Configuration, type ContactMessageDto } from '~~/.api-client' +import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' + +export function useContactApi() { + const appBaseUrl = useRuntimeConfig().app.baseURL + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public + + const basePath = withoutTrailingSlash( + cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : clientProxyBasePath + serverApiBasePath) + ) + + const contactApiClient = new ContactApi( + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) + ) + + async function sendContactMessage(contactMessageDto: ContactMessageDto): Promise { + return contactApiClient.sendContactMessage({ contactMessageDto }) + } + + return { + sendContactMessage + } +} diff --git a/legalconsenthub/app/composables/index.ts b/legalconsenthub/app/composables/index.ts index 229ab0f..84818da 100644 --- a/legalconsenthub/app/composables/index.ts +++ b/legalconsenthub/app/composables/index.ts @@ -5,3 +5,5 @@ export { useApplicationFormVersionApi } from './applicationFormVersion/useApplic export { useNotificationApi } from './notification/useNotificationApi' export { useUser } from './user/useUser' export { useUserApi } from './user/useUserApi' +export { useContact } from './contact/useContact' +export { useContactApi } from './contact/useContactApi' diff --git a/legalconsenthub/app/layouts/default.vue b/legalconsenthub/app/layouts/default.vue index 5037cb1..7902483 100644 --- a/legalconsenthub/app/layouts/default.vue +++ b/legalconsenthub/app/layouts/default.vue @@ -55,20 +55,13 @@ const { t: $t } = useI18n() const links = [ [ { - label: $t('common.general'), - icon: 'i-lucide-user', - to: '/settings', + label: $t('contact.title'), + icon: 'i-lucide-mail', + to: '/contact', exact: true } ], - [ - { - label: $t('common.general'), - icon: 'i-lucide-user', - to: '/settings', - exact: true - } - ] + [] ] const open = ref(false) const logger = useLogger().withTag('layout') diff --git a/legalconsenthub/app/pages/contact.vue b/legalconsenthub/app/pages/contact.vue new file mode 100644 index 0000000..51c37b6 --- /dev/null +++ b/legalconsenthub/app/pages/contact.vue @@ -0,0 +1,110 @@ + + + diff --git a/legalconsenthub/i18n/locales/de.json b/legalconsenthub/i18n/locales/de.json index 1fce54e..bc8c57e 100644 --- a/legalconsenthub/i18n/locales/de.json +++ b/legalconsenthub/i18n/locales/de.json @@ -263,5 +263,32 @@ "onCommentAdded": "Wenn jemand meinen Antrag kommentiert", "saved": "Einstellungen gespeichert" } + }, + "contact": { + "title": "Kontakt", + "header": "Kontaktieren Sie unsere Experten", + "description": "Haben Sie Fragen zur Mitbestimmung, zum Datenschutz oder zu IT-Systemen? Unsere Experten helfen Ihnen gerne weiter.", + "experts": { + "laborLaw": "Fachanwalt für Arbeitsrecht", + "itConsultant": "IT-Consultant", + "dataProtection": "Datenschutzbeauftragter", + "itSecurity": "IT-Sicherheitsbeauftragter" + }, + "form": { + "title": "Nachricht senden", + "description": "Beschreiben Sie Ihr Anliegen und wir melden uns schnellstmöglich bei Ihnen.", + "subject": "Betreff", + "subjectPlaceholder": "Worum geht es in Ihrer Anfrage?", + "message": "Nachricht", + "messagePlaceholder": "Beschreiben Sie Ihr Anliegen", + "submit": "Nachricht senden" + }, + "success": { + "title": "Nachricht gesendet", + "description": "Ihre Nachricht wurde erfolgreich gesendet. Wir werden uns so schnell wie möglich bei Ihnen melden." + }, + "error": { + "description": "Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut." + } } } diff --git a/legalconsenthub/i18n/locales/en.json b/legalconsenthub/i18n/locales/en.json index 3e8a439..ae7be9a 100644 --- a/legalconsenthub/i18n/locales/en.json +++ b/legalconsenthub/i18n/locales/en.json @@ -263,5 +263,32 @@ "onCommentAdded": "When someone comments on my application form", "saved": "Settings saved" } + }, + "contact": { + "title": "Contact", + "header": "Contact Our Experts", + "description": "Do you have questions about co-determination, data protection, or IT systems? Our experts are happy to help.", + "experts": { + "laborLaw": "Labor Law Attorney", + "itConsultant": "IT Consultant", + "dataProtection": "Data Protection Officer", + "itSecurity": "IT Security Officer" + }, + "form": { + "title": "Send a Message", + "description": "Describe your inquiry and we will get back to you as soon as possible.", + "subject": "Subject", + "subjectPlaceholder": "What is your inquiry about?", + "message": "Message", + "messagePlaceholder": "Describe your inquiry", + "submit": "Send Message" + }, + "success": { + "title": "Message Sent", + "description": "Your message has been sent successfully. We will get back to you as soon as possible." + }, + "error": { + "description": "The message could not be sent. Please try again later." + } } }