Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/main/java/com/example/ead_backend/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<List<NotificationDTO>> getNotifications(@PathVariable Long userId) {
log.info("Fetching notifications for user {}", userId);
List<NotificationDTO> 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<List<NotificationDTO>> getUnreadNotifications(@PathVariable Long userId) {
log.info("Fetching unread notifications for user {}", userId);
List<NotificationDTO> 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<String> 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<String> 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<String> 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<String> deleteReadNotifications(@PathVariable Long userId) {
log.info("Deleting read notifications for user {}", userId);
notificationService.deleteReadNotifications(userId);
return ResponseEntity.ok("Read notifications deleted successfully");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,22 @@ public interface NotificationRepository extends JpaRepository<Notification, Long
* @return list of notifications
*/
List<Notification> 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<Notification> 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<Notification> findByUserIdAndIsReadTrueOrderByCreatedAtDesc(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NotificationDTO> 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);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
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;

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
@Slf4j
public class ProgressCalculationService {

private final ProgressUpdateRepository progressUpdateRepository;
private final TimeLogRepository timeLogRepository;
private final AppointmentRepository appointmentRepository;

/**
* Calculate the average progress percentage for an appointment.
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
}

/**
Expand Down
Loading