From 7e8faa0e9aac938cfa143f4b09a3255f310f901d Mon Sep 17 00:00:00 2001 From: kennethdomdom Date: Sun, 12 Apr 2026 21:54:19 +0800 Subject: [PATCH 1/2] feat/add manual assigning --- .../controller/TicketController.java | 10 ++++ .../ucms_backend/dto/AssignTicketRequest.java | 14 ++++++ .../com/ucms_backend/dto/TicketResponse.java | 8 ++++ .../com/ucms_backend/model/entity/Ticket.java | 3 ++ .../ucms_backend/service/TicketService.java | 48 +++++++++++++++++-- .../V13__add_ticket_assigned_admin.sql | 2 + 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/ucms_backend/dto/AssignTicketRequest.java create mode 100644 src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql diff --git a/src/main/java/com/ucms_backend/controller/TicketController.java b/src/main/java/com/ucms_backend/controller/TicketController.java index 7f0583f..14c3080 100644 --- a/src/main/java/com/ucms_backend/controller/TicketController.java +++ b/src/main/java/com/ucms_backend/controller/TicketController.java @@ -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; @@ -64,6 +65,15 @@ public ResponseEntity> updateStatus( return ResponseEntity.ok(ApiResponse.ok("Status updated", ticketResponse)); } + @PatchMapping("/{id}/assign") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> 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> confirmResolved(@PathVariable Long id) { diff --git a/src/main/java/com/ucms_backend/dto/AssignTicketRequest.java b/src/main/java/com/ucms_backend/dto/AssignTicketRequest.java new file mode 100644 index 0000000..d84897a --- /dev/null +++ b/src/main/java/com/ucms_backend/dto/AssignTicketRequest.java @@ -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; +} diff --git a/src/main/java/com/ucms_backend/dto/TicketResponse.java b/src/main/java/com/ucms_backend/dto/TicketResponse.java index 2c37f10..550e6a5 100644 --- a/src/main/java/com/ucms_backend/dto/TicketResponse.java +++ b/src/main/java/com/ucms_backend/dto/TicketResponse.java @@ -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; @@ -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()) @@ -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) diff --git a/src/main/java/com/ucms_backend/model/entity/Ticket.java b/src/main/java/com/ucms_backend/model/entity/Ticket.java index 8224d9b..87cab89 100644 --- a/src/main/java/com/ucms_backend/model/entity/Ticket.java +++ b/src/main/java/com/ucms_backend/model/entity/Ticket.java @@ -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; diff --git a/src/main/java/com/ucms_backend/service/TicketService.java b/src/main/java/com/ucms_backend/service/TicketService.java index c4b00d4..70bb866 100644 --- a/src/main/java/com/ucms_backend/service/TicketService.java +++ b/src/main/java/com/ucms_backend/service/TicketService.java @@ -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; @@ -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 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())); } private void applyUrgency(Ticket ticket) { @@ -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); + } + public TicketResponse updateStatus(Long id, UpdateStatusRequest request) { Ticket ticket = ticketRepository.findById(id) .orElseThrow(() -> new AppException(404, "TICKET_NOT_FOUND", "Ticket not found")); diff --git a/src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql b/src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql new file mode 100644 index 0000000..2eeb6c3 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_ticket_assigned_admin.sql @@ -0,0 +1,2 @@ +ALTER TABLE ticket + ADD COLUMN assigned_admin_id UUID REFERENCES profile(auth_user_id); From ee82678eaad02428b243e4430b4443d70ed9b531 Mon Sep 17 00:00:00 2001 From: kennethdomdom Date: Mon, 13 Apr 2026 04:47:30 +0800 Subject: [PATCH 2/2] fix/notification syncing --- src/main/java/com/ucms_backend/controller/SyncController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ucms_backend/controller/SyncController.java b/src/main/java/com/ucms_backend/controller/SyncController.java index 3aee095..80d0d91 100644 --- a/src/main/java/com/ucms_backend/controller/SyncController.java +++ b/src/main/java/com/ucms_backend/controller/SyncController.java @@ -43,7 +43,7 @@ public ResponseEntity>>> syncTicke } @GetMapping("/notifications") - @PreAuthorize("hasRole('STUDENT')") + @PreAuthorize("hasAnyRole('STUDENT','ADMIN')") public ResponseEntity>>> syncNotifications( @RequestParam(required = false) Instant since ) {