feat(#36): Notification rework with single and all comments mark as read
This commit is contained in:
@@ -4,10 +4,12 @@ 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
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJpaAuditing
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
class LegalconsenthubApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.betriebsratkanzlei.legalconsenthub.application_form_version.Applicati
|
||||
import com.betriebsratkanzlei.legalconsenthub.comment.CommentRepository
|
||||
import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormCreatedEvent
|
||||
import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormSubmittedEvent
|
||||
import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormUpdatedEvent
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormInvalidStateException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException
|
||||
@@ -115,6 +116,11 @@ class ApplicationFormService(
|
||||
|
||||
if (existingSnapshot != newSnapshot) {
|
||||
versionService.createVersion(updatedApplicationForm, currentUser)
|
||||
|
||||
// Notify the form author if someone else made changes
|
||||
if (updatedApplicationForm.createdBy.keycloakId != currentUser.keycloakId) {
|
||||
createNotificationForFormUpdate(updatedApplicationForm, currentUser.name)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedApplicationForm
|
||||
@@ -180,6 +186,7 @@ class ApplicationFormService(
|
||||
clickTarget = clickTarget,
|
||||
recipientId = null,
|
||||
targetRoles = null,
|
||||
excludedUserId = applicationForm.createdBy.keycloakId,
|
||||
type = NotificationType.INFO,
|
||||
organizationId = applicationForm.organizationId,
|
||||
)
|
||||
@@ -187,6 +194,41 @@ class ApplicationFormService(
|
||||
notificationService.createNotificationForOrganization(createNotificationDto)
|
||||
}
|
||||
|
||||
private fun createNotificationForFormUpdate(
|
||||
applicationForm: ApplicationForm,
|
||||
modifierName: String,
|
||||
) {
|
||||
val title = "Änderungen an Ihrem Mitbestimmungsantrag"
|
||||
val message =
|
||||
"$modifierName hat Änderungen an Ihrem Mitbestimmungsantrag '${applicationForm.name}' vorgenommen."
|
||||
val clickTarget = "/application-forms/${applicationForm.id}/0"
|
||||
|
||||
val createNotificationDto =
|
||||
CreateNotificationDto(
|
||||
title = title,
|
||||
message = message,
|
||||
clickTarget = clickTarget,
|
||||
recipientId = applicationForm.createdBy.keycloakId,
|
||||
targetRoles = null,
|
||||
excludedUserId = null,
|
||||
type = NotificationType.INFO,
|
||||
organizationId = applicationForm.organizationId,
|
||||
)
|
||||
|
||||
notificationService.createNotificationForUser(createNotificationDto)
|
||||
|
||||
// Publish email event for form author
|
||||
eventPublisher.publishEvent(
|
||||
ApplicationFormUpdatedEvent(
|
||||
applicationFormId = applicationForm.id!!,
|
||||
organizationId = applicationForm.organizationId,
|
||||
updaterName = modifierName,
|
||||
formName = applicationForm.name,
|
||||
authorKeycloakId = applicationForm.createdBy.keycloakId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun addFormElementToSubSection(
|
||||
applicationFormId: UUID,
|
||||
subsectionId: UUID,
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.comment
|
||||
|
||||
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationFormRepository
|
||||
import com.betriebsratkanzlei.legalconsenthub.email.CommentAddedEvent
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.CommentNotCreatedException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.CommentNotDeletedException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.CommentNotFoundException
|
||||
import com.betriebsratkanzlei.legalconsenthub.error.CommentNotUpdatedException
|
||||
import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService
|
||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CommentDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateCommentDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
||||
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
@@ -23,6 +31,9 @@ class CommentService(
|
||||
private val commentRepository: CommentRepository,
|
||||
private val applicationFormRepository: ApplicationFormRepository,
|
||||
private val commentMapper: CommentMapper,
|
||||
private val notificationService: NotificationService,
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val objectMapper: ObjectMapper,
|
||||
) {
|
||||
fun createComment(
|
||||
applicationFormId: UUID,
|
||||
@@ -37,9 +48,70 @@ class CommentService(
|
||||
throw CommentNotCreatedException(e)
|
||||
}
|
||||
|
||||
// Notify the form author if someone else added a comment
|
||||
val applicationForm =
|
||||
applicationFormRepository
|
||||
.findById(applicationFormId)
|
||||
.orElseThrow { ApplicationFormNotFoundException(applicationFormId) }
|
||||
|
||||
if (applicationForm.createdBy.keycloakId != savedComment.createdBy.keycloakId) {
|
||||
createNotificationForNewComment(
|
||||
savedComment,
|
||||
applicationForm.createdBy.keycloakId,
|
||||
applicationForm.organizationId,
|
||||
)
|
||||
}
|
||||
|
||||
return savedComment
|
||||
}
|
||||
|
||||
private fun createNotificationForNewComment(
|
||||
comment: Comment,
|
||||
formAuthorKeycloakId: String,
|
||||
organizationId: String,
|
||||
) {
|
||||
val formName = comment.applicationForm?.name ?: "Unbekannt"
|
||||
val title = "Neuer Kommentar zu Ihrem Mitbestimmungsantrag"
|
||||
val message =
|
||||
"${comment.createdBy.name} hat einen Kommentar zu Ihrem Mitbestimmungsantrag " +
|
||||
"'$formName' hinzugefügt."
|
||||
val clickTarget = "/application-forms/${comment.applicationForm?.id}/0"
|
||||
|
||||
val createNotificationDto =
|
||||
CreateNotificationDto(
|
||||
title = title,
|
||||
message = message,
|
||||
clickTarget = clickTarget,
|
||||
recipientId = formAuthorKeycloakId,
|
||||
targetRoles = null,
|
||||
excludedUserId = null,
|
||||
type = NotificationType.INFO,
|
||||
organizationId = organizationId,
|
||||
)
|
||||
|
||||
notificationService.createNotificationForUser(createNotificationDto)
|
||||
|
||||
// Publish email event for form author
|
||||
val plainText = extractPlainTextFromTipTap(comment.message)
|
||||
val commentPreview =
|
||||
if (plainText.length > 100) {
|
||||
plainText.take(100) + "..."
|
||||
} else {
|
||||
plainText
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(
|
||||
CommentAddedEvent(
|
||||
applicationFormId = comment.applicationForm?.id!!,
|
||||
organizationId = organizationId,
|
||||
commenterName = comment.createdBy.name,
|
||||
formName = formName,
|
||||
authorKeycloakId = formAuthorKeycloakId,
|
||||
commentPreview = commentPreview,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getCommentById(id: UUID): Comment = commentRepository.findById(id).orElseThrow { CommentNotFoundException(id) }
|
||||
|
||||
fun getComments(
|
||||
@@ -100,4 +172,43 @@ class CommentService(
|
||||
throw CommentNotDeletedException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts plain text from TipTap/ProseMirror JSON content.
|
||||
* TipTap stores content as JSON like: {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}]}
|
||||
*/
|
||||
private fun extractPlainTextFromTipTap(jsonContent: String): String {
|
||||
return try {
|
||||
val rootNode = objectMapper.readTree(jsonContent)
|
||||
val textBuilder = StringBuilder()
|
||||
extractTextRecursively(rootNode, textBuilder)
|
||||
textBuilder.toString().trim()
|
||||
} catch (e: Exception) {
|
||||
// If parsing fails, return the original content (might be plain text)
|
||||
jsonContent
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractTextRecursively(
|
||||
node: JsonNode,
|
||||
builder: StringBuilder,
|
||||
) {
|
||||
when {
|
||||
node.has("text") -> {
|
||||
builder.append(node.get("text").asText())
|
||||
}
|
||||
node.has("content") -> {
|
||||
val content = node.get("content")
|
||||
if (content.isArray) {
|
||||
content.forEachIndexed { index, child ->
|
||||
extractTextRecursively(child, builder)
|
||||
// Add newline between paragraphs
|
||||
if (child.has("type") && child.get("type").asText() == "paragraph" && index < content.size() - 1) {
|
||||
builder.append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.email
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class ApplicationFormUpdatedEvent(
|
||||
val applicationFormId: UUID,
|
||||
val organizationId: String?,
|
||||
val updaterName: String,
|
||||
val formName: String,
|
||||
val authorKeycloakId: String,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.email
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class CommentAddedEvent(
|
||||
val applicationFormId: UUID,
|
||||
val organizationId: String?,
|
||||
val commenterName: String,
|
||||
val formName: String,
|
||||
val authorKeycloakId: String,
|
||||
val commentPreview: String,
|
||||
)
|
||||
@@ -70,4 +70,69 @@ class EmailEventListener(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
fun handleApplicationFormUpdated(event: ApplicationFormUpdatedEvent) {
|
||||
logger.info("Processing ApplicationFormUpdatedEvent for form: ${event.formName}")
|
||||
|
||||
val recipient =
|
||||
userRepository
|
||||
.findByKeycloakIdAndEmailOnFormUpdatedTrue(event.authorKeycloakId)
|
||||
?.takeIf { !it.email.isNullOrBlank() }
|
||||
|
||||
if (recipient == null) {
|
||||
logger.info("No recipient found for form updated event (author has email disabled or no email)")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Sending form updated email to author: ${recipient.email}")
|
||||
|
||||
val subject = "Ihr Mitbestimmungsantrag wurde aktualisiert: ${event.formName}"
|
||||
val body =
|
||||
emailService.buildFormUpdatedEmail(
|
||||
formName = event.formName,
|
||||
updaterName = event.updaterName,
|
||||
applicationFormId = event.applicationFormId,
|
||||
)
|
||||
|
||||
emailService.sendEmail(
|
||||
to = recipient.email!!,
|
||||
subject = subject,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
fun handleCommentAdded(event: CommentAddedEvent) {
|
||||
logger.info("Processing CommentAddedEvent for form: ${event.formName}")
|
||||
|
||||
val recipient =
|
||||
userRepository
|
||||
.findByKeycloakIdAndEmailOnCommentAddedTrue(event.authorKeycloakId)
|
||||
?.takeIf { !it.email.isNullOrBlank() }
|
||||
|
||||
if (recipient == null) {
|
||||
logger.info("No recipient found for comment added event (author has email disabled or no email)")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("Sending comment added email to author: ${recipient.email}")
|
||||
|
||||
val subject = "Neuer Kommentar zu Ihrem Antrag: ${event.formName}"
|
||||
val body =
|
||||
emailService.buildCommentAddedEmail(
|
||||
formName = event.formName,
|
||||
commenterName = event.commenterName,
|
||||
applicationFormId = event.applicationFormId,
|
||||
commentPreview = event.commentPreview,
|
||||
)
|
||||
|
||||
emailService.sendEmail(
|
||||
to = recipient.email!!,
|
||||
subject = subject,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,30 @@ class EmailService(
|
||||
context.setVariable("applicationFormId", applicationFormId)
|
||||
return templateEngine.process("email/form_submitted", context)
|
||||
}
|
||||
|
||||
fun buildFormUpdatedEmail(
|
||||
formName: String,
|
||||
updaterName: String,
|
||||
applicationFormId: UUID,
|
||||
): String {
|
||||
val context = Context()
|
||||
context.setVariable("formName", formName)
|
||||
context.setVariable("updaterName", updaterName)
|
||||
context.setVariable("applicationFormId", applicationFormId)
|
||||
return templateEngine.process("email/form_updated", context)
|
||||
}
|
||||
|
||||
fun buildCommentAddedEmail(
|
||||
formName: String,
|
||||
commenterName: String,
|
||||
applicationFormId: UUID,
|
||||
commentPreview: String,
|
||||
): String {
|
||||
val context = Context()
|
||||
context.setVariable("formName", formName)
|
||||
context.setVariable("commenterName", commenterName)
|
||||
context.setVariable("applicationFormId", applicationFormId)
|
||||
context.setVariable("commentPreview", commentPreview)
|
||||
return templateEngine.process("email/comment_added", context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class Notification(
|
||||
var recipient: User? = null,
|
||||
@Column(nullable = true, columnDefinition = "TEXT")
|
||||
var targetRoles: String? = null,
|
||||
@Column(nullable = true)
|
||||
var excludedUserId: String? = null,
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
var type: NotificationType = NotificationType.INFO,
|
||||
|
||||
@@ -128,10 +128,12 @@ class NotificationController(
|
||||
@PreAuthorize(
|
||||
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
|
||||
)
|
||||
override fun clearAllNotifications(organizationId: String): ResponseEntity<Unit> {
|
||||
override fun clearAllNotifications(): ResponseEntity<Unit> {
|
||||
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||
val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||
validateOrganizationAccess(principal, organizationId)
|
||||
val organizationId =
|
||||
principal.organizationIds.firstOrNull()
|
||||
?: throw IllegalStateException("User has no organization")
|
||||
|
||||
notificationService.clearAllNotifications(
|
||||
recipientKeycloakId = recipientId,
|
||||
@@ -141,6 +143,14 @@ class NotificationController(
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
@PreAuthorize(
|
||||
"hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
|
||||
)
|
||||
override fun deleteNotification(id: UUID): ResponseEntity<Unit> {
|
||||
notificationService.deleteNotification(id)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
private fun validateOrganizationAccess(
|
||||
principal: CustomJwtTokenPrincipal,
|
||||
organizationId: String,
|
||||
|
||||
@@ -39,6 +39,7 @@ class NotificationMapper(
|
||||
clickTarget = createNotificationDto.clickTarget,
|
||||
recipient = recipient,
|
||||
targetRoles = targetRoles,
|
||||
excludedUserId = createNotificationDto.excludedUserId,
|
||||
type = createNotificationDto.type,
|
||||
organizationId = createNotificationDto.organizationId,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class NotificationPurgeScheduler(
|
||||
private val notificationService: NotificationService,
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NotificationPurgeScheduler::class.java)
|
||||
|
||||
/**
|
||||
* Purges read notifications older than 90 days.
|
||||
* Runs daily at 2:00 AM.
|
||||
*/
|
||||
@Scheduled(cron = "0 0 2 * * *")
|
||||
fun purgeOldReadNotifications() {
|
||||
logger.info("Starting scheduled purge of old read notifications")
|
||||
try {
|
||||
notificationService.purgeOldReadNotifications(daysOld = 90)
|
||||
logger.info("Successfully purged old read notifications")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to purge old read notifications", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
@@ -77,4 +78,16 @@ interface NotificationRepository : JpaRepository<Notification, UUID> {
|
||||
@Param("keycloakId") keycloakId: String,
|
||||
@Param("organizationId") organizationId: String,
|
||||
)
|
||||
|
||||
@Modifying
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM Notification n
|
||||
WHERE n.isRead = true
|
||||
AND n.createdAt < :cutoffDate
|
||||
""",
|
||||
)
|
||||
fun deleteReadNotificationsOlderThan(
|
||||
@Param("cutoffDate") cutoffDate: Instant,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ class NotificationService(
|
||||
|
||||
val filteredContent =
|
||||
allNotifications.content.filter { notification ->
|
||||
// Exclude notifications where the current user is the excluded user
|
||||
if (notification.excludedUserId == recipientKeycloakId) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
when {
|
||||
// Direct recipient notification
|
||||
notification.recipient != null -> true
|
||||
@@ -105,6 +110,11 @@ class NotificationService(
|
||||
)
|
||||
|
||||
return allNotifications.filter { notification ->
|
||||
// Exclude notifications where the current user is the excluded user
|
||||
if (notification.excludedUserId == recipientKeycloakId) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
when {
|
||||
// Direct recipient notification
|
||||
notification.recipient != null -> true
|
||||
@@ -160,4 +170,33 @@ class NotificationService(
|
||||
) {
|
||||
notificationRepository.deleteAllByRecipientAndOrganization(recipientKeycloakId, organizationId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteNotification(notificationId: UUID) {
|
||||
val notification =
|
||||
notificationRepository
|
||||
.findById(notificationId)
|
||||
.orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") }
|
||||
|
||||
val principal = SecurityContextHolder.getContext().authentication.principal as? CustomJwtTokenPrincipal
|
||||
val currentUserKeycloakId =
|
||||
principal?.id
|
||||
?: throw IllegalStateException("User ID not found in security context")
|
||||
|
||||
// Allow deletion if user is the recipient or if it's an organization-wide notification
|
||||
if (notification.recipient != null && notification.recipient?.keycloakId != currentUserKeycloakId) {
|
||||
throw IllegalArgumentException("Cannot delete notification for another user")
|
||||
}
|
||||
|
||||
notificationRepository.delete(notification)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun purgeOldReadNotifications(daysOld: Int = 90) {
|
||||
val cutoffDate =
|
||||
java.time.Instant
|
||||
.now()
|
||||
.minus(java.time.Duration.ofDays(daysOld.toLong()))
|
||||
notificationRepository.deleteReadNotificationsOlderThan(cutoffDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ class User(
|
||||
var emailOnFormCreated: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var emailOnFormSubmitted: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var emailOnFormUpdated: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var emailOnCommentAdded: Boolean = true,
|
||||
@CreatedDate
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant? = null,
|
||||
|
||||
@@ -31,6 +31,8 @@ class UserController(
|
||||
email = updateEmailPreferencesDto.email,
|
||||
emailOnFormCreated = updateEmailPreferencesDto.emailOnFormCreated ?: true,
|
||||
emailOnFormSubmitted = updateEmailPreferencesDto.emailOnFormSubmitted ?: true,
|
||||
emailOnFormUpdated = updateEmailPreferencesDto.emailOnFormUpdated ?: true,
|
||||
emailOnCommentAdded = updateEmailPreferencesDto.emailOnCommentAdded ?: true,
|
||||
)
|
||||
return ResponseEntity.ok(userMapper.toUserDto(user))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ class UserMapper {
|
||||
email = user.email,
|
||||
emailOnFormCreated = user.emailOnFormCreated,
|
||||
emailOnFormSubmitted = user.emailOnFormSubmitted,
|
||||
emailOnFormUpdated = user.emailOnFormUpdated,
|
||||
emailOnCommentAdded = user.emailOnCommentAdded,
|
||||
)
|
||||
|
||||
fun toUser(userDto: UserDto): User {
|
||||
@@ -24,6 +26,8 @@ class UserMapper {
|
||||
email = userDto.email,
|
||||
emailOnFormCreated = userDto.emailOnFormCreated ?: true,
|
||||
emailOnFormSubmitted = userDto.emailOnFormSubmitted ?: true,
|
||||
emailOnFormUpdated = userDto.emailOnFormUpdated ?: true,
|
||||
emailOnCommentAdded = userDto.emailOnCommentAdded ?: true,
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@@ -8,4 +8,8 @@ interface UserRepository : JpaRepository<User, String> {
|
||||
fun findByOrganizationIdAndEmailOnFormCreatedTrue(organizationId: String?): List<User>
|
||||
|
||||
fun findByOrganizationIdAndEmailOnFormSubmittedTrue(organizationId: String?): List<User>
|
||||
|
||||
fun findByKeycloakIdAndEmailOnFormUpdatedTrue(keycloakId: String): User?
|
||||
|
||||
fun findByKeycloakIdAndEmailOnCommentAddedTrue(keycloakId: String): User?
|
||||
}
|
||||
|
||||
@@ -77,12 +77,16 @@ class UserService(
|
||||
email: String?,
|
||||
emailOnFormCreated: Boolean,
|
||||
emailOnFormSubmitted: Boolean,
|
||||
emailOnFormUpdated: Boolean,
|
||||
emailOnCommentAdded: Boolean,
|
||||
): User {
|
||||
val user = userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) }
|
||||
|
||||
user.email = email
|
||||
user.emailOnFormCreated = emailOnFormCreated
|
||||
user.emailOnFormSubmitted = emailOnFormSubmitted
|
||||
user.emailOnFormUpdated = emailOnFormUpdated
|
||||
user.emailOnCommentAdded = emailOnCommentAdded
|
||||
|
||||
return userRepository.save(user)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!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: #3B82F6; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
|
||||
.content { background-color: #f9fafb; padding: 20px; }
|
||||
.comment-preview { background-color: #e5e7eb; padding: 15px; border-radius: 5px; margin: 15px 0; font-style: italic; }
|
||||
.button { display: inline-block; padding: 10px 20px; background-color: #3B82F6; 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 Kommentar zu Ihrem Antrag</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hallo,</p>
|
||||
<p>Es wurde ein neuer Kommentar zu Ihrem Mitbestimmungsantrag hinzugefügt:</p>
|
||||
<ul>
|
||||
<li><strong>Antrag:</strong> <span th:text="${formName}"></span></li>
|
||||
<li><strong>Kommentar von:</strong> <span th:text="${commenterName}"></span></li>
|
||||
</ul>
|
||||
<div class="comment-preview">
|
||||
<span th:text="${commentPreview}"></span>
|
||||
</div>
|
||||
<p>Klicken Sie auf den folgenden Link, um den Kommentar anzusehen:</p>
|
||||
<a th:href="@{http://localhost:3001/application-forms/{id}/0(id=${applicationFormId})}" class="button">Kommentar 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,34 @@
|
||||
<!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: #F59E0B; 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: #F59E0B; 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 aktualisiert</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hallo,</p>
|
||||
<p>Ihr Mitbestimmungsantrag wurde von einer anderen Person aktualisiert:</p>
|
||||
<ul>
|
||||
<li><strong>Antrag:</strong> <span th:text="${formName}"></span></li>
|
||||
<li><strong>Aktualisiert von:</strong> <span th:text="${updaterName}"></span></li>
|
||||
</ul>
|
||||
<p>Klicken Sie auf den folgenden Link, um die Änderungen anzusehen:</p>
|
||||
<a th:href="@{http://localhost:3001/application-forms/{id}/0(id=${applicationFormId})}" class="button">Änderungen ansehen</a>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Diese E-Mail wurde automatisch von Legal Consent Hub gesendet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user