[#36] Add rating for resolved ticket#6
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a customer satisfaction rating workflow for resolved tickets, spanning DB schema, API responses, UI display/input, and an automated closure policy for unrated resolved tickets.
Changes:
- Add
rating,ratingComment, andresolvedAtto theTicketmodel and expose them in ticket detail/workbench APIs. - Add a
/tickets/{id}/ratingendpoint and a scheduled job to auto-close unrated resolved tickets after a configurable number of days. - Add a reusable
RatingsUI component and surface rating display/submission in ticket detail/workbench pages; update the tickets manual.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/frontend/src/types/domain/tickets.ts | Extends ticket domain types with rating and ratingComment. |
| src/frontend/src/pages/TicketWorkbenchFormPage.tsx | Displays rating (read-only) on the workbench edit form when present. |
| src/frontend/src/pages/SupportTicketDetailPage.tsx | Adds rating submission UI for resolved tickets and read-only display when already rated. |
| src/frontend/src/components/ui/ratings.tsx | New star-rating UI component used by pages. |
| src/backend/main/resources/application.properties | Adds config for auto-close window (ticket.rating.auto-close-days). |
| src/backend/main/java/ai/mnemosyne_systems/service/TicketAutoCloseService.java | New scheduled service to auto-close unrated resolved tickets. |
| src/backend/main/java/ai/mnemosyne_systems/resource/UserTicketApiResource.java | Includes rating fields in user ticket detail response payload. |
| src/backend/main/java/ai/mnemosyne_systems/resource/TicketWorkbenchApiResource.java | Includes rating fields in workbench bootstrap/form payload. |
| src/backend/main/java/ai/mnemosyne_systems/resource/TicketResource.java | Tracks resolvedAt when status transitions to/from Resolved. |
| src/backend/main/java/ai/mnemosyne_systems/resource/TicketRatingApiResource.java | New endpoint for submitting ticket ratings. |
| src/backend/main/java/ai/mnemosyne_systems/resource/SupportTicketApiResource.java | Includes rating fields in support ticket detail response payload. |
| src/backend/main/java/ai/mnemosyne_systems/resource/SuperuserTicketApiResource.java | Includes rating fields in superuser ticket detail response payload. |
| src/backend/main/java/ai/mnemosyne_systems/model/Ticket.java | Adds persisted fields: rating, rating comment, and resolved timestamp. |
| doc/manual/en/11-tickets.md | Documents rating flow and auto-close behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+44
to
+58
| User user = AuthHelper.findUser(auth); | ||
| if (user == null || (!User.TYPE_USER.equalsIgnoreCase(user.type) && !AuthHelper.isSupport(user) | ||
| && !AuthHelper.isSuperuser(user) && !AuthHelper.isTam(user))) { | ||
| throw new WebApplicationException(Response.seeOther(URI.create("/login")).build()); | ||
| } | ||
|
|
||
| Ticket ticket = Ticket.findById(id); | ||
| if (ticket == null) { | ||
| throw new NotFoundException(); | ||
| } | ||
|
|
||
| // Only the requester or someone from the same company can logically rate, but we enforce typical ticket access. | ||
| if (!MessageVisibilitySupport.canAccessTicket(user, ticket)) { | ||
| throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); | ||
| } |
Comment on lines
+69
to
+71
| ticket.rating = rating; | ||
| ticket.ratingComment = ratingComment; | ||
|
|
| for (Ticket ticket : ticketsToClose) { | ||
| LOGGER.infof("Auto-closing ticket %d (%s) as it has been resolved for %d days with no rating.", ticket.id, | ||
| ticket.name, autoCloseDays); | ||
| String previousStatus = ticket.status; |
Comment on lines
+141
to
+148
| <div | ||
| style={{ | ||
| position: "absolute", | ||
| top: 0, | ||
| overflow: "hidden", | ||
| width: `${fillPercentage * 100}%`, | ||
| }} | ||
| > |
Comment on lines
+62
to
+66
| const rating = value || 0; | ||
| const fullStars = Math.floor(rating); | ||
| const hasPartial = rating % 1 > 0; | ||
| const emptyCount = Math.max(0, totalStars - fullStars - (hasPartial ? 1 : 0)); | ||
|
|
Comment on lines
+37
to
+43
| @POST | ||
| @Path("/{id}/rating") | ||
| @Transactional | ||
| public Response submitRating(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, | ||
| @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, | ||
| @FormParam("rating") Integer rating, @FormParam("ratingComment") String ratingComment) { | ||
|
|
Comment on lines
+231
to
+233
| if (!sameStatus(previousStatus, "Resolved") && sameStatus(status, "Resolved")) { | ||
| ticket.resolvedAt = java.time.LocalDateTime.now(); | ||
| } else if (!sameStatus(status, "Resolved") && !sameStatus(status, "Closed")) { |
Comment on lines
+408
to
+410
| if (!sameStatus(previousStatus, "Resolved") && sameStatus(status, "Resolved")) { | ||
| ticket.resolvedAt = java.time.LocalDateTime.now(); | ||
| } else if (!sameStatus(status, "Resolved") && !sameStatus(status, "Closed")) { |
Comment on lines
+39
to
+79
| @POST | ||
| @Path("/{id}/rating") | ||
| @Transactional | ||
| public Response submitRating(@CookieParam(AuthHelper.AUTH_COOKIE) String auth, | ||
| @HeaderParam("X-Billetsys-Client") String client, @PathParam("id") Long id, | ||
| @FormParam("rating") Integer rating, @FormParam("ratingComment") String ratingComment) { | ||
|
|
||
| User user = AuthHelper.findUser(auth); | ||
| if (user == null || (!User.TYPE_USER.equalsIgnoreCase(user.type) && !AuthHelper.isSuperuser(user))) { | ||
| throw new WebApplicationException(Response.seeOther(URI.create("/login")).build()); | ||
| } | ||
|
|
||
| Ticket ticket = Ticket.findById(id); | ||
| if (ticket == null) { | ||
| throw new NotFoundException(); | ||
| } | ||
|
|
||
| if (!MessageVisibilitySupport.canAccessTicket(user, ticket)) { | ||
| throw new WebApplicationException(Response.seeOther(URI.create("/")).build()); | ||
| } | ||
|
|
||
| if (!"Resolved".equalsIgnoreCase(ticket.status)) { | ||
| throw new BadRequestException("Ticket must be resolved to be rated."); | ||
| } | ||
|
|
||
| if (ticket.rating != null) { | ||
| throw new BadRequestException("This ticket has already been rated."); | ||
| } | ||
|
|
||
| if (rating == null || rating < 1 || rating > 10) { | ||
| throw new BadRequestException("Rating must be between 1 and 10."); | ||
| } | ||
|
|
||
| ticket.rating = rating; | ||
| ticket.ratingComment = normalizeComment(ratingComment); | ||
|
|
||
| if ("react".equalsIgnoreCase(client)) { | ||
| return Response.ok(java.util.Map.of("redirectTo", "")).type(MediaType.APPLICATION_JSON).build(); | ||
| } | ||
| return ReactRedirectSupport.redirect(client, "/tickets/" + ticket.id); | ||
| } |
Comment on lines
+942
to
+964
| <div className="space-y-2"> | ||
| <label className="text-sm font-semibold"> | ||
| How would you rate the resolution of this ticket? | ||
| </label> | ||
| <Ratings | ||
| value={ratingValue} | ||
| onValueChange={setRatingValue} | ||
| asInput | ||
| variant="yellow" | ||
| /> | ||
| </div> | ||
| <div className="space-y-2 pt-2"> | ||
| <label className="text-sm font-semibold"> | ||
| Optional comment | ||
| </label> | ||
| <Textarea | ||
| value={ratingComment} | ||
| onChange={(e) => setRatingComment(e.target.value)} | ||
| placeholder="Tell us about your experience..." | ||
| maxLength={2000} | ||
| className="min-h-[100px]" | ||
| /> | ||
| </div> |
|
|
||
| * **Users** see the rating on their own tickets. | ||
| * **Superusers** see ratings on all tickets within their company. | ||
| * **TAMs** see ratings on tickets for the companies they are assigned to. TAMs can only see their own company's tickets. |
ba71158 to
d1995f8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.