diff --git a/src/apps/support/README.md b/src/apps/support/README.md index eaad0a17d..ead5236ab 100644 --- a/src/apps/support/README.md +++ b/src/apps/support/README.md @@ -3,7 +3,8 @@ The Support subapp lets authenticated Topcoder members open requests, follow their status, and reply to the Topcoder Support Team. Users with the exact `Topcoder Support Team` role can see all tickets, assign themselves, search -closed tickets, reply to tickets assigned to them, and close resolved requests. +closed tickets, and reply to or close resolved requests assigned to them. +Closed ticket details identify the Support Team user who closed the request. ## Routes diff --git a/src/apps/support/src/lib/models/support.models.ts b/src/apps/support/src/lib/models/support.models.ts index 5c346295f..f0c94d64f 100644 --- a/src/apps/support/src/lib/models/support.models.ts +++ b/src/apps/support/src/lib/models/support.models.ts @@ -33,6 +33,7 @@ export interface SupportTicketSummary { status: SupportTicketStatus openedAt: string closedAt?: string + closedByUserId?: string updatedAt: string latestActivityAt: string responseCount: number diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx index 66b321d20..9e69ac6a5 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx @@ -196,6 +196,49 @@ describe('TicketDetailPage reply access', () => { .toBeTruthy() }) + it('identifies the support staff member who closed the ticket', () => { + mockUseSWR.mockReturnValue({ + data: { + ...closedTicket, + assignees: [{ + assignedAt: '2026-08-07T00:30:00.000Z', + handle: 'support-agent', + userId: '99999', + }], + closedByUserId: '99999', + }, + error: undefined, + isValidating: false, + mutate: mockMutate, + }) + + render() + + expect(screen.getByText((_content, element) => ( + element?.tagName === 'P' + && element.textContent?.includes('Closed') === true + && element.textContent?.includes('by support-agent') === true + ))) + .toBeTruthy() + }) + + it('falls back to the stored closer user ID when no assignee snapshot matches', () => { + mockUseSWR.mockReturnValue({ + data: { + ...closedTicket, + closedByUserId: 'legacy-staff-1', + }, + error: undefined, + isValidating: false, + mutate: mockMutate, + }) + + render() + + expect(screen.getByText('legacy-staff-1')) + .toBeTruthy() + }) + it('preserves freshly revalidated assignees when marking a ticket read completes', async () => { const markReadRequest = createDeferred() mockedMarkRead.mockReturnValue(markReadRequest.promise) @@ -242,7 +285,7 @@ describe('TicketDetailPage reply access', () => { .toBeUndefined() }) - it('requires non-owner support staff to assign an open ticket before replying', () => { + it('requires non-owner support staff to assign an open ticket before replying or closing it', () => { mockProfile = { roles: ['Topcoder Support Team'], userId: 99999, @@ -264,9 +307,13 @@ describe('TicketDetailPage reply access', () => { .toBeNull() expect(screen.getByText('Assign this ticket to yourself before replying.')) .toBeTruthy() + expect((screen.getByRole('button', { + name: 'Close support ticket', + }) as HTMLButtonElement).disabled) + .toBe(true) }) - it('lets assigned support staff reply to an open ticket', () => { + it('lets assigned support staff reply to and close an open ticket', () => { mockProfile = { roles: ['Topcoder Support Team'], userId: 99999, @@ -293,5 +340,9 @@ describe('TicketDetailPage reply access', () => { .toBeTruthy() expect(screen.queryByText('Assign this ticket to yourself before replying.')) .toBeNull() + expect((screen.getByRole('button', { + name: 'Close support ticket', + }) as HTMLButtonElement).disabled) + .toBe(false) }) }) diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx b/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx index 4df066db3..8ac4bfeb5 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx @@ -106,6 +106,9 @@ export const TicketDetailPage: FC = () => { assignee => String(assignee.userId) === currentUserId, )) const closed = data.status === 'CLOSED' + const closedByAssignee = data.closedByUserId + ? data.assignees.find(assignee => String(assignee.userId) === String(data.closedByUserId)) + : undefined const ticketOwner = Boolean(currentUserId && String(data.memberUserId) === currentUserId) const canReply = ticketOwner || (!closed && (!supportTeam || assignedToCurrentUser)) const replyContext = `${data.id}-reply-${replyRevision}` @@ -234,6 +237,21 @@ export const TicketDetailPage: FC = () => { Closed {' '} {formatSupportDate(data.closedAt)} + {data.closedByUserId && ( + <> + {' '} + by + {' '} + + {closedByAssignee ? ( + + ) : data.closedByUserId} + + + )}

)} @@ -247,6 +265,7 @@ export const TicketDetailPage: FC = () => { size='md' />