diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/DenyFeedbackRequestDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/DenyFeedbackRequestDTO.java new file mode 100644 index 000000000..088d8510b --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/DenyFeedbackRequestDTO.java @@ -0,0 +1,57 @@ +package com.objectcomputing.checkins.services.feedback_request.DTO; + +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Introspected + +public class DenyFeedbackRequestDTO { + + @NotBlank(message = "Reason cannot be blank") + private String reason; + + @NotNull(message = "Denier cannot be null") + @Valid + private UserDTO denier; + + @NotNull(message = "Creator cannot be null") + @Valid + private UserDTO creator; + + // Constructors + public DenyFeedbackRequestDTO() {} + + public DenyFeedbackRequestDTO(String reason, UserDTO denier, UserDTO creator) { + this.reason = reason; + this.denier = denier; + this.creator = creator; + } + + // Getters + public String getReason() { + return reason; + } + + public UserDTO getDenier() { + return denier; + } + + public UserDTO getCreator() { + return creator; + } + + // Setters + public void setReason(String reason) { + this.reason = reason; + } + + public void setDenier(UserDTO denier) { + this.denier = denier; + } + + public void setCreator(UserDTO creator) { + this.creator = creator; + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/UserDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/UserDTO.java new file mode 100644 index 000000000..dc6823f65 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/DTO/UserDTO.java @@ -0,0 +1,51 @@ +package com.objectcomputing.checkins.services.feedback_request.DTO; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +import io.micronaut.core.annotation.Introspected; + + +@Introspected + +public class UserDTO { + + @NotNull(message = "User ID cannot be null") + private UUID id; + + @NotBlank(message = "User name cannot be blank") + private String name; + + // Constructors + public UserDTO() {} + + // Constructor with only ID (for cases where only ID is required) + public UserDTO(UUID id) { + this.id = id; + } + + // Constructor with ID and name (for cases where both ID and name are required) + public UserDTO(UUID id, String name) { + this.id = id; + this.name = name; + } + + // Getters + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + // Setters + public void setId(UUID id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequest.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequest.java index 5753f7b98..2d83aed71 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequest.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequest.java @@ -12,6 +12,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; import lombok.Getter; import lombok.Setter; @@ -86,6 +87,18 @@ public class FeedbackRequest { @Schema(description = "the id of the review period in that this request was created for") private UUID reviewPeriodId; + @Column(name = "denied") + @Nullable + @Schema(description = "Whether the feedback request has been denied") + private boolean denied = false; + + @Column(name = "reason") + @Nullable + @Schema(description = "Denial reason") + private String reason; + + + public FeedbackRequest(UUID creatorId, UUID requesteeId, UUID recipientId, @@ -94,7 +107,9 @@ public FeedbackRequest(UUID creatorId, @Nullable LocalDate dueDate, String status, @Nullable LocalDate submitDate, - @Nullable UUID reviewPeriodId) { + @Nullable UUID reviewPeriodId, + boolean denied, + @Nullable String reason) { this.id = null; this.creatorId = creatorId; this.requesteeId = requesteeId; @@ -105,6 +120,8 @@ public FeedbackRequest(UUID creatorId, this.status = status; this.submitDate = submitDate; this.reviewPeriodId = reviewPeriodId; + this.denied = denied; + this.reason = reason; } public FeedbackRequest() {} @@ -123,12 +140,14 @@ public boolean equals(Object o) { && Objects.equals(dueDate, that.dueDate) && Objects.equals(status, that.status) && Objects.equals(submitDate, that.submitDate) - && Objects.equals(reviewPeriodId, that.reviewPeriodId); + && Objects.equals(reviewPeriodId, that.reviewPeriodId) + && Objects.equals(denied, that.denied) + && Objects.equals(reason, that.reason); } @Override public int hashCode() { - return Objects.hash(id, creatorId, recipientId, requesteeId, sendDate, templateId, dueDate, status, submitDate, reviewPeriodId); + return Objects.hash(id, creatorId, recipientId, requesteeId, sendDate, templateId, dueDate, status, submitDate, reviewPeriodId, denied, reason); } @Override @@ -144,6 +163,8 @@ public String toString() { ", status='" + status + ", submitDate='" + submitDate + ", reviewPeriodId='" + reviewPeriodId + + ", denied='" + denied + + ", reason='" + reason + '}'; } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestController.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestController.java index 1e2fa3a27..bcddec824 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestController.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestController.java @@ -2,6 +2,9 @@ import com.objectcomputing.checkins.services.permissions.Permission; import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import com.objectcomputing.checkins.services.feedback_request.DTO.DenyFeedbackRequestDTO; +import com.objectcomputing.checkins.services.feedback_request.DTO.UserDTO; +import com.objectcomputing.checkins.services.notification.NotificationService; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.format.Format; import io.micronaut.http.HttpResponse; @@ -10,6 +13,7 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Delete; import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Put; import io.micronaut.http.annotation.Status; @@ -19,6 +23,7 @@ import io.micronaut.security.rules.SecurityRule; import io.micronaut.validation.Validated; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Inject; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -35,9 +40,12 @@ public class FeedbackRequestController { private final FeedbackRequestServices feedbackReqServices; + private final NotificationService notificationService; - public FeedbackRequestController(FeedbackRequestServices feedbackReqServices) { + @Inject + public FeedbackRequestController(FeedbackRequestServices feedbackReqServices, NotificationService notificationService) { this.feedbackReqServices = feedbackReqServices; + this.notificationService = notificationService; } /** @@ -91,7 +99,7 @@ public void delete(@NotNull UUID id) { public HttpResponse getById(UUID id) { FeedbackRequest savedFeedbackRequest = feedbackReqServices.getById(id); return savedFeedbackRequest == null ? HttpResponse.notFound() : HttpResponse.ok(fromEntity(savedFeedbackRequest)) - .headers(headers -> headers.location(URI.create("/feedback_request" + savedFeedbackRequest.getId()))); + .headers(headers -> headers.location(URI.create("/feedback_request/" + savedFeedbackRequest.getId()))); } /** @@ -106,13 +114,68 @@ public HttpResponse getById(UUID id) { */ @RequiredPermission(Permission.CAN_VIEW_FEEDBACK_REQUEST) @Get("/{?creatorId,requesteeId,recipientId,oldestDate,reviewPeriodId,templateId,requesteeIds}") - public List findByValues(@Nullable UUID creatorId, @Nullable UUID requesteeId, @Nullable UUID recipientId, @Nullable @Format("yyyy-MM-dd") LocalDate oldestDate, @Nullable UUID reviewPeriodId, @Nullable UUID templateId, @Nullable List requesteeIds) { + public List findByValues( + @Nullable UUID creatorId, + @Nullable UUID requesteeId, + @Nullable UUID recipientId, + @Nullable @Format("yyyy-MM-dd") LocalDate oldestDate, + @Nullable UUID reviewPeriodId, + @Nullable UUID templateId, + @Nullable List requesteeIds) { return feedbackReqServices.findByValues(creatorId, requesteeId, recipientId, oldestDate, reviewPeriodId, templateId, requesteeIds) .stream() .map(this::fromEntity) .toList(); } + /** + * Deny a feedback request + * + * @param id {@link UUID} ID of the feedback request to deny + * @param body Request body containing reason, denier, and creator information + * @return {@link FeedbackRequestResponseDTO} with updated denial status + */ + @Post("/{id}/deny") + @RequiredPermission(Permission.CAN_DENY_FEEDBACK_REQUEST) + public HttpResponse denyFeedbackRequest( + @PathVariable("id") @NotNull UUID id, + @Body @Valid DenyFeedbackRequestDTO body + ) { + FeedbackRequest feedbackRequest = feedbackReqServices.getById(id); + if (feedbackRequest == null) { + return HttpResponse.notFound(); + } + + String reason = body.getReason(); + UserDTO denier = body.getDenier(); + UserDTO creator = body.getCreator(); + + if (!feedbackRequest.isDenied() && reason != null && !reason.trim().isEmpty() && denier != null && creator != null) { + FeedbackRequestUpdateDTO dto = new FeedbackRequestUpdateDTO(); + dto.setId(feedbackRequest.getId()); + dto.setDueDate(feedbackRequest.getDueDate()); + dto.setStatus(feedbackRequest.getStatus()); + dto.setSubmitDate(feedbackRequest.getSubmitDate()); + dto.setRecipientId(feedbackRequest.getRecipientId()); + dto.setDenied(true); + dto.setReason(reason); + + FeedbackRequest updatedFeedbackRequest = feedbackReqServices.update(dto); + + UUID creatorId = creator.getId(); + String denierName = denier.getName(); + notificationService.sendNotification( + creatorId, + String.format("Your feedback request was denied by %s. Reason: %s", denierName, reason) + ); + + return HttpResponse.ok(fromEntity(updatedFeedbackRequest)); + } else { + return HttpResponse.badRequest(); + + } +} + private FeedbackRequestResponseDTO fromEntity(FeedbackRequest feedbackRequest) { FeedbackRequestResponseDTO dto = new FeedbackRequestResponseDTO(); dto.setId(feedbackRequest.getId()); @@ -125,7 +188,8 @@ private FeedbackRequestResponseDTO fromEntity(FeedbackRequest feedbackRequest) { dto.setStatus(feedbackRequest.getStatus()); dto.setSubmitDate(feedbackRequest.getSubmitDate()); dto.setReviewPeriodId(feedbackRequest.getReviewPeriodId()); - + dto.setDenied(feedbackRequest.isDenied()); + dto.setReason(feedbackRequest.getReason()); return dto; } @@ -139,6 +203,9 @@ private FeedbackRequest fromDTO(FeedbackRequestCreateDTO dto) { dto.getDueDate(), dto.getStatus(), dto.getSubmitDate(), - dto.getReviewPeriodId()); + dto.getReviewPeriodId(), + dto.isDenied(), + dto.getReason() + ); } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestCreateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestCreateDTO.java index 91674b498..3ecda5b80 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestCreateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestCreateDTO.java @@ -4,6 +4,7 @@ import io.micronaut.core.annotation.Nullable; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Null; import lombok.Getter; import lombok.Setter; @@ -51,5 +52,12 @@ public class FeedbackRequestCreateDTO { @Schema(description = "the id of the review period in that this request was created for") private UUID reviewPeriodId; + @Schema(description = "Whether the feedback request has been denied") + private boolean denied = false; + + @Nullable + @Schema(description = "Reason for the request being denied") + private String reason; + } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestDenialDTO.txt b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestDenialDTO.txt new file mode 100644 index 000000000..bebcae8a9 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestDenialDTO.txt @@ -0,0 +1,59 @@ +package com.objectcomputing.checkins.services.feedback_request; + +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import com.objectcomputing.checkins.services.feedback_request.DTO.UserDTO; + + +@Introspected + +public class FeedbackRequestDenialDTO { + + @NotBlank(message = "Reason cannot be blank") + private String reason; + + @NotNull(message = "Denier cannot be null") + @Valid + private UserDTO denier; + + @NotNull(message = "Creator cannot be null") + @Valid + private UserDTO creator; + + // Constructors + public FeedbackRequestDenialDTO() {} + + public FeedbackRequestDenialDTO(String reason, UserDTO denier, UserDTO creator) { + this.reason = reason; + this.denier = denier; + this.creator = creator; + } + + // Getters + public String getReason() { + return reason; + } + + public UserDTO getDenier() { + return denier; + } + + public UserDTO getCreator() { + return creator; + } + + // Setters + public void setReason(String reason) { + this.reason = reason; + } + + public void setDenier(UserDTO denier) { + this.denier = denier; + } + + public void setCreator(UserDTO creator) { + this.creator = creator; + } +} diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestResponseDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestResponseDTO.java index 2b3f0a145..584b54d13 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestResponseDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestResponseDTO.java @@ -55,4 +55,15 @@ public class FeedbackRequestResponseDTO { @Schema(description = "the id of the review period in that this request was created for") private UUID reviewPeriodId; + @Schema(description = "Whether the feedback request has been denied") + private boolean denied = false; + + @Nullable + @Schema(description = "Reason for the request being denied") + private String reason; + + public FeedbackRequestResponseDTO() { + this.denied = false; + } + } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServices.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServices.java index 90cfd09ce..f5c0a844f 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServices.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServices.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.UUID; +import io.micronaut.http.HttpResponse; + public interface FeedbackRequestServices { FeedbackRequest save(FeedbackRequest feedbackRequest); @@ -14,4 +16,5 @@ public interface FeedbackRequestServices { FeedbackRequest getById(UUID id); List findByValues(UUID creatorId, UUID requesteeId, UUID recipientId, LocalDate oldestDate, UUID reviewPeriodId, UUID templateId, List requesteeIds); + } \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServicesImpl.java index 180d8fef3..e6718126a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestServicesImpl.java @@ -11,6 +11,7 @@ import com.objectcomputing.checkins.services.memberprofile.MemberProfileUtils; import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; +import com.objectcomputing.checkins.services.permissions.Permission; import com.objectcomputing.checkins.services.reviews.ReviewAssignment; import com.objectcomputing.checkins.services.reviews.ReviewAssignmentRepository; import com.objectcomputing.checkins.services.reviews.ReviewPeriod; @@ -170,6 +171,15 @@ public FeedbackRequest update(FeedbackRequestUpdateDTO feedbackRequestUpdateDTO) throw new BadArgException("Cannot update feedback request that does not exist"); } + if (feedbackRequest.isDenied()) { + UUID currentUserId = currentUserServices.getCurrentUser().getId(); + if (!currentUserId.equals(originalFeedback.getRecipientId())) { + if (!currentUserServices.hasPermission(Permission.CAN_ADMINISTER_FEEDBACK_REQUESTS)) { + throw new PermissionException(NOT_AUTHORIZED_MSG); + } + } + } + validateMembers(originalFeedback); Set reviewAssignmentsSet = Set.of(); @@ -215,6 +225,10 @@ public FeedbackRequest update(FeedbackRequestUpdateDTO feedbackRequestUpdateDTO) throw new BadArgException("Send date of feedback request must be before the due date."); } + if (feedbackRequest.isDenied() && (feedbackRequestUpdateDTO.getReason() == null || feedbackRequestUpdateDTO.getReason().trim().isEmpty())) { + throw new BadArgException("A reason must be provided for denying the request."); + } + FeedbackRequest storedRequest = feedbackReqRepository.update(feedbackRequest); MemberProfile reviewer = memberProfileServices.getById(storedRequest.getRecipientId()); MemberProfile requestee = memberProfileServices.getById(storedRequest.getRequesteeId()); @@ -371,7 +385,8 @@ private FeedbackRequest getFromDTO(FeedbackRequestUpdateDTO dto) { feedbackRequest.setStatus(dto.getStatus()); feedbackRequest.setSubmitDate(dto.getSubmitDate()); feedbackRequest.setRecipientId(dto.getRecipientId()); - + feedbackRequest.setDenied(dto.isDenied()); + feedbackRequest.setReason(dto.getReason()); return feedbackRequest; } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestUpdateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestUpdateDTO.java index 6c25849ec..71f8df406 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestUpdateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/feedback_request/FeedbackRequestUpdateDTO.java @@ -4,6 +4,7 @@ import io.micronaut.core.annotation.Nullable; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import com.objectcomputing.checkins.services.feedback_request.DTO.UserDTO; import lombok.Getter; import lombok.Setter; @@ -35,4 +36,11 @@ public class FeedbackRequestUpdateDTO { @Schema(description = "the recipient of the request, used to reassign") private UUID recipientId; + @Schema(description = "Whether the feedback request has been denied") + private boolean denied = false; + + @Nullable + @Schema(description = "Reason for the request being denied") + private String reason; + } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServices.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServices.java index 98a3d154c..c5eb5c4c2 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServices.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServices.java @@ -2,6 +2,8 @@ import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.role.RoleType; +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.exceptions.PermissionException; public interface CurrentUserServices { @@ -13,4 +15,6 @@ public interface CurrentUserServices { MemberProfile getCurrentUser(); + boolean hasPermission(Permission permission); + } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java index ae063117c..f6f1a826a 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/memberprofile/currentuser/CurrentUserServicesImpl.java @@ -4,6 +4,7 @@ import com.objectcomputing.checkins.exceptions.NotFoundException; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository; +import com.objectcomputing.checkins.services.permissions.Permission; import com.objectcomputing.checkins.services.role.Role; import com.objectcomputing.checkins.services.role.RoleServices; import com.objectcomputing.checkins.services.role.RoleType; @@ -84,4 +85,10 @@ private MemberProfile saveNewUser(String firstName, String lastName, String work return createdMember; } + + @Override + public boolean hasPermission(Permission permission) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'hasPermission'"); + } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationController.java b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationController.java new file mode 100644 index 000000000..457ea8b31 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationController.java @@ -0,0 +1,45 @@ +package com.objectcomputing.checkins.services.notification; + +import com.objectcomputing.checkins.services.permissions.Permission; +import com.objectcomputing.checkins.services.permissions.RequiredPermission; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.scheduling.annotation.ExecuteOn; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.validation.Validated; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +@Validated +@Controller("/services/notifications") +@ExecuteOn(TaskExecutors.BLOCKING) +@Secured(SecurityRule.IS_AUTHENTICATED) +@Tag(name = "notification") +public class NotificationController { + + private final NotificationService notificationService; + + public NotificationController(NotificationService notificationService) { + this.notificationService = notificationService; + } + + /** + * Send a notification to a user + * + * @param notificationDTO {@link NotificationDTO} containing the userId and message + * @return {@link HttpResponse} with status indicating success or error + */ + @Post("/send") + @RequiredPermission(Permission.CAN_SEND_NOTIFICATIONS) + public HttpResponse sendNotification(@Body @Valid @NotNull NotificationDTO notificationDTO) { + notificationService.sendNotification(notificationDTO.getUserId(), notificationDTO.getMessage()); + return HttpResponse.ok(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationDTO.java new file mode 100644 index 000000000..0ac78bdd0 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationDTO.java @@ -0,0 +1,33 @@ +package com.objectcomputing.checkins.services.notification; + +import io.micronaut.core.annotation.Introspected; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@Introspected +public class NotificationDTO { + + @NotNull + private UUID userId; + + @NotBlank + private String message; + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationService.java b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationService.java new file mode 100644 index 000000000..754aca9e3 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationService.java @@ -0,0 +1,8 @@ +package com.objectcomputing.checkins.services.notification; + +import java.util.UUID; + +public interface NotificationService { + + void sendNotification(UUID userId, String message); +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationServiceImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationServiceImpl.java new file mode 100644 index 000000000..2f8e5b9f1 --- /dev/null +++ b/server/src/main/java/com/objectcomputing/checkins/services/notification/NotificationServiceImpl.java @@ -0,0 +1,66 @@ +package com.objectcomputing.checkins.services.notification; + +import io.micronaut.core.annotation.NonNull; +import com.objectcomputing.checkins.notifications.email.EmailSender; +import com.objectcomputing.checkins.services.memberprofile.MemberProfile; +import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices; +import com.objectcomputing.checkins.services.memberprofile.currentuser.CurrentUserServices; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.objectcomputing.checkins.notifications.email.MailJetFactory; + + +import java.util.UUID; + +@Singleton +public class NotificationServiceImpl implements NotificationService { + + private static final Logger LOG = LoggerFactory.getLogger(NotificationServiceImpl.class); + + private final EmailSender emailSender; + private final MemberProfileServices memberProfileServices; + private final CurrentUserServices currentUserServices; + + @Inject + public NotificationServiceImpl( + @Named(MailJetFactory.HTML_FORMAT) EmailSender emailSender, + MemberProfileServices memberProfileServices, + CurrentUserServices currentUserServices + ) { + this.emailSender = emailSender; + this.memberProfileServices = memberProfileServices; + this.currentUserServices = currentUserServices; + } + + @Override + public void sendNotification(@NonNull UUID userId, @NonNull String message) { + MemberProfile user = memberProfileServices.getById(userId); + + if (user == null) { + throw new RuntimeException("User not found with ID: " + userId); + } + + String subject = "Feedback Request Denied"; + + MemberProfile denier = getCurrentDenier(); + String content = "

Feedback Request Denied

" + + "

Dear " + user.getFirstName() + " " + user.getLastName() + ",

" + + "

Your feedback request has been denied. The reason provided was:

" + + "

" + message + "

" + + "

Best Regards,

" + + "

" + denier.getFirstName() + " " + denier.getLastName() + "

"; + + + String fromName = denier.getFirstName() + " " + denier.getLastName(); + String fromAddress = denier.getWorkEmail(); + + emailSender.sendEmail(fromName, fromAddress, subject, content, user.getWorkEmail()); + } + + private MemberProfile getCurrentDenier() { + return currentUserServices.getCurrentUser(); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java index 29e8add4c..9878cc90f 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/permissions/Permission.java @@ -8,12 +8,14 @@ @JsonSerialize(using = PermissionSerializer.class) public enum Permission { CAN_VIEW_FEEDBACK_REQUEST("View feedback requests", "Feedback"), + CAN_DENY_FEEDBACK_REQUEST("Deny feedback requests", "Feedback"), CAN_CREATE_FEEDBACK_REQUEST("Create feedback requests", "Feedback"), CAN_DELETE_FEEDBACK_REQUEST("Delete feedback requests", "Feedback"), CAN_CREATE_KUDOS("Create kudos", "Feedback"), CAN_ADMINISTER_KUDOS("Administer kudos", "Feedback"), CAN_VIEW_FEEDBACK_ANSWER("View feedback answers", "Feedback"), - CAN_SEND_EMAIL("Send email", "Feedback"), + CAN_SEND_EMAIL("Send email", "Notifications"), + CAN_SEND_NOTIFICATIONS("Send notifications", "Notifications"), CAN_DELETE_ORGANIZATION_MEMBERS("Delete organization members", "User Management"), CAN_CREATE_ORGANIZATION_MEMBERS("Create organization members", "User Management"), CAN_IMPERSONATE_MEMBERS("Impersonate organization members", "Security"), @@ -59,7 +61,9 @@ public enum Permission { CAN_ADMINISTER_VOLUNTEERING_ORGANIZATIONS("Update volunteering organizations", "Volunteering"), CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS("Update volunteering relationships", "Volunteering"), CAN_ADMINISTER_VOLUNTEERING_EVENTS("Update volunteering events", "Volunteering"), - CAN_ADMINISTER_DOCUMENTATION("Administer documentation and role documentation", "Documentation"); + CAN_ADMINISTER_DOCUMENTATION("Administer documentation and role documentation", "Documentation"), + CAN_ADMINISTER_FEEDBACK_REQUESTS("Administer feedback requests", "Feedback"); + private final String description; private final String category; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java index 2710f8b44..862b83065 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/reviews/ReviewPeriodServicesImpl.java @@ -356,7 +356,8 @@ private void createReviewRequest(ReviewPeriod period, LocalDate sendDate = LocalDate.now(); FeedbackRequest request = new FeedbackRequest( creatorId, revieweeId, reviewerId, templateId, sendDate, - dueDate, "sent", null, period.getId()); + dueDate, "sent", null, period.getId(), false, null + ); feedbackRequestServices.save(request); } catch(Exception ex) { LOG.error(ex.toString()); diff --git a/server/src/main/resources/db/common/V119__alter_feedback_requests_table.sql b/server/src/main/resources/db/common/V119__alter_feedback_requests_table.sql new file mode 100644 index 000000000..264f55612 --- /dev/null +++ b/server/src/main/resources/db/common/V119__alter_feedback_requests_table.sql @@ -0,0 +1,10 @@ +-- Migration to add 'denied' column to the feedback_requests table + +BEGIN; + +ALTER TABLE feedback_requests +ADD COLUMN denied BOOLEAN DEFAULT FALSE NOT NULL; + +COMMENT ON COLUMN feedback_requests.denied IS 'Indicates whether the feedback request has been denied.'; + +COMMIT; diff --git a/server/src/main/resources/db/common/V120__alter_feedback_requests_table.sql b/server/src/main/resources/db/common/V120__alter_feedback_requests_table.sql new file mode 100644 index 000000000..85403a4c2 --- /dev/null +++ b/server/src/main/resources/db/common/V120__alter_feedback_requests_table.sql @@ -0,0 +1,4 @@ +ALTER TABLE feedback_requests +ADD COLUMN reason VARCHAR(255); + +COMMENT ON COLUMN feedback_requests.reason IS 'Reason provided when the feedback request is denied.'; \ No newline at end of file diff --git a/server/src/main/resources/db/dev/R__Load_testing_data.sql b/server/src/main/resources/db/dev/R__Load_testing_data.sql index 4693bf5fd..4dc0625b3 100644 --- a/server/src/main/resources/db/dev/R__Load_testing_data.sql +++ b/server/src/main/resources/db/dev/R__Load_testing_data.sql @@ -658,6 +658,11 @@ insert into role_permissions values ('e8a4fff8-e984-4e59-be84-a713c9fa8d23', 'CAN_DELETE_FEEDBACK_REQUEST'); +insert into role_permissions + (roleid, permission) +values + ('e8a4fff8-e984-4e59-be84-a713c9fa8d23', 'CAN_DENY_FEEDBACK_REQUEST'); + insert into role_permissions (roleid, permission) values @@ -893,6 +898,11 @@ insert into role_permissions values ('e8a4fff8-e984-4e59-be84-a713c9fa8d23', 'CAN_SEND_EMAIL'); +insert into role_permissions + (roleid, permission) +values + ('e8a4fff8-e984-4e59-be84-a713c9fa8d23', 'CAN_ADMINISTER_FEEDBACK_REQUESTS'); + -- PDL Permissions insert into role_permissions (roleid, permission) @@ -990,6 +1000,11 @@ insert into role_permissions values ('8bda2ae9-58c1-4843-a0d5-d0952621f9df', 'CAN_DELETE_FEEDBACK_REQUEST'); +insert into role_permissions + (roleid, permission) +values + ('8bda2ae9-58c1-4843-a0d5-d0952621f9df', 'CAN_DENY_FEEDBACK_REQUEST'); + insert into role_permissions (roleid, permission) values diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java index 8b1b1c6cc..f4c7df493 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/FeedbackRequestFixture.java @@ -1,15 +1,15 @@ package com.objectcomputing.checkins.services.fixture; + import com.objectcomputing.checkins.services.feedback_request.FeedbackRequest; import com.objectcomputing.checkins.services.feedback_template.FeedbackTemplate; import com.objectcomputing.checkins.services.memberprofile.MemberProfile; import com.objectcomputing.checkins.services.reviews.ReviewPeriod; import java.time.LocalDate; - -import java.util.UUID; import java.util.List; +import java.util.UUID; -public interface FeedbackRequestFixture extends RepositoryFixture, FeedbackTemplateFixture { +public interface FeedbackRequestFixture extends FeedbackTemplateFixture { /** * Creates a sample feedback request @@ -21,7 +21,19 @@ public interface FeedbackRequestFixture extends RepositoryFixture, FeedbackTempl */ default FeedbackRequest createSampleFeedbackRequest(MemberProfile creator, MemberProfile requestee, MemberProfile recipient, UUID templateId) { LocalDate testDate = LocalDate.of(2010, 10, 8); - return new FeedbackRequest(creator.getId(), requestee.getId(), recipient.getId(), templateId, testDate, null, "pending", null, null); + return new FeedbackRequest( + creator.getId(), + requestee.getId(), + recipient.getId(), + templateId, + testDate, + null, + "pending", + null, + null, + false, + null + ); } /** @@ -35,7 +47,19 @@ default FeedbackRequest createSampleFeedbackRequest(MemberProfile creator, Membe */ default FeedbackRequest createSampleFeedbackRequest(MemberProfile creator, MemberProfile requestee, MemberProfile recipient, UUID templateId, ReviewPeriod reviewPeriod) { LocalDate testDate = LocalDate.of(2010, 10, 8); - return new FeedbackRequest(creator.getId(), requestee.getId(), recipient.getId(), templateId, testDate, null, "pending", null, reviewPeriod.getId()); + return new FeedbackRequest( + creator.getId(), + requestee.getId(), + recipient.getId(), + templateId, + testDate, + null, + "pending", + null, + reviewPeriod.getId(), + false, + null + ); } /** @@ -48,7 +72,20 @@ default FeedbackRequest createSampleFeedbackRequest(MemberProfile creator, Membe */ default FeedbackRequest saveSampleFeedbackRequest(MemberProfile creator, MemberProfile requestee, MemberProfile recipient, UUID templateId) { LocalDate testDate = LocalDate.of(2010, 10, 8); - return getFeedbackRequestRepository().save(new FeedbackRequest(creator.getId(), requestee.getId(), recipient.getId(), templateId, testDate, null, "pending", null, null)); + FeedbackRequest feedbackRequest = new FeedbackRequest( + creator.getId(), + requestee.getId(), + recipient.getId(), + templateId, + testDate, + null, + "pending", + null, + null, + false, + null + ); + return getFeedbackRequestRepository().save(feedbackRequest); } /** @@ -57,16 +94,43 @@ default FeedbackRequest saveSampleFeedbackRequest(MemberProfile creator, MemberP * @param recipient The {@link MemberProfile} of the member giving feedback * @param requestee The {@link MemberProfile} of the requestee of the feedback request * @param templateId The UUID of the FeedbackTemplate + * @param reviewPeriod the {@link ReviewPeriod} that this feedback request is associated with * @return The saved {@link FeedbackRequest} */ default FeedbackRequest saveSampleFeedbackRequest(MemberProfile creator, MemberProfile requestee, MemberProfile recipient, UUID templateId, ReviewPeriod reviewPeriod) { LocalDate testDate = LocalDate.of(2010, 10, 8); - return getFeedbackRequestRepository().save(new FeedbackRequest(creator.getId(), requestee.getId(), recipient.getId(), templateId, testDate, null, "pending", null, reviewPeriod.getId())); + FeedbackRequest feedbackRequest = new FeedbackRequest( + creator.getId(), + requestee.getId(), + recipient.getId(), + templateId, + testDate, + null, + "pending", + null, + reviewPeriod.getId(), + false, + null + ); + return getFeedbackRequestRepository().save(feedbackRequest); } default FeedbackRequest saveSampleFeedbackRequestWithStatus(MemberProfile creator, MemberProfile requestee, MemberProfile recipient, UUID templateId, String status) { LocalDate testDate = LocalDate.of(2010, 10, 8); - return getFeedbackRequestRepository().save(new FeedbackRequest(creator.getId(), requestee.getId(), recipient.getId(), templateId, testDate, null, status, null, null)); + FeedbackRequest feedbackRequest = new FeedbackRequest( + creator.getId(), + requestee.getId(), + recipient.getId(), + templateId, + testDate, + null, + status, + null, + null, + false, + null + ); + return getFeedbackRequestRepository().save(feedbackRequest); } default MemberProfile createADefaultRecipient() { @@ -115,6 +179,6 @@ default FeedbackRequest saveFeedbackRequest(MemberProfile creator, MemberProfile default List getFeedbackRequests(MemberProfile recipient) { return getFeedbackRequestRepository() - .findByValues(null, null, recipient.getId().toString(), null, null, null); + .findByValues(null, null, recipient.getId().toString(), null, null, null); } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java index cae3a2720..ae5ec6651 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/fixture/PermissionFixture.java @@ -94,6 +94,7 @@ public interface PermissionFixture extends RolePermissionFixture { Permission.CAN_ADMINISTER_VOLUNTEERING_RELATIONSHIPS, Permission.CAN_ADMINISTER_VOLUNTEERING_EVENTS, Permission.CAN_ADMINISTER_DOCUMENTATION, + Permission.CAN_ADMINISTER_FEEDBACK_REQUESTS, Permission.CAN_ADMINISTER_KUDOS, Permission.CAN_CREATE_KUDOS, Permission.CAN_IMPERSONATE_MEMBERS, diff --git a/web-ui/src/api/feedback.js b/web-ui/src/api/feedback.js index 856618502..4cf7bc81c 100644 --- a/web-ui/src/api/feedback.js +++ b/web-ui/src/api/feedback.js @@ -1,3 +1,4 @@ +import { createDateStrForV6InputFromSections } from '@mui/x-date-pickers/internals'; import { resolve } from './api.js'; import { getFeedbackTemplateWithQuestions } from './feedbacktemplate.js'; @@ -118,6 +119,47 @@ export const cancelFeedbackRequest = async (feedbackRequest, cookie) => { }); }; +export const denyFeedbackRequest = async (requestId, reason, denier, creator, cookie) => { + console.log('Sending deny feedback request with data:', { + reason, + denier, + creator + }); + return resolve({ + method: 'POST', + url: `${feedbackRequestURL}/${requestId}/deny`, + data: { + reason: reason, + denier: denier, + creator: { + id: creator.id, + name: "Anonymous" + } + }, + headers: { + 'X-CSRF-Header': cookie, + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + } + }); +}; + +export const sendNotification = async (userId, message, cookie) => { + return resolve({ + method: 'POST', + url: '/services/notifications/send', + data: { + userId: userId, + message: message + }, + headers: { + 'X-CSRF-Header': cookie, + Accept: 'applications/json', + 'Content-Type': 'application/json;charset=UTF-8' + } + }); +}; + export const deleteFeedbackRequestById = async (id, cookie) => { return resolve({ method: 'DELETE', @@ -126,6 +168,8 @@ export const deleteFeedbackRequestById = async (id, cookie) => { }); }; + + export const getFeedbackRequestById = async (id, cookie) => { return resolve({ url: `${feedbackRequestURL}/${id}`, diff --git a/web-ui/src/components/feedback_request_card/FeedbackRequestCard.css b/web-ui/src/components/feedback_request_card/FeedbackRequestCard.css index 549a98343..76b4cff65 100644 --- a/web-ui/src/components/feedback_request_card/FeedbackRequestCard.css +++ b/web-ui/src/components/feedback_request_card/FeedbackRequestCard.css @@ -12,6 +12,12 @@ } } +.denied-request { + opacity: 0.6; + pointer-events: none; + background-color: var(--checkins-palette-background-disabled); +} + .has-padding-top { padding-top: 1em; } @@ -63,6 +69,16 @@ width: 100%; } +.deny-feedback-button { + background-color: var(--checkins-palette-error); + color: white; + margin-top: 1em; + + &:hover { + background-color: darken(var(--checkins-palette-error), 10%); + } +} + .MuiFormControl-root { width: 20%; margin-right: 2em; @@ -80,4 +96,16 @@ .response-link { font-size: 0.7rem; } + + .deny-feedback-button { + font-size: 0.7rem; + padding: 6px 8px; + } } + +.denied-label { + color: var(--checkins-palette-error); + font-weight: bold; + margin-top: 10px; + display: block; +} \ No newline at end of file diff --git a/web-ui/src/components/feedback_request_card/FeedbackRequestCard.jsx b/web-ui/src/components/feedback_request_card/FeedbackRequestCard.jsx index ae439d4ef..0721c3584 100644 --- a/web-ui/src/components/feedback_request_card/FeedbackRequestCard.jsx +++ b/web-ui/src/components/feedback_request_card/FeedbackRequestCard.jsx @@ -91,7 +91,9 @@ const FeedbackRequestCard = ({ templateName, responses, sortType, - dateRange + dateRange, + onDeny, + isDenied }) => { const { state } = useContext(AppContext); const requesteeProfile = selectProfile(state, requesteeId); @@ -215,7 +217,7 @@ const FeedbackRequestCard = ({ }, [state, sortType, dateRange, responses, withinDateRange]); return ( -
+
diff --git a/web-ui/src/components/feedback_request_card/__snapshots__/FeedbackRequestCard.test.jsx.snap b/web-ui/src/components/feedback_request_card/__snapshots__/FeedbackRequestCard.test.jsx.snap index 75039b517..1be3c5e7f 100644 --- a/web-ui/src/components/feedback_request_card/__snapshots__/FeedbackRequestCard.test.jsx.snap +++ b/web-ui/src/components/feedback_request_card/__snapshots__/FeedbackRequestCard.test.jsx.snap @@ -3,7 +3,7 @@ exports[`renders correctly 1`] = `