diff --git a/src/main/java/com/example/ead_backend/config/WebSocketConfig.java b/src/main/java/com/example/ead_backend/config/WebSocketConfig.java new file mode 100644 index 0000000..09e3a79 --- /dev/null +++ b/src/main/java/com/example/ead_backend/config/WebSocketConfig.java @@ -0,0 +1,43 @@ +package com.example.ead_backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * WebSocket configuration for real-time notifications. + * Configures STOMP protocol over WebSocket with SockJS fallback. + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * Configure message broker for pub/sub messaging. + * + * @param config the message broker registry + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // Enable simple broker for topic subscriptions + config.enableSimpleBroker("/topic"); + + // Set application destination prefix for client messages + config.setApplicationDestinationPrefixes("/app"); + } + + /** + * Register STOMP endpoints with SockJS fallback. + * + * @param registry the STOMP endpoint registry + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // Register endpoint for WebSocket connections + registry.addEndpoint("/ws/progress") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/NotificationController.java b/src/main/java/com/example/ead_backend/controller/NotificationController.java new file mode 100644 index 0000000..8734b4f --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/NotificationController.java @@ -0,0 +1,101 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.NotificationDTO; +import com.example.ead_backend.service.NotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * REST Controller for notification operations. + */ +@RestController +@RequestMapping("/api/customer/notifications") +@RequiredArgsConstructor +@Slf4j +@CrossOrigin(origins = "http://localhost:3000") +public class NotificationController { + + private final NotificationService notificationService; + + /** + * Get all notifications for a user. + * + * @param userId the user ID + * @return list of notifications + */ + @GetMapping("/{userId}") + public ResponseEntity> getNotifications(@PathVariable Long userId) { + log.info("Fetching notifications for user {}", userId); + List notifications = notificationService.getNotificationsForUser(userId); + return ResponseEntity.ok(notifications); + } + + /** + * Get unread notifications for a user. + * + * @param userId the user ID + * @return list of unread notifications + */ + @GetMapping("/{userId}/unread") + public ResponseEntity> getUnreadNotifications(@PathVariable Long userId) { + log.info("Fetching unread notifications for user {}", userId); + List notifications = notificationService.getUnreadNotifications(userId); + return ResponseEntity.ok(notifications); + } + + /** + * Mark notification as read. + * + * @param notificationId the notification ID + * @return success response + */ + @PutMapping("/{notificationId}/read") + public ResponseEntity markAsRead(@PathVariable Long notificationId) { + log.info("Marking notification {} as read", notificationId); + notificationService.markAsRead(notificationId); + return ResponseEntity.ok("Notification marked as read"); + } + + /** + * Mark all notifications as read for a user. + * + * @param userId the user ID + * @return success response + */ + @PutMapping("/user/{userId}/read-all") + public ResponseEntity markAllAsRead(@PathVariable Long userId) { + log.info("Marking all notifications as read for user {}", userId); + notificationService.markAllAsRead(userId); + return ResponseEntity.ok("All notifications marked as read"); + } + + /** + * Delete a notification. + * + * @param notificationId the notification ID + * @return success response + */ + @DeleteMapping("/{notificationId}") + public ResponseEntity deleteNotification(@PathVariable Long notificationId) { + log.info("Deleting notification {}", notificationId); + notificationService.deleteNotification(notificationId); + return ResponseEntity.ok("Notification deleted successfully"); + } + + /** + * Delete all read notifications for a user. + * + * @param userId the user ID + * @return success response + */ + @DeleteMapping("/user/{userId}/read") + public ResponseEntity deleteReadNotifications(@PathVariable Long userId) { + log.info("Deleting read notifications for user {}", userId); + notificationService.deleteReadNotifications(userId); + return ResponseEntity.ok("Read notifications deleted successfully"); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java b/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java index 988087b..d33d572 100644 --- a/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java +++ b/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java @@ -17,7 +17,7 @@ @RequestMapping("/api/employee/progress") @RequiredArgsConstructor @Slf4j -@CrossOrigin(origins = "http://localhost:3000") +@CrossOrigin(origins = "http://localhost:3000") public class ProgressUpdateController { private final ProgressService progressService; diff --git a/src/main/java/com/example/ead_backend/controller/ProgressViewController.java b/src/main/java/com/example/ead_backend/controller/ProgressViewController.java index d64600f..a03403f 100644 --- a/src/main/java/com/example/ead_backend/controller/ProgressViewController.java +++ b/src/main/java/com/example/ead_backend/controller/ProgressViewController.java @@ -16,7 +16,7 @@ @RequestMapping("/api/customer/progress") @RequiredArgsConstructor @Slf4j -@CrossOrigin(origins = {"http://localhost:3001", "http://localhost:3000"}) +@CrossOrigin(origins = { "http://localhost:3001", "http://localhost:3000" }) public class ProgressViewController { private final ProgressService progressService; diff --git a/src/main/java/com/example/ead_backend/model/enums/NotificationType.java b/src/main/java/com/example/ead_backend/model/enums/NotificationType.java index 7e62695..8eb216d 100644 --- a/src/main/java/com/example/ead_backend/model/enums/NotificationType.java +++ b/src/main/java/com/example/ead_backend/model/enums/NotificationType.java @@ -14,6 +14,16 @@ public enum NotificationType { */ STATUS_CHANGE, + /** + * Notification for service completion (100% progress) + */ + COMPLETION, + + /** + * Alert notification for delays or time overruns + */ + DELAY_ALERT, + /** * General notifications */ diff --git a/src/main/java/com/example/ead_backend/repository/NotificationRepository.java b/src/main/java/com/example/ead_backend/repository/NotificationRepository.java index 881591e..c2a1126 100644 --- a/src/main/java/com/example/ead_backend/repository/NotificationRepository.java +++ b/src/main/java/com/example/ead_backend/repository/NotificationRepository.java @@ -20,4 +20,22 @@ public interface NotificationRepository extends JpaRepository findByUserIdOrderByCreatedAtDesc(Long userId); + + /** + * Find all unread notifications for a specific user ordered by creation date + * descending. + * + * @param userId the user ID + * @return list of unread notifications + */ + List findByUserIdAndIsReadFalseOrderByCreatedAtDesc(Long userId); + + /** + * Find all read notifications for a specific user ordered by creation date + * descending. + * + * @param userId the user ID + * @return list of read notifications + */ + List findByUserIdAndIsReadTrueOrderByCreatedAtDesc(Long userId); } diff --git a/src/main/java/com/example/ead_backend/service/NotificationService.java b/src/main/java/com/example/ead_backend/service/NotificationService.java index b1745d5..a38d853 100644 --- a/src/main/java/com/example/ead_backend/service/NotificationService.java +++ b/src/main/java/com/example/ead_backend/service/NotificationService.java @@ -34,4 +34,33 @@ public interface NotificationService { * @param notificationId the notification ID */ void markAsRead(Long notificationId); + + /** + * Get unread notifications for a user. + * + * @param userId the user ID + * @return list of unread notification DTOs + */ + List getUnreadNotifications(Long userId); + + /** + * Mark all notifications as read for a user. + * + * @param userId the user ID + */ + void markAllAsRead(Long userId); + + /** + * Delete a notification. + * + * @param notificationId the notification ID + */ + void deleteNotification(Long notificationId); + + /** + * Delete all read notifications for a user. + * + * @param userId the user ID + */ + void deleteReadNotifications(Long userId); } diff --git a/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java b/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java index 565ea6b..a36489e 100644 --- a/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java +++ b/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java @@ -1,7 +1,10 @@ package com.example.ead_backend.service; +import com.example.ead_backend.model.entity.Appointment; import com.example.ead_backend.model.entity.ProgressUpdate; import com.example.ead_backend.repository.ProgressUpdateRepository; +import com.example.ead_backend.repository.TimeLogRepository; +import com.example.ead_backend.repository.AppointmentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -9,7 +12,8 @@ import java.util.List; /** - * Service for calculating progress percentages based on recorded updates. + * Service for calculating progress percentages based on recorded updates and + * time logged. */ @Service @RequiredArgsConstructor @@ -17,6 +21,8 @@ public class ProgressCalculationService { private final ProgressUpdateRepository progressUpdateRepository; + private final TimeLogRepository timeLogRepository; + private final AppointmentRepository appointmentRepository; /** * Calculate the average progress percentage for an appointment. @@ -58,4 +64,94 @@ public int getLatestProgress(Long appointmentId) { return updates.get(updates.size() - 1).getPercentage(); } + + /** + * Calculate time-based progress for an appointment. + * Compares actual time logged against estimated time. + * + * Formula: (Total Time Logged / Estimated Time) * 100 + * + * @param appointmentId the appointment ID + * @return time-based progress percentage (0-100+, can exceed 100 if overtime) + */ + public int calculateTimeBasedProgress(Long appointmentId) { + try { + // 1. Get total time logged + Double totalLoggedHours = timeLogRepository.getTotalHoursLogged(appointmentId); + + if (totalLoggedHours == null || totalLoggedHours == 0) { + log.debug("No time logged yet for appointment {}", appointmentId); + return 0; + } + + // 2. Get appointment estimated hours + Appointment appointment = appointmentRepository.findById(appointmentId) + .orElse(null); + + if (appointment == null) { + log.warn("Appointment {} not found", appointmentId); + return 0; + } + + Double estimatedHours = appointment.getEstimatedHours(); + + if (estimatedHours == null || estimatedHours <= 0) { + log.debug("No estimated hours set for appointment {}", appointmentId); + return 0; + } + + // 3. Calculate percentage: (logged / estimated) * 100 + int percentage = (int) Math.round((totalLoggedHours / estimatedHours) * 100); + + log.debug("Time-based progress for appointment {}: {}h / {}h = {}%", + appointmentId, totalLoggedHours, estimatedHours, percentage); + + // Allow percentage to exceed 100 to indicate overtime + return percentage; + + } catch (Exception e) { + log.error("Error calculating time-based progress for appointment {}", appointmentId, e); + return 0; + } + } + + /** + * Check if appointment is behind schedule based on time analysis. + * + * @param appointmentId the appointment ID + * @param currentManualProgress current manually entered progress % + * @return true if time elapsed exceeds progress by significant margin + */ + public boolean isAppointmentDelayed(Long appointmentId, int currentManualProgress) { + try { + // Calculate time-based progress (how much time has been used) + int timeBasedProgress = calculateTimeBasedProgress(appointmentId); + + if (timeBasedProgress == 0) { + // No time logged yet, can't determine delay + return false; + } + + // Calculate the gap: if time used exceeds work progress, it's delayed + // Example: 80% of time used but only 50% work done = 30% gap = delayed + int progressGap = timeBasedProgress - currentManualProgress; + + // Threshold: If gap exceeds 20%, consider it delayed + boolean isDelayed = progressGap > 20; + + if (isDelayed) { + log.warn("Appointment {} is DELAYED: {}% time used but only {}% work completed (gap: {}%)", + appointmentId, timeBasedProgress, currentManualProgress, progressGap); + } else { + log.debug("Appointment {} is on schedule: {}% time used, {}% work completed", + appointmentId, timeBasedProgress, currentManualProgress); + } + + return isDelayed; + + } catch (Exception e) { + log.error("Error checking delay status for appointment {}", appointmentId, e); + return false; + } + } } diff --git a/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java b/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java index e7f90b6..f170634 100644 --- a/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java +++ b/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java @@ -24,6 +24,7 @@ public class WebSocketNotificationService { /** * Broadcast a progress update to all subscribers of a specific appointment. + * Also broadcasts to global topic for all employees. * * @param appointmentId the appointment ID * @param response the progress response @@ -40,10 +41,14 @@ public void broadcastProgressUpdate(Long appointmentId, ProgressResponse respons .timestamp(Timestamp.from(Instant.now())) .build(); - String destination = "/topic/progress." + appointmentId; - messagingTemplate.convertAndSend(destination, message); + // Broadcast to appointment-specific topic + String appointmentTopic = "/topic/progress." + appointmentId; + messagingTemplate.convertAndSend(appointmentTopic, message); + log.debug("Progress update broadcasted to {}", appointmentTopic); - log.debug("Progress update broadcasted to {}", destination); + // Broadcast to global progress updates topic for all employees + messagingTemplate.convertAndSend("/topic/progress-updates", message); + log.debug("Progress update broadcasted to global topic: /topic/progress-updates"); } /** diff --git a/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java index 72bc691..33088c2 100644 --- a/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java +++ b/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java @@ -65,4 +65,48 @@ public void markAsRead(Long notificationId) { log.info("Notification {} marked as read", notificationId); }); } + + @Override + @Transactional(readOnly = true) + public List getUnreadNotifications(Long userId) { + log.debug("Fetching unread notifications for user {}", userId); + List notifications = notificationRepository + .findByUserIdAndIsReadFalseOrderByCreatedAtDesc(userId); + + return notifications.stream() + .map(notificationMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void markAllAsRead(Long userId) { + log.info("Marking all notifications as read for user {}", userId); + List notifications = notificationRepository + .findByUserIdAndIsReadFalseOrderByCreatedAtDesc(userId); + + notifications.forEach(notification -> notification.setIsRead(true)); + notificationRepository.saveAll(notifications); + + log.info("Marked {} notifications as read for user {}", notifications.size(), userId); + } + + @Override + @Transactional + public void deleteNotification(Long notificationId) { + log.info("Deleting notification {}", notificationId); + notificationRepository.deleteById(notificationId); + } + + @Override + @Transactional + public void deleteReadNotifications(Long userId) { + log.info("Deleting read notifications for user {}", userId); + List readNotifications = notificationRepository + .findByUserIdAndIsReadTrueOrderByCreatedAtDesc(userId); + + notificationRepository.deleteAll(readNotifications); + + log.info("Deleted {} read notifications for user {}", readNotifications.size(), userId); + } } diff --git a/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java index 6caa1e3..d7ce578 100644 --- a/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java +++ b/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java @@ -49,24 +49,49 @@ public ProgressResponse createOrUpdateProgress(Long appointmentId, ProgressUpdat ProgressUpdate saved = progressUpdateRepository.save(progressUpdate); log.debug("Progress update saved with ID: {}", saved.getId()); - // Calculate overall progress percentage + // Calculate overall progress percentage (time-based calculation) int overallProgress = progressCalculationService.getLatestProgress(appointmentId); log.debug("Overall progress for appointment {}: {}%", appointmentId, overallProgress); - // Create notification + // Create base notification String notificationMessage = String.format( "Progress updated for appointment #%d: %s (%d%%)", appointmentId, request.getStage(), request.getPercentage()); + NotificationType notificationType = NotificationType.PROGRESS_UPDATE; + + // Check for completion (100%) - trigger completion notification + if (request.getPercentage() >= 100) { + notificationMessage = String.format( + "Service completed for appointment #%d! Final stage: %s", + appointmentId, request.getStage()); + notificationType = NotificationType.COMPLETION; + log.info("Appointment {} marked as COMPLETED (100%)", appointmentId); + } + + // Check for delays (time overrun) + // In real scenario, fetch estimated time from appointment and compare with + // actual time logged + // For now, we'll check if progress is less than expected at this time + // Example: If 80% time passed but only 60% progress, it's delayed + boolean isDelayed = checkForDelays(appointmentId, request.getPercentage()); + if (isDelayed && request.getPercentage() < 100) { + notificationMessage = String.format( + "DELAY ALERT: Appointment #%d is behind schedule. Current progress: %d%%", + appointmentId, request.getPercentage()); + notificationType = NotificationType.DELAY_ALERT; + log.warn("Appointment {} is DELAYED - progress behind schedule", appointmentId); + } + Notification notification = Notification.builder() .userId(updatedBy) // In real scenario, should notify customer - .type(NotificationType.PROGRESS_UPDATE) + .type(notificationType) .message(notificationMessage) .isRead(false) .build(); notificationRepository.save(notification); - log.debug("Notification created for progress update"); + log.debug("Notification created: type={}, message={}", notificationType, notificationMessage); // Convert to response ProgressResponse response = progressMapper.toResponse(saved); @@ -94,6 +119,27 @@ public ProgressResponse createOrUpdateProgress(Long appointmentId, ProgressUpdat return response; } + /** + * Check if appointment is delayed based on time logged vs estimated time. + * This is a simplified check - in production, fetch actual time data from + * TimeLog. + * + * @param appointmentId the appointment ID + * @param currentProgress current progress percentage + * @return true if delayed + */ + private boolean checkForDelays(Long appointmentId, int currentProgress) { + // TODO: In production, implement actual time comparison: + // 1. Fetch appointment estimated time (hours) + // 2. Sum all time logged from TimeLog table for this appointment + // 3. Calculate time progress = (logged hours / estimated hours) * 100 + // 4. If time progress > current progress by threshold (e.g., 20%), it's delayed + + // Placeholder logic: assume delayed if progress < 50% (for demo) + // Replace with actual time-based calculation when TimeLog data is available + return currentProgress < 50; + } + @Override @Transactional(readOnly = true) public List getProgressForAppointment(Long appointmentId) {