Enhance ticket management with dashboard actions and priority settings
- Introduced a new `runTicketDashboardActionJob` to handle ticket actions (close, claim, set priority) with Discord side effects. - Added a priority dropdown in the ticket control panel, allowing users to set ticket urgency levels. - Updated WebUI to display open tickets with options to claim, close, or adjust priority, enhancing user interaction. - Improved localization for new ticket management features in both English and German. - Documented changes in phase tracking to reflect the new ticket dashboard functionalities.
This commit is contained in:
@@ -3,9 +3,13 @@ import type {
|
||||
TicketCategoryDashboardCreate,
|
||||
TicketCategoryDashboardUpdate,
|
||||
TicketConfigDashboard,
|
||||
TicketConfigDashboardPatch
|
||||
TicketConfigDashboardPatch,
|
||||
TicketDashboard,
|
||||
TicketDashboardAction,
|
||||
TicketPriority,
|
||||
TicketStatus
|
||||
} from '@nexumi/shared';
|
||||
import type { TicketCategory } from '@prisma/client';
|
||||
import type { Ticket, TicketCategory } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, getTicketsQueue, getTicketsQueueEvents } from '../queues';
|
||||
|
||||
@@ -19,6 +23,13 @@ export class TicketPanelSyncError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class TicketActionSyncError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'TicketActionSyncError';
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTicketConfig(guildId: string) {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
return prisma.ticketConfig.upsert({ where: { guildId }, update: {}, create: { guildId } });
|
||||
@@ -214,3 +225,104 @@ export async function deleteTicketCategory(guildId: string, categoryId: string):
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
type TicketWithCategory = Ticket & { category: TicketCategory | null };
|
||||
|
||||
function toTicketDashboard(ticket: TicketWithCategory): TicketDashboard {
|
||||
return {
|
||||
id: ticket.id,
|
||||
openerId: ticket.openerId,
|
||||
claimedById: ticket.claimedById,
|
||||
channelId: ticket.channelId,
|
||||
threadId: ticket.threadId,
|
||||
status: ticket.status as TicketStatus,
|
||||
priority: ticket.priority as TicketPriority,
|
||||
subject: ticket.subject,
|
||||
categoryId: ticket.categoryId,
|
||||
categoryName: ticket.category?.name ?? null,
|
||||
lastActivityAt: ticket.lastActivityAt.toISOString(),
|
||||
createdAt: ticket.createdAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function listOpenTickets(guildId: string): Promise<TicketDashboard[]> {
|
||||
const tickets = await prisma.ticket.findMany({
|
||||
where: {
|
||||
guildId,
|
||||
status: { in: ['OPEN', 'CLAIMED'] }
|
||||
},
|
||||
include: { category: true },
|
||||
orderBy: { lastActivityAt: 'desc' },
|
||||
take: 100
|
||||
});
|
||||
return tickets.map(toTicketDashboard);
|
||||
}
|
||||
|
||||
export async function getTicketDashboard(
|
||||
guildId: string,
|
||||
ticketId: string
|
||||
): Promise<TicketDashboard | null> {
|
||||
const ticket = await prisma.ticket.findFirst({
|
||||
where: { id: ticketId, guildId },
|
||||
include: { category: true }
|
||||
});
|
||||
return ticket ? toTicketDashboard(ticket) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close / claim / priority need Discord side effects → bot via BullMQ.
|
||||
*/
|
||||
export async function applyTicketDashboardAction(
|
||||
guildId: string,
|
||||
ticketId: string,
|
||||
action: TicketDashboardAction,
|
||||
actorUserId: string
|
||||
): Promise<TicketDashboard | null> {
|
||||
const existing = await prisma.ticket.findFirst({
|
||||
where: { id: ticketId, guildId },
|
||||
include: { category: true }
|
||||
});
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
if (existing.status === 'CLOSED') {
|
||||
throw new TicketActionSyncError('This ticket is already closed');
|
||||
}
|
||||
|
||||
const { confirmed } = await addJobAndAwait(
|
||||
getTicketsQueue(),
|
||||
getTicketsQueueEvents(),
|
||||
'ticketDashboardAction',
|
||||
{
|
||||
ticketId,
|
||||
guildId,
|
||||
actorUserId,
|
||||
action: action.action,
|
||||
reason: action.action === 'close' ? action.reason : undefined,
|
||||
priority: action.action === 'priority' ? action.priority : undefined
|
||||
},
|
||||
{
|
||||
jobId: `ticket-action-${ticketId}-${action.action}-${Date.now()}`,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 25
|
||||
},
|
||||
45_000
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
throw new TicketActionSyncError(
|
||||
'The bot did not confirm the ticket action in time. Reload the list shortly to check the result.'
|
||||
);
|
||||
}
|
||||
|
||||
if (action.action === 'close') {
|
||||
const closed = await getTicketDashboard(guildId, ticketId);
|
||||
return closed;
|
||||
}
|
||||
|
||||
const updated = await getTicketDashboard(guildId, ticketId);
|
||||
if (!updated) {
|
||||
throw new TicketActionSyncError('Ticket action finished but the ticket could not be reloaded');
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user