feat(sprint-6): Phase 6 — Notifications (WebSocket) + PWA
- WebSocket: Spring STOMP + SockJS, NotificationService, persistent notifications table - NotificationController: GET/PUT endpoints for notification management - Frontend: notification bell with unread badge, dropdown panel, real-time via STOMP - PWA: manifest.json, service worker (manual sw.js), offline page, install prompt - PWA icons (192+512), dark theme colors, standalone display - Full i18n (de/en) for notifications and PWA - Flyway V10 migration for notifications table - spring-boot-starter-websocket dependency added
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import de.cannamanage.domain.entity.Notification;
|
||||
import de.cannamanage.domain.enums.NotificationType;
|
||||
import de.cannamanage.service.repository.NotificationRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Notification service — persists notifications and delivers in real-time via WebSocket.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationService {
|
||||
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
/**
|
||||
* Send a notification: persist to DB + push via WebSocket.
|
||||
*/
|
||||
@Transactional
|
||||
public Notification sendNotification(UUID userId, NotificationType type, String title, String message, String link) {
|
||||
var notification = new Notification(userId, type, title, message, link);
|
||||
notification = notificationRepository.save(notification);
|
||||
|
||||
// Push to user's WebSocket queue
|
||||
messagingTemplate.convertAndSendToUser(
|
||||
userId.toString(),
|
||||
"/queue/notifications",
|
||||
Map.of(
|
||||
"id", notification.getId(),
|
||||
"type", notification.getType().name(),
|
||||
"title", notification.getTitle(),
|
||||
"message", notification.getMessage(),
|
||||
"link", notification.getLink() != null ? notification.getLink() : "",
|
||||
"read", notification.isRead(),
|
||||
"createdAt", notification.getCreatedAt().toString()
|
||||
)
|
||||
);
|
||||
|
||||
log.debug("Notification sent to user {}: {} - {}", userId, type, title);
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last 10 notifications for a user (unread first).
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Notification> getRecentNotifications(UUID userId) {
|
||||
return notificationRepository.findTop10ByUserIdOrderByReadAscCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread count for badge display.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long getUnreadCount(UUID userId) {
|
||||
return notificationRepository.countByUserIdAndReadFalse(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single notification as read.
|
||||
*/
|
||||
@Transactional
|
||||
public void markAsRead(UUID notificationId) {
|
||||
notificationRepository.findById(notificationId).ifPresent(n -> {
|
||||
n.setRead(true);
|
||||
notificationRepository.save(n);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user.
|
||||
*/
|
||||
@Transactional
|
||||
public int markAllAsRead(UUID userId) {
|
||||
return notificationRepository.markAllAsRead(userId);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.Notification;
|
||||
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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, UUID> {
|
||||
|
||||
List<Notification> findByUserIdOrderByReadAscCreatedAtDesc(UUID userId);
|
||||
|
||||
List<Notification> findTop10ByUserIdOrderByReadAscCreatedAtDesc(UUID userId);
|
||||
|
||||
long countByUserIdAndReadFalse(UUID userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true WHERE n.userId = :userId AND n.read = false")
|
||||
int markAllAsRead(@Param("userId") UUID userId);
|
||||
}
|
||||
Reference in New Issue
Block a user