feat(#23): Add email notifications

This commit is contained in:
2025-11-23 18:46:14 +01:00
parent e769bfb011
commit b72d564868
26 changed files with 613 additions and 18 deletions

View File

@@ -539,6 +539,42 @@ paths:
"503":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
/users/{id}/email-preferences:
parameters:
- name: id
in: path
required: true
schema:
type: string
put:
summary: Update user email preferences
operationId: updateUserEmailPreferences
tags:
- user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateEmailPreferencesDto"
responses:
"200":
description: Email preferences updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/UserDto"
"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"
"404":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound"
"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"
####### Comments #######
/comments/{id}:
parameters:
@@ -1426,6 +1462,26 @@ components:
organizationId:
type: string
nullable: true
email:
type: string
nullable: true
emailOnFormCreated:
type: boolean
default: true
emailOnFormSubmitted:
type: boolean
default: true
UpdateEmailPreferencesDto:
type: object
properties:
email:
type: string
nullable: true
emailOnFormCreated:
type: boolean
emailOnFormSubmitted:
type: boolean
UserStatus:
type: string

View File

@@ -1,3 +1,5 @@
# Same file as .env in Synology docker directory
# Database Configuration
LEGALCONSENTHUB_POSTGRES_USER=legalconsenthub
LEGALCONSENTHUB_POSTGRES_PASSWORD=legalconsenthub
@@ -8,6 +10,8 @@ KEYCLOAK_POSTGRES_PASSWORD=keycloak
KEYCLOAK_POSTGRES_DB=keycloak
KEYCLOAK_ISSUER_URL=http://keycloak.lugnas.de
MAIL_HOST=maildev
# Keycloak Configuration
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=

View File

@@ -61,3 +61,12 @@ services:
retries: 5
start_period: 30s
timeout: 10s
maildev:
image: maildev/maildev:2.2.1
container_name: legalconsenthub-maildev
ports:
- "1080:1080"
- "1025:1025"
networks:
- legalconsenthub-net

View File

@@ -127,3 +127,12 @@ services:
retries: 5
start_period: 30s
timeout: 10s
maildev:
image: maildev/maildev:2.2.1
container_name: legalconsenthub-maildev
ports:
- "1080:1080"
- "1025:1025"
networks:
- legalconsenthub-net

View File

@@ -38,6 +38,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation "com.openhtmltopdf:openhtmltopdf-core:$openHtmlVersion"
implementation "com.openhtmltopdf:openhtmltopdf-pdfbox:$openHtmlVersion"
implementation "com.openhtmltopdf:openhtmltopdf-java2d:$openHtmlVersion"

View File

@@ -3,9 +3,11 @@ package com.betriebsratkanzlei.legalconsenthub
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.scheduling.annotation.EnableAsync
@SpringBootApplication
@EnableJpaAuditing
@EnableAsync
class LegalconsenthubApplication
fun main(args: Array<String>) {

View File

@@ -1,6 +1,8 @@
package com.betriebsratkanzlei.legalconsenthub.application_form
import com.betriebsratkanzlei.legalconsenthub.application_form_version.ApplicationFormVersionService
import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormCreatedEvent
import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormSubmittedEvent
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormInvalidStateException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException
@@ -15,6 +17,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
@@ -28,6 +31,7 @@ class ApplicationFormService(
private val notificationService: NotificationService,
private val versionService: ApplicationFormVersionService,
private val userService: UserService,
private val eventPublisher: ApplicationEventPublisher,
) {
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto)
@@ -41,6 +45,15 @@ class ApplicationFormService(
val currentUser = userService.getCurrentUser()
versionService.createVersion(savedApplicationForm, currentUser)
eventPublisher.publishEvent(
ApplicationFormCreatedEvent(
applicationFormId = savedApplicationForm.id!!,
organizationId = savedApplicationForm.organizationId,
creatorName = currentUser.name,
formName = savedApplicationForm.name,
),
)
return savedApplicationForm
}
@@ -111,6 +124,15 @@ class ApplicationFormService(
createNotificationForOrganization(savedApplicationForm)
eventPublisher.publishEvent(
ApplicationFormSubmittedEvent(
applicationFormId = savedApplicationForm.id!!,
organizationId = savedApplicationForm.organizationId,
creatorName = currentUser.name,
formName = savedApplicationForm.name,
),
)
return savedApplicationForm
}

View File

@@ -0,0 +1,10 @@
package com.betriebsratkanzlei.legalconsenthub.email
import java.util.UUID
data class ApplicationFormCreatedEvent(
val applicationFormId: UUID,
val organizationId: String?,
val creatorName: String,
val formName: String,
)

View File

@@ -0,0 +1,10 @@
package com.betriebsratkanzlei.legalconsenthub.email
import java.util.UUID
data class ApplicationFormSubmittedEvent(
val applicationFormId: UUID,
val organizationId: String?,
val creatorName: String,
val formName: String,
)

View File

@@ -0,0 +1,73 @@
package com.betriebsratkanzlei.legalconsenthub.email
import com.betriebsratkanzlei.legalconsenthub.user.UserRepository
import org.slf4j.LoggerFactory
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
@Component
class EmailEventListener(
private val userRepository: UserRepository,
private val emailService: EmailService,
) {
private val logger = LoggerFactory.getLogger(EmailEventListener::class.java)
@Async
@EventListener
fun handleApplicationFormCreated(event: ApplicationFormCreatedEvent) {
logger.info("Processing ApplicationFormCreatedEvent for form: ${event.formName}")
val recipients =
userRepository
.findByOrganizationIdAndEmailOnFormCreatedTrue(event.organizationId)
.filter { !it.email.isNullOrBlank() }
logger.info("Found ${recipients.size} recipients for form created event")
recipients.forEach { user ->
val subject = "Neuer Mitbestimmungsantrag: ${event.formName}"
val body =
emailService.buildFormCreatedEmail(
formName = event.formName,
creatorName = event.creatorName,
applicationFormId = event.applicationFormId,
)
emailService.sendEmail(
to = user.email!!,
subject = subject,
body = body,
)
}
}
@Async
@EventListener
fun handleApplicationFormSubmitted(event: ApplicationFormSubmittedEvent) {
logger.info("Processing ApplicationFormSubmittedEvent for form: ${event.formName}")
val recipients =
userRepository
.findByOrganizationIdAndEmailOnFormSubmittedTrue(event.organizationId)
.filter { !it.email.isNullOrBlank() }
logger.info("Found ${recipients.size} recipients for form submitted event")
recipients.forEach { user ->
val subject = "Mitbestimmungsantrag eingereicht: ${event.formName}"
val body =
emailService.buildFormSubmittedEmail(
formName = event.formName,
creatorName = event.creatorName,
applicationFormId = event.applicationFormId,
)
emailService.sendEmail(
to = user.email!!,
subject = subject,
body = body,
)
}
}
}

View File

@@ -0,0 +1,63 @@
package com.betriebsratkanzlei.legalconsenthub.email
import org.slf4j.LoggerFactory
import org.springframework.mail.MailException
import org.springframework.mail.javamail.JavaMailSender
import org.springframework.mail.javamail.MimeMessageHelper
import org.springframework.stereotype.Service
import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context
import java.util.UUID
@Service
class EmailService(
private val mailSender: JavaMailSender,
private val templateEngine: TemplateEngine,
) {
private val logger = LoggerFactory.getLogger(EmailService::class.java)
fun sendEmail(
to: String,
subject: String,
body: String,
) {
try {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true, "UTF-8")
helper.setTo(to)
helper.setSubject(subject)
helper.setText(body, true)
helper.setFrom("noreply@legalconsenthub.com")
mailSender.send(message)
logger.info("Email sent successfully to: $to")
} catch (e: MailException) {
logger.error("Failed to send email to: $to", e)
}
}
fun buildFormCreatedEmail(
formName: String,
creatorName: String,
applicationFormId: UUID,
): String {
val context = Context()
context.setVariable("formName", formName)
context.setVariable("creatorName", creatorName)
context.setVariable("applicationFormId", applicationFormId)
return templateEngine.process("email/form_created", context)
}
fun buildFormSubmittedEmail(
formName: String,
creatorName: String,
applicationFormId: UUID,
): String {
val context = Context()
context.setVariable("formName", formName)
context.setVariable("creatorName", creatorName)
context.setVariable("applicationFormId", applicationFormId)
return templateEngine.process("email/form_submitted", context)
}
}

View File

@@ -34,6 +34,7 @@ class JwtUserSyncFilter(
val jwt: Jwt = auth.token
val keycloakId = jwt.subject
val name = jwt.getClaimAsString("name")
val email = jwt.getClaimAsString("email")
// Extract organization information from JWT
val organizationClaim = jwt.getClaimAsMap("organization")
@@ -46,7 +47,15 @@ class JwtUserSyncFilter(
}
}
val user = UserDto(keycloakId, name, organizationId)
val user =
UserDto(
keycloakId = keycloakId,
name = name,
organizationId = organizationId,
email = email,
emailOnFormCreated = null,
emailOnFormSubmitted = null,
)
if (keycloakId != null) {
userService.createUpdateUserFromJwt(user)

View File

@@ -21,6 +21,12 @@ class User(
var name: String,
@Column(nullable = true)
var organizationId: String? = null,
@Column(nullable = true)
var email: String? = null,
@Column(nullable = false)
var emailOnFormCreated: Boolean = true,
@Column(nullable = false)
var emailOnFormSubmitted: Boolean = true,
@CreatedDate
@Column(nullable = false)
var createdAt: LocalDateTime? = null,

View File

@@ -1,6 +1,7 @@
package com.betriebsratkanzlei.legalconsenthub.user
import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi
import com.betriebsratkanzlei.legalconsenthub_api.model.UpdateEmailPreferencesDto
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
@@ -19,4 +20,18 @@ class UserController(
userService.deleteUser(id)
return ResponseEntity.noContent().build()
}
override fun updateUserEmailPreferences(
id: String,
updateEmailPreferencesDto: UpdateEmailPreferencesDto,
): ResponseEntity<UserDto> {
val user =
userService.updateEmailPreferences(
userId = id,
email = updateEmailPreferencesDto.email,
emailOnFormCreated = updateEmailPreferencesDto.emailOnFormCreated ?: true,
emailOnFormSubmitted = updateEmailPreferencesDto.emailOnFormSubmitted ?: true,
)
return ResponseEntity.ok(userMapper.toUserDto(user))
}
}

View File

@@ -10,6 +10,9 @@ class UserMapper {
keycloakId = user.keycloakId,
name = user.name,
organizationId = user.organizationId,
email = user.email,
emailOnFormCreated = user.emailOnFormCreated,
emailOnFormSubmitted = user.emailOnFormSubmitted,
)
fun toUser(userDto: UserDto): User {
@@ -18,6 +21,9 @@ class UserMapper {
keycloakId = userDto.keycloakId,
name = userDto.name,
organizationId = userDto.organizationId,
email = userDto.email,
emailOnFormCreated = userDto.emailOnFormCreated ?: true,
emailOnFormSubmitted = userDto.emailOnFormSubmitted ?: true,
)
return user

View File

@@ -4,4 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface UserRepository : JpaRepository<User, String>
interface UserRepository : JpaRepository<User, String> {
fun findByOrganizationIdAndEmailOnFormCreatedTrue(organizationId: String?): List<User>
fun findByOrganizationIdAndEmailOnFormSubmittedTrue(organizationId: String?): List<User>
}

View File

@@ -17,9 +17,7 @@ class UserService(
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
val userId = principal.id ?: throw IllegalStateException("User ID not found")
return userRepository
.findById(userId)
.orElseThrow { UserNotFoundException(userId) }
return userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) }
}
@Transactional
@@ -29,11 +27,7 @@ class UserService(
if (existingUser.isEmpty) {
return createUser(userDto)
} else {
val user = existingUser.get()
if (user.organizationId == null && userDto.organizationId != null) {
user.organizationId = userDto.organizationId
}
return updateUser(userMapper.toUserDto(user))
return updateUser(userDto)
}
}
@@ -47,33 +41,49 @@ class UserService(
keycloakId = userDto.keycloakId,
name = userDto.name,
organizationId = userDto.organizationId,
email = userDto.email,
)
return userRepository.save(user)
}
fun getUserById(userId: String): User =
userRepository
.findById(userId)
.orElseThrow { UserNotFoundException(userId) }
userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) }
@Transactional
fun updateUser(userDto: UserDto): User {
val user =
userRepository
.findById(userDto.keycloakId)
.orElseThrow { UserNotFoundException(userDto.keycloakId) }
val user = userRepository.findById(userDto.keycloakId).orElseThrow { UserNotFoundException(userDto.keycloakId) }
user.name = userDto.name
// Only update organization if it's not already set
if (user.organizationId == null && userDto.organizationId != null) {
user.organizationId = userDto.organizationId
}
if (userDto.email != null && user.email != userDto.email) {
user.email = userDto.email
}
return userRepository.save(user)
}
fun deleteUser(userId: String) {
userRepository.deleteById(userId)
}
@Transactional
fun updateEmailPreferences(
userId: String,
email: String?,
emailOnFormCreated: Boolean,
emailOnFormSubmitted: Boolean,
): User {
val user = userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) }
user.email = email
user.emailOnFormCreated = emailOnFormCreated
user.emailOnFormSubmitted = emailOnFormSubmitted
return userRepository.save(user)
}
}

View File

@@ -38,3 +38,21 @@ spring:
jwt:
issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI}
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI}
mail:
host: ${MAIL_HOST:0.0.0.0}
port: ${MAIL_PORT:1025}
username: ${MAIL_USERNAME:}
password: ${MAIL_PASSWORD:}
properties:
mail:
smtp:
auth: false
starttls:
enable: false
required: false
ssl:
enable: false
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4F46E5; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
.content { background-color: #f9fafb; padding: 20px; }
.button { display: inline-block; padding: 10px 20px; background-color: #4F46E5; color: white; text-decoration: none; border-radius: 5px; margin-top: 15px; }
.footer { color: #6b7280; font-size: 12px; margin-top: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Neuer Mitbestimmungsantrag erstellt</h2>
</div>
<div class="content">
<p>Hallo,</p>
<p>Ein neuer Mitbestimmungsantrag wurde erstellt:</p>
<ul>
<li><strong>Antrag:</strong> <span th:text="${formName}"></span></li>
<li><strong>Erstellt von:</strong> <span th:text="${creatorName}"></span></li>
</ul>
<p>Klicken Sie auf den folgenden Link, um den Antrag anzusehen:</p>
<a th:href="@{http://localhost:3001/application-forms/{id}/0(id=${applicationFormId})}" class="button">Antrag ansehen</a>
</div>
<div class="footer">
<p>Diese E-Mail wurde automatisch von Legal Consent Hub gesendet.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #059669; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
.content { background-color: #f9fafb; padding: 20px; }
.button { display: inline-block; padding: 10px 20px; background-color: #059669; color: white; text-decoration: none; border-radius: 5px; margin-top: 15px; }
.footer { color: #6b7280; font-size: 12px; margin-top: 20px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>Mitbestimmungsantrag eingereicht</h2>
</div>
<div class="content">
<p>Hallo,</p>
<p>Ein Mitbestimmungsantrag wurde zur Prüfung eingereicht:</p>
<ul>
<li><strong>Antrag:</strong> <span th:text="${formName}"></span></li>
<li><strong>Eingereicht von:</strong> <span th:text="${creatorName}"></span></li>
</ul>
<p>Klicken Sie auf den folgenden Link, um den Antrag zu prüfen:</p>
<a th:href="@{http://localhost:3001/application-forms/{id}/0(id=${applicationFormId})}" class="button">Antrag prüfen</a>
</div>
<div class="footer">
<p>Diese E-Mail wurde automatisch von Legal Consent Hub gesendet.</p>
</div>
</div>
</body>
</html>

View File

@@ -5,3 +5,5 @@ export { useApplicationFormVersionApi } from './applicationFormVersion/useApplic
export { useApplicationFormNavigation } from './useApplicationFormNavigation'
export { useNotification } from './notification/useNotification'
export { useNotificationApi } from './notification/useNotificationApi'
export { useUser } from './user/useUser'
export { useUserApi } from './user/useUserApi'

View File

@@ -0,0 +1,26 @@
import type { UpdateEmailPreferencesDto, UserDto } from '~~/.api-client'
import { useUserApi } from '~/composables'
export function useUser() {
const userApi = useUserApi()
async function getUserById(userId: string): Promise<UserDto> {
return await userApi.getUserById(userId)
}
async function updateEmailPreferences(
userId: string,
email: string | null,
emailOnFormCreated: boolean,
emailOnFormSubmitted: boolean
): Promise<UserDto> {
const updateDto: UpdateEmailPreferencesDto = { email, emailOnFormCreated, emailOnFormSubmitted }
return await userApi.updateEmailPreferences(userId, updateDto)
}
return {
getUserById,
updateEmailPreferences
}
}

View File

@@ -0,0 +1,42 @@
import { UserApi, Configuration, type UserDto, type UpdateEmailPreferencesDto } from '~~/.api-client'
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
import { wrappedFetchWrap } from '~/utils/wrappedFetch'
export function useUserApi() {
const appBaseUrl = useRuntimeConfig().app.baseURL
const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
const basePath = withoutTrailingSlash(
cleanDoubleSlashes(
import.meta.client
? appBaseUrl + clientProxyBasePath
: useRequestURL().origin + clientProxyBasePath + serverApiBasePath
)
)
const userApiClient = new UserApi(
new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) })
)
async function getUserById(id: string): Promise<UserDto> {
return userApiClient.getUserById({ id })
}
async function updateEmailPreferences(
id: string,
updateEmailPreferencesDto: UpdateEmailPreferencesDto
): Promise<UserDto> {
return userApiClient.updateUserEmailPreferences({ id, updateEmailPreferencesDto })
}
async function deleteUser(id: string): Promise<void> {
return userApiClient.deleteUser({ id })
}
return {
getUserById,
updateEmailPreferences,
deleteUser
}
}

View File

@@ -72,6 +72,33 @@
</div>
</div>
</UCard>
<!-- Email Notifications Section -->
<UCard>
<template #header>
<div>
<h3 class="text-lg font-semibold text-highlighted">{{ $t('settings.email.title') }}</h3>
<p class="text-sm text-muted mt-1">{{ $t('settings.email.description') }}</p>
</div>
</template>
<div class="space-y-4">
<UInput
v-model="emailAddress"
:label="$t('settings.email.emailAddress')"
type="email"
placeholder="user@example.com"
class="w-full max-w-md"
/>
<div class="space-y-3">
<UCheckbox v-model="emailOnFormCreated" :label="$t('settings.email.onFormCreated')" />
<UCheckbox v-model="emailOnFormSubmitted" :label="$t('settings.email.onFormSubmitted')" />
</div>
<UButton :label="$t('common.save')" color="primary" :loading="isSaving" @click="saveEmailPreferences" />
</div>
</UCard>
</div>
</template>
</UDashboardPanel>
@@ -79,6 +106,8 @@
<script setup lang="ts">
import { de, en } from '@nuxt/ui/locale'
import { useUserStore } from '~~/stores/useUserStore'
import { useUser } from '~/composables'
definePageMeta({
layout: 'default'
@@ -87,6 +116,9 @@ definePageMeta({
const { t: $t, locale, setLocale } = useI18n()
const colorMode = useColorMode()
const appConfig = useAppConfig()
const toast = useToast()
const userStore = useUserStore()
const { getUserById, updateEmailPreferences } = useUser()
const colors = [
'red',
@@ -107,6 +139,50 @@ const colors = [
'pink'
]
const emailAddress = ref('')
const emailOnFormCreated = ref(true)
const emailOnFormSubmitted = ref(true)
const isSaving = ref(false)
onMounted(async () => {
if (userStore.user) {
try {
const userData = await getUserById(userStore.user.keycloakId)
emailAddress.value = userData.email || ''
emailOnFormCreated.value = userData.emailOnFormCreated ?? true
emailOnFormSubmitted.value = userData.emailOnFormSubmitted ?? true
} catch (error) {
console.error('Failed to load user email preferences:', error)
}
}
})
async function saveEmailPreferences() {
if (!userStore.user) return
isSaving.value = true
try {
await updateEmailPreferences(
userStore.user.keycloakId,
emailAddress.value || null,
emailOnFormCreated.value,
emailOnFormSubmitted.value
)
toast.add({
title: $t('settings.email.saved'),
color: 'success'
})
} catch {
toast.add({
title: $t('common.error'),
color: 'error'
})
} finally {
isSaving.value = false
}
}
function handleLocaleChange(newLocale: string | undefined) {
if (newLocale) {
setLocale(newLocale as 'de' | 'en')

View File

@@ -170,5 +170,31 @@
"employer": "Arbeitgeber",
"worksCouncilMember": "Betriebsratsmitglied",
"worksCouncilChair": "Betriebsratsvorsitzender"
},
"settings": {
"title": "Einstellungen",
"language": {
"title": "Sprache",
"description": "Wählen Sie Ihre bevorzugte Sprache"
},
"appearance": {
"title": "Erscheinungsbild",
"description": "Wählen Sie Ihr bevorzugtes Farbschema",
"light": "Hell",
"dark": "Dunkel"
},
"theme": {
"title": "Designfarben",
"description": "Passen Sie die Primärfarbe an",
"primary": "Primärfarbe"
},
"email": {
"title": "E-Mail-Benachrichtigungen",
"description": "Verwalten Sie Ihre E-Mail-Benachrichtigungseinstellungen",
"emailAddress": "E-Mail-Adresse",
"onFormCreated": "Bei Erstellung eines Antrags",
"onFormSubmitted": "Bei Einreichung eines Antrags",
"saved": "Einstellungen gespeichert"
}
}
}

View File

@@ -170,5 +170,31 @@
"employer": "Employer",
"worksCouncilMember": "Works Council Member",
"worksCouncilChair": "Works Council Chair"
},
"settings": {
"title": "Settings",
"language": {
"title": "Language",
"description": "Select your preferred language"
},
"appearance": {
"title": "Appearance",
"description": "Choose your preferred color scheme",
"light": "Light",
"dark": "Dark"
},
"theme": {
"title": "Theme Colors",
"description": "Customize the primary color",
"primary": "Primary Color"
},
"email": {
"title": "Email Notifications",
"description": "Manage your email notification preferences",
"emailAddress": "Email Address",
"onFormCreated": "When an application form is created",
"onFormSubmitted": "When an application form is submitted",
"saved": "Settings saved"
}
}
}