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'
/>