Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ResponseEntity<ApiResponse<SyncResponse<List<TicketResponse>>>> syncTicke
}

@GetMapping("/notifications")
@PreAuthorize("hasRole('STUDENT')")
@PreAuthorize("hasAnyRole('STUDENT','ADMIN')")
public ResponseEntity<ApiResponse<SyncResponse<List<NotificationResponse>>>> syncNotifications(
@RequestParam(required = false) Instant since
) {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/ucms_backend/controller/TicketController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ucms_backend.controller;

import com.ucms_backend.dto.ApiResponse;
import com.ucms_backend.dto.AssignTicketRequest;
import com.ucms_backend.dto.CreateTicketRequest;
import com.ucms_backend.dto.TicketResponse;
import com.ucms_backend.dto.UpdateStatusRequest;
Expand Down Expand Up @@ -64,6 +65,15 @@ public ResponseEntity<ApiResponse<TicketResponse>> updateStatus(
return ResponseEntity.ok(ApiResponse.ok("Status updated", ticketResponse));
}

@PatchMapping("/{id}/assign")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<TicketResponse>> assignAdmin(
@PathVariable Long id,
@Valid @RequestBody AssignTicketRequest request) {
TicketResponse result = ticketService.assignAdmin(id, request);
return ResponseEntity.ok(ApiResponse.ok("Admin assigned", result));
}

@PatchMapping("/{id}/confirm-resolved")
@PreAuthorize("hasRole('STUDENT')")
public ResponseEntity<ApiResponse<TicketResponse>> confirmResolved(@PathVariable Long id) {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/ucms_backend/dto/AssignTicketRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ucms_backend.dto;

import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssignTicketRequest {

private UUID adminId;
}
8 changes: 8 additions & 0 deletions src/main/java/com/ucms_backend/dto/TicketResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class TicketResponse {
private LocalDateTime urgencyUpdatedAt;
private boolean urgencyOverridden;
private String urgencyOverrideReason;
private java.util.UUID assignedAdminId;
private String assignedAdminName;
private String studentName;
private String studentId;
private String studentCourse;
Expand All @@ -48,6 +50,10 @@ public static TicketResponse from(Ticket ticket, String categoryName, Profile pr
}

public static TicketResponse from(Ticket ticket, String categoryName, Profile profile, boolean hasAdminResponse) {
return from(ticket, categoryName, profile, hasAdminResponse, null);
}

public static TicketResponse from(Ticket ticket, String categoryName, Profile profile, boolean hasAdminResponse, String assignedAdminName) {
return TicketResponse.builder()
.id(ticket.getId())
.ticketNumber(ticket.getTicketNumber())
Expand All @@ -67,6 +73,8 @@ public static TicketResponse from(Ticket ticket, String categoryName, Profile pr
.urgencyUpdatedAt(ticket.getUrgencyUpdatedAt())
.urgencyOverridden(ticket.isUrgencyOverridden())
.urgencyOverrideReason(ticket.getUrgencyOverrideReason())
.assignedAdminId(ticket.getAssignedAdminId())
.assignedAdminName(assignedAdminName)
.studentName(profile != null ? profile.getName() : null)
.studentId(profile != null ? profile.getStudentId() : null)
.studentCourse(profile != null ? profile.getCourse() : null)
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/ucms_backend/model/entity/Ticket.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public class Ticket {
@Column(name = "user_id", nullable = false)
private UUID userId;

@Column(name = "assigned_admin_id")
private UUID assignedAdminId;

@Column(name = "category_id", nullable = false)
private Long categoryId;

Expand Down
48 changes: 45 additions & 3 deletions src/main/java/com/ucms_backend/service/TicketService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ucms_backend.service;

import com.ucms_backend.dto.AssignTicketRequest;
import com.ucms_backend.dto.CreateTicketRequest;
import com.ucms_backend.dto.RealtimeEventResponse;
import com.ucms_backend.dto.TicketResponse;
Expand Down Expand Up @@ -71,20 +72,30 @@ private String resolveCategoryName(Long categoryId) {
.orElse(null);
}

private String resolveAdminName(UUID adminId) {
if (adminId == null) return null;
return profileRepository.findById(adminId)
.map(Profile::getName)
.orElse(null);
}

private TicketResponse toResponse(Ticket ticket) {
boolean hasAdminResponse = ticketResponseRepository.existsByTicketId(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}

private TicketResponse toResponse(Ticket ticket, Set<Long> ticketIdsWithResponses) {
boolean hasAdminResponse = ticketIdsWithResponses.contains(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), null, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}

private TicketResponse toDetailedResponse(Ticket ticket) {
Profile studentProfile = profileRepository.findById(ticket.getUserId()).orElse(null);
boolean hasAdminResponse = ticketResponseRepository.existsByTicketId(ticket.getId());
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), studentProfile, hasAdminResponse);
return TicketResponse.from(ticket, resolveCategoryName(ticket.getCategoryId()), studentProfile, hasAdminResponse,
resolveAdminName(ticket.getAssignedAdminId()));
}
Comment on lines +75 to 99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid N+1 profile queries when populating assignedAdminName.

Lines [75-79] are called from list mapping paths (Lines [84-85], [90-91], [97-98]), causing one profileRepository.findById(...) per ticket. This will degrade admin ticket-list performance.

Refactor direction (batch-resolve admin names)
+// Build once per list request
+private Map<UUID, String> resolveAdminNames(List<Ticket> tickets) {
+    Set<UUID> ids = tickets.stream()
+            .map(Ticket::getAssignedAdminId)
+            .filter(java.util.Objects::nonNull)
+            .collect(java.util.stream.Collectors.toSet());
+    if (ids.isEmpty()) return Map.of();
+    return profileRepository.findAllById(ids).stream()
+            .collect(java.util.stream.Collectors.toMap(Profile::getAuthUserId, Profile::getName));
+}

Then pass the map into list mappers instead of calling findById per ticket.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ucms_backend/service/TicketService.java` around lines 75 -
99, The code currently calls resolveAdminName(UUID) inside
toResponse/toDetailedResponse for each ticket, causing N+1 queries; instead,
collect all assignedAdminId values for a ticket list, query
profileRepository.findAllById(...) once to build a Map<UUID,String> of
adminId->name, and change the mappers to accept that map (e.g., add overloads
toResponse(Ticket, Set<Long>, Map<UUID,String>) and toDetailedResponse(Ticket,
Map<UUID,String>) or modify resolveAdminName to accept the map) so the mapping
logic uses map.get(adminId) rather than profileRepository.findById per ticket;
ensure callers that build lists compute the map and pass it into the mapper
methods.


private void applyUrgency(Ticket ticket) {
Expand Down Expand Up @@ -337,6 +348,37 @@ public TicketResponse confirmResolved(Long ticketId) {
return toResponse(saved);
}

public TicketResponse assignAdmin(Long ticketId, AssignTicketRequest request) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new AppException(404, "TICKET_NOT_FOUND", "Ticket not found"));

if (ticket.getStatus() != TicketStatus.PENDING) {
throw new AppException(409, "INVALID_ASSIGNMENT", "Can only assign admin to PENDING tickets");
}

UUID targetAdminId = (request.getAdminId() != null)
? request.getAdminId()
: SecurityUtils.getCurrentUserId();

Profile admin = profileRepository.findById(targetAdminId)
.orElseThrow(() -> new AppException(404, "ADMIN_NOT_FOUND", "Admin not found"));

if (!"ADMIN".equals(admin.getRole())) {
throw new AppException(400, "INVALID_ADMIN", "Target user is not an admin");
}

ticket.setAssignedAdminId(targetAdminId);
Ticket saved = ticketRepository.save(ticket);

notificationService.createNotification(
targetAdminId,
saved.getId(),
"You have been assigned ticket #" + saved.getTicketNumber()
);

return toDetailedResponse(saved);
}
Comment on lines +351 to +380

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Publish a realtime ticket event after admin assignment.

assignAdmin(...) mutates ticket state (Line [370]) but does not call publishTicketEvent(...), unlike other ticket mutations. Clients relying on SSE may not see assignment updates until refresh.

Suggested fix
         ticket.setAssignedAdminId(targetAdminId);
         Ticket saved = ticketRepository.save(ticket);
+        publishTicketEvent(saved, "TICKET_ASSIGNED", "ADMIN");
 
         notificationService.createNotification(
                 targetAdminId,
                 saved.getId(),
                 "You have been assigned ticket #" + saved.getTicketNumber()
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ucms_backend/service/TicketService.java` around lines 351 -
380, The assignAdmin method updates and saves the Ticket but never publishes a
realtime event; after saving the ticket (Ticket saved =
ticketRepository.save(ticket)) invoke publishTicketEvent(saved) (or the existing
publishTicketEvent method used by other mutations) so the SSE/WS subscribers
receive the assignment update, and keep the existing
notificationService.createNotification and return toDetailedResponse(saved) as
before.


public TicketResponse updateStatus(Long id, UpdateStatusRequest request) {
Ticket ticket = ticketRepository.findById(id)
.orElseThrow(() -> new AppException(404, "TICKET_NOT_FOUND", "Ticket not found"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE ticket
ADD COLUMN assigned_admin_id UUID REFERENCES profile(auth_user_id);
Loading