feat(#23): Add email notifications
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
26
legalconsenthub/app/composables/user/useUser.ts
Normal file
26
legalconsenthub/app/composables/user/useUser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
42
legalconsenthub/app/composables/user/useUserApi.ts
Normal file
42
legalconsenthub/app/composables/user/useUserApi.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user