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:
@@ -43,6 +43,7 @@ export const TICKET_CTRL_CLAIM = 'ticket:ctrl:claim';
|
||||
export const TICKET_CTRL_CLOSE = 'ticket:ctrl:close';
|
||||
export const TICKET_CTRL_ADD = 'ticket:ctrl:add';
|
||||
export const TICKET_CTRL_REMOVE = 'ticket:ctrl:remove';
|
||||
export const TICKET_CTRL_PRIORITY = 'ticket:ctrl:priority';
|
||||
|
||||
/** Cap private-thread staff adds to stay within Discord rate limits. */
|
||||
const MAX_THREAD_SUPPORT_MEMBERS = 50;
|
||||
@@ -160,13 +161,16 @@ export function isTicketControlInteraction(customId: string): boolean {
|
||||
customId === TICKET_CTRL_CLAIM ||
|
||||
customId === TICKET_CTRL_CLOSE ||
|
||||
customId === TICKET_CTRL_ADD ||
|
||||
customId === TICKET_CTRL_REMOVE
|
||||
customId === TICKET_CTRL_REMOVE ||
|
||||
customId === TICKET_CTRL_PRIORITY
|
||||
);
|
||||
}
|
||||
|
||||
export function buildTicketControlPanel(locale: 'de' | 'en'): {
|
||||
embeds: EmbedBuilder[];
|
||||
components: ActionRowBuilder<ButtonBuilder | UserSelectMenuBuilder>[];
|
||||
components: ActionRowBuilder<
|
||||
ButtonBuilder | UserSelectMenuBuilder | StringSelectMenuBuilder
|
||||
>[];
|
||||
} {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(t(locale, 'ticket.control.title'))
|
||||
@@ -184,6 +188,34 @@ export function buildTicketControlPanel(locale: 'de' | 'en'): {
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
);
|
||||
|
||||
const priorityRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId(TICKET_CTRL_PRIORITY)
|
||||
.setPlaceholder(t(locale, 'ticket.control.priorityPlaceholder'))
|
||||
.addOptions(
|
||||
{
|
||||
label: t(locale, 'ticket.priority.LOW'),
|
||||
value: 'LOW',
|
||||
description: t(locale, 'ticket.control.priorityLowHint')
|
||||
},
|
||||
{
|
||||
label: t(locale, 'ticket.priority.NORMAL'),
|
||||
value: 'NORMAL',
|
||||
description: t(locale, 'ticket.control.priorityNormalHint')
|
||||
},
|
||||
{
|
||||
label: t(locale, 'ticket.priority.HIGH'),
|
||||
value: 'HIGH',
|
||||
description: t(locale, 'ticket.control.priorityHighHint')
|
||||
},
|
||||
{
|
||||
label: t(locale, 'ticket.priority.URGENT'),
|
||||
value: 'URGENT',
|
||||
description: t(locale, 'ticket.control.priorityUrgentHint')
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const addRow = new ActionRowBuilder<UserSelectMenuBuilder>().addComponents(
|
||||
new UserSelectMenuBuilder()
|
||||
.setCustomId(TICKET_CTRL_ADD)
|
||||
@@ -202,7 +234,7 @@ export function buildTicketControlPanel(locale: 'de' | 'en'): {
|
||||
|
||||
return {
|
||||
embeds: [embed],
|
||||
components: [buttons, addRow, removeRow]
|
||||
components: [buttons, priorityRow, addRow, removeRow]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -467,6 +499,94 @@ export async function runTicketPanelSyncJob(
|
||||
return syncTicketPanel(context, data.guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard ticket actions (close / claim / priority). Staff checks are done by
|
||||
* the WebUI (`requireGuildAccess`); the bot applies Discord side effects.
|
||||
*/
|
||||
export async function runTicketDashboardActionJob(
|
||||
context: BotContext,
|
||||
data: {
|
||||
ticketId: string;
|
||||
guildId: string;
|
||||
actorUserId: string;
|
||||
action: 'close' | 'claim' | 'priority';
|
||||
reason?: string;
|
||||
priority?: string;
|
||||
}
|
||||
): Promise<{ id: string; status: string; priority: string; claimedById: string | null }> {
|
||||
const ticket = await context.prisma.ticket.findFirst({
|
||||
where: { id: data.ticketId, guildId: data.guildId },
|
||||
include: { category: true }
|
||||
});
|
||||
if (!ticket) {
|
||||
throw new TicketError('not_found');
|
||||
}
|
||||
if (ticket.status === 'CLOSED') {
|
||||
throw new TicketError('closed');
|
||||
}
|
||||
|
||||
const locale = await getGuildLocale(context.prisma, data.guildId);
|
||||
|
||||
if (data.action === 'close') {
|
||||
await closeTicket(context, ticket, data.actorUserId, locale, data.reason);
|
||||
const closed = await context.prisma.ticket.findUniqueOrThrow({ where: { id: ticket.id } });
|
||||
return {
|
||||
id: closed.id,
|
||||
status: closed.status,
|
||||
priority: closed.priority,
|
||||
claimedById: closed.claimedById
|
||||
};
|
||||
}
|
||||
|
||||
if (data.action === 'claim') {
|
||||
if (ticket.claimedById === data.actorUserId) {
|
||||
throw new TicketError('already_claimed');
|
||||
}
|
||||
const updated = await context.prisma.ticket.update({
|
||||
where: { id: ticket.id },
|
||||
data: {
|
||||
claimedById: data.actorUserId,
|
||||
status: 'CLAIMED',
|
||||
lastActivityAt: new Date()
|
||||
}
|
||||
});
|
||||
const channelId = ticket.channelId ?? ticket.threadId;
|
||||
if (channelId) {
|
||||
const channel = await fetchSendableChannel(context, channelId);
|
||||
if (channel) {
|
||||
await channel.send(tf(locale, 'ticket.claimed', { user: `<@${data.actorUserId}>` }));
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: updated.id,
|
||||
status: updated.status,
|
||||
priority: updated.priority,
|
||||
claimedById: updated.claimedById
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = TicketPrioritySchema.parse(data.priority);
|
||||
const updated = await context.prisma.ticket.update({
|
||||
where: { id: ticket.id },
|
||||
data: { priority: parsed, lastActivityAt: new Date() }
|
||||
});
|
||||
const channelId = ticket.channelId ?? ticket.threadId;
|
||||
if (channelId) {
|
||||
const channel = await fetchSendableChannel(context, channelId);
|
||||
if (channel) {
|
||||
await channel.send(
|
||||
tf(locale, 'ticket.prioritySet', { priority: t(locale, `ticket.priority.${parsed}`) })
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: updated.id,
|
||||
status: updated.status,
|
||||
priority: updated.priority,
|
||||
claimedById: updated.claimedById
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOpenModal(category: TicketCategory, locale: 'de' | 'en'): ModalBuilder {
|
||||
const fields = parseFormFields(category.formFields);
|
||||
const modal = new ModalBuilder()
|
||||
|
||||
Reference in New Issue
Block a user