diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63615a6d..709633e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ parssonVersion="1.1.7" springbootVersion = "3.3.5" springDependencyManagementVersion="1.1.6" jhisterVersion = "8.7.1" +j2HtmlVersion = "1.6.0" [libraries] jhipster-framework = { module = "tech.jhipster:jhipster-framework", version.ref = "jhisterVersion" } @@ -29,6 +30,7 @@ mapstruct = { module = "org.mapstruct:mapstruct", version.ref = "mapstructVersio mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstructVersion" } jclouds = { module = "org.apache.jclouds:jclouds-all", version.ref = "jcloudsVersion" } dot-env = { module = "io.github.cdimascio:dotenv-java", version.ref = "dotEnvVersion" } +j2html = {module = "com.j2html:j2html", version.ref="j2HtmlVersion"} logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackVersion" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junitVersion" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" } diff --git a/server/build.gradle b/server/build.gradle index 0ed41a1f..8640a512 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -88,6 +88,7 @@ dependencies { implementation("com.github.ben-manes.caffeine:caffeine") implementation("com.zaxxer:HikariCP") implementation(libs.dot.env) + implementation(libs.j2html) implementation(libs.bundles.json) implementation("jakarta.annotation:jakarta.annotation-api") diff --git a/server/src/main/java/io/flexwork/modules/collab/domain/Notification.java b/server/src/main/java/io/flexwork/modules/collab/domain/Notification.java new file mode 100644 index 00000000..c527bad8 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/domain/Notification.java @@ -0,0 +1,43 @@ +package io.flexwork.modules.collab.domain; + +import io.flexwork.modules.usermanagement.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "fw_notification") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Notification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMPTZ") + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private boolean isRead; +} diff --git a/server/src/main/java/io/flexwork/modules/collab/repository/NotificationRepository.java b/server/src/main/java/io/flexwork/modules/collab/repository/NotificationRepository.java new file mode 100644 index 00000000..bdd14a1c --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/repository/NotificationRepository.java @@ -0,0 +1,19 @@ +package io.flexwork.modules.collab.repository; + +import io.flexwork.modules.collab.domain.Notification; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +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; + +@Repository +public interface NotificationRepository extends JpaRepository { + + List findByUserIdAndIsReadFalse(Long userId); + + @Modifying + @Query("UPDATE Notification n SET n.isRead = true WHERE n.id IN :ids") + void markAsRead(@Param("ids") List ids); +} diff --git a/server/src/main/java/io/flexwork/modules/collab/service/NotificationService.java b/server/src/main/java/io/flexwork/modules/collab/service/NotificationService.java new file mode 100644 index 00000000..23585f64 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/service/NotificationService.java @@ -0,0 +1,34 @@ +package io.flexwork.modules.collab.service; + +import io.flexwork.modules.collab.repository.NotificationRepository; +import io.flexwork.modules.collab.service.dto.NotificationDTO; +import io.flexwork.modules.collab.service.mapper.NotificationMapper; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class NotificationService { + private final NotificationRepository notificationRepository; + + private final NotificationMapper notificationMapper; + + public NotificationService( + NotificationRepository notificationRepository, NotificationMapper notificationMapper) { + this.notificationRepository = notificationRepository; + this.notificationMapper = notificationMapper; + } + + @Transactional(readOnly = true) + public List getUnreadNotificationsForUser(Long userId) { + return notificationRepository.findByUserIdAndIsReadFalse(userId).stream() + .map(notificationMapper::toDTO) + .toList(); + } + + @Transactional + public void markNotificationsAsRead(List notificationIds) { + notificationRepository.markAsRead(notificationIds); + } +} diff --git a/server/src/main/java/io/flexwork/modules/collab/service/dto/NotificationDTO.java b/server/src/main/java/io/flexwork/modules/collab/service/dto/NotificationDTO.java new file mode 100644 index 00000000..00f9a076 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/service/dto/NotificationDTO.java @@ -0,0 +1,15 @@ +package io.flexwork.modules.collab.service.dto; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class NotificationDTO { + private Long id; + private String content; + private LocalDateTime createdAt; + private Long userId; + private boolean isRead; +} diff --git a/server/src/main/java/io/flexwork/modules/collab/service/dto/OrganizationDTO.java b/server/src/main/java/io/flexwork/modules/collab/service/dto/OrganizationDTO.java index d1aaa72b..5f2ad945 100644 --- a/server/src/main/java/io/flexwork/modules/collab/service/dto/OrganizationDTO.java +++ b/server/src/main/java/io/flexwork/modules/collab/service/dto/OrganizationDTO.java @@ -7,10 +7,10 @@ @Data @Builder public class OrganizationDTO { - private Long id; // Organization ID - private String name; // Organization name - private String logoUrl; // Logo URL - private String slogan; // Organization slogan - private String description; // Description of the organization - private Set teams; // Set of team IDs + private Long id; + private String name; + private String logoUrl; + private String slogan; + private String description; + private Set teams; } diff --git a/server/src/main/java/io/flexwork/modules/collab/service/event/NewTeamRequestCreatedEvent.java b/server/src/main/java/io/flexwork/modules/collab/service/event/NewTeamRequestCreatedEvent.java new file mode 100644 index 00000000..cfe9ee03 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/service/event/NewTeamRequestCreatedEvent.java @@ -0,0 +1,15 @@ +package io.flexwork.modules.collab.service.event; + +import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class NewTeamRequestCreatedEvent extends ApplicationEvent { + private TeamRequestDTO teamRequest; + + public NewTeamRequestCreatedEvent(Object source, TeamRequestDTO teamRequest) { + super(source); + this.teamRequest = teamRequest; + } +} diff --git a/server/src/main/java/io/flexwork/modules/collab/service/listener/TeamRequestEventListener.java b/server/src/main/java/io/flexwork/modules/collab/service/listener/TeamRequestEventListener.java new file mode 100644 index 00000000..dfdbe144 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/service/listener/TeamRequestEventListener.java @@ -0,0 +1,54 @@ +package io.flexwork.modules.collab.service.listener; + +import static j2html.TagCreator.*; + +import io.flexwork.modules.collab.domain.Notification; +import io.flexwork.modules.collab.repository.NotificationRepository; +import io.flexwork.modules.collab.repository.TeamRepository; +import io.flexwork.modules.collab.service.event.NewTeamRequestCreatedEvent; +import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.usermanagement.domain.User; +import io.flexwork.modules.usermanagement.service.dto.UserWithTeamRoleDTO; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class TeamRequestEventListener { + private final NotificationRepository notificationRepository; + private final TeamRepository teamRepository; + + public TeamRequestEventListener( + NotificationRepository notificationRepository, TeamRepository teamRepository) { + this.notificationRepository = notificationRepository; + this.teamRepository = teamRepository; + } + + @EventListener + public void onNewTeamRequestCreated(NewTeamRequestCreatedEvent event) { + TeamRequestDTO teamRequestDTO = event.getTeamRequest(); + String html = + p( + text("A new "), + a("ticket request").withHref("#"), + text(" has been just created by "), + a("user").withHref("#")) + .render(); + + List usersInTeam = + teamRepository.findUsersByTeamId(teamRequestDTO.getTeamId()); + List notifications = + usersInTeam.stream() + .map( + user -> + Notification.builder() + .content(html) + .user(User.builder().id(user.getId()).build()) + .isRead(false) + .createdAt(LocalDateTime.now()) + .build()) + .toList(); + notificationRepository.saveAll(notifications); + } +} diff --git a/server/src/main/java/io/flexwork/modules/collab/service/mapper/NotificationMapper.java b/server/src/main/java/io/flexwork/modules/collab/service/mapper/NotificationMapper.java new file mode 100644 index 00000000..4942f2c5 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/service/mapper/NotificationMapper.java @@ -0,0 +1,16 @@ +package io.flexwork.modules.collab.service.mapper; + +import io.flexwork.modules.collab.domain.Notification; +import io.flexwork.modules.collab.service.dto.NotificationDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface NotificationMapper { + + @Mapping(source = "user.id", target = "userId") + NotificationDTO toDTO(Notification notification); + + @Mapping(source = "userId", target = "user.id") + Notification toEntity(NotificationDTO notificationDTO); +} diff --git a/server/src/main/java/io/flexwork/modules/collab/web/rest/NotificationController.java b/server/src/main/java/io/flexwork/modules/collab/web/rest/NotificationController.java new file mode 100644 index 00000000..3b1c7988 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/collab/web/rest/NotificationController.java @@ -0,0 +1,46 @@ +package io.flexwork.modules.collab.web.rest; + +import io.flexwork.modules.collab.service.NotificationService; +import io.flexwork.modules.collab.service.dto.NotificationDTO; +import java.util.List; +import lombok.Data; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/notifications") +public class NotificationController { + private final NotificationService notificationService; + + public NotificationController(NotificationService notificationService) { + this.notificationService = notificationService; + } + + @PostMapping("/mark-read") + public ResponseEntity markNotificationsAsRead(@RequestBody MarkReadRequest request) { + if (request.getNotificationIds() == null || request.getNotificationIds().isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + notificationService.markNotificationsAsRead(request.getNotificationIds()); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/unread") + public ResponseEntity> getUnreadNotifications( + @RequestParam("userId") Long userId) { + List notifications = + notificationService.getUnreadNotificationsForUser(userId); + return ResponseEntity.ok(notifications); + } + + @Data + public static class MarkReadRequest { + private List notificationIds; + } +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java b/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java index 7a528c24..86364999 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java @@ -2,6 +2,7 @@ import static io.flexwork.query.QueryUtils.createSpecification; +import io.flexwork.modules.collab.service.event.NewTeamRequestCreatedEvent; import io.flexwork.modules.teams.domain.TeamRequest; import io.flexwork.modules.teams.repository.TeamRequestRepository; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; @@ -11,6 +12,7 @@ import java.util.Optional; import org.jclouds.rest.ResourceNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; @@ -23,12 +25,16 @@ public class TeamRequestService { private final TeamRequestRepository teamRequestRepository; private final TeamRequestMapper teamRequestMapper; + private final ApplicationEventPublisher eventPublisher; @Autowired public TeamRequestService( - TeamRequestRepository teamRequestRepository, TeamRequestMapper teamRequestMapper) { + TeamRequestRepository teamRequestRepository, + TeamRequestMapper teamRequestMapper, + ApplicationEventPublisher eventPublisher) { this.teamRequestRepository = teamRequestRepository; this.teamRequestMapper = teamRequestMapper; + this.eventPublisher = eventPublisher; } @Transactional(readOnly = true) @@ -60,7 +66,9 @@ public TeamRequestDTO createTeamRequest(TeamRequestDTO teamRequestDTO) { TeamRequest teamRequest = teamRequestMapper.toEntity(teamRequestDTO); teamRequest.setCreatedDate(LocalDateTime.now()); teamRequest = teamRequestRepository.save(teamRequest); - return teamRequestMapper.toDto(teamRequest); + TeamRequestDTO savedTeamRequestDTO = teamRequestMapper.toDto(teamRequest); + eventPublisher.publishEvent(new NewTeamRequestCreatedEvent(this, savedTeamRequestDTO)); + return savedTeamRequestDTO; } @Transactional diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml index 0f8a5a99..254aab9b 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml @@ -174,7 +174,6 @@ referencedTableName="fw_organization" referencedColumnNames="id" onDelete="CASCADE" constraintName="fk_teams_organization" /> - @@ -229,6 +228,28 @@ baseTableName="fw_comment" baseColumnNames="created_by" constraintName="fk_comment_user" referencedTableName="fw_user" referencedColumnNames="id" /> + + + + + + + + + + + + + + + + + +