all repos — stealth-developers @ 1157318bdd18710cdc0c2aeb4851724d06234a39

db,bot: list tickets command
vi did:web:vt3e.cat
Sun, 28 Jun 2026 01:57:03 +0100
commit

1157318bdd18710cdc0c2aeb4851724d06234a39

parent

c389ef7352d200fa469e7fd0c4d7ba3c37143367

M apps/bot/src/feats/tickets/README.mdapps/bot/src/feats/tickets/README.md

@@ -31,4 +31,4 @@ - [ ] other ticket stuff

- [x] permissions - [ ] add participants - [x] log messages - - [ ] private thread too + - [x] private thread too
M apps/bot/src/feats/tickets/buttons.tsapps/bot/src/feats/tickets/buttons.ts

@@ -1,6 +1,13 @@

import { option } from "@purrkit/router"; import { Result } from "@sapphire/framework"; -import { ButtonBuilder, ButtonStyle } from "discord.js"; +import { type TicketWithRelations, getTicketsByUser } from "@stealth-developers/db"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ContainerBuilder, + TextDisplayBuilder, +} from "discord.js"; import { kitten } from "@/client"; import { PublicError, errorMessage, handleError } from "@/lib";

@@ -199,6 +206,72 @@ },

async run() {}, }); +// +export const listTicketsPageButton = kitten.button("b-list-tickets-page", { + options: { + userId: option.number({ required: true }), + page: option.number({ required: true }), + }, + async run(interaction, { userId, page }) { + const tickets = await getTicketsByUser(userId); + if (tickets.length === 0) { + return errorMessage(interaction, "You have no tickets."); + } + + const payload = getTicketsListPayload(userId, page, tickets); + await interaction.update({ + ...payload, + flags: ["IsComponentsV2"], + }); + }, +}); + +export function getTicketsListPayload( + userId: number, + page: number, + tickets: TicketWithRelations[], +) { + const TICKETS_PER_PAGE = 5; + const totalPages = Math.ceil(tickets.length / TICKETS_PER_PAGE); + const currentPage = Math.max(1, Math.min(page, totalPages)); + + const pageTickets = tickets.slice( + (currentPage - 1) * TICKETS_PER_PAGE, + currentPage * TICKETS_PER_PAGE, + ); + + const container = new ContainerBuilder(); + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(`### Your Tickets (Page ${currentPage}/${totalPages})`), + ); + + for (const ticket of pageTickets) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + [ + `* **Ticket #${ticket.localId}** (${ticket.id}) - ${ticket.status}`, + `\t* opened <t:${Math.floor(ticket.openedAt.getTime() / 1000)}>`, + ticket.closedAt ? ` * closed <t:${Math.floor(ticket.closedAt.getTime() / 1000)}>` : "", + ].join("\n"), + ), + ); + } + + const row = new ActionRowBuilder<ButtonBuilder>(); + + if (currentPage > 1) + row.addComponents( + TicketButtonPresets.listTicketsPageButton(userId, currentPage - 1, "Previous"), + ); + if (currentPage < totalPages) + row.addComponents(TicketButtonPresets.listTicketsPageButton(userId, currentPage + 1, "Next")); + if (row.components.length > 0) container.addActionRowComponents(row); + + return { + components: row.components.length > 0 ? [container] : [], + }; +} + export const TicketButtonsMap = { newTicketButton, closeTicketButton,

@@ -210,6 +283,7 @@ viewCommentsButton,

editTicketButton, cleanTicketButton, thankButton, + listTicketsPageButton, } as const; export const TicketButtons = Object.values(TicketButtonsMap); export type TicketButtonKeys = keyof typeof TicketButtonsMap | "transcriptsLink" | "uploadLink";

@@ -291,6 +365,13 @@ .setCustomId(thankButton.id({ ticketId: id }))

.setLabel("Thank your moderator") .setEmoji("💖") .setStyle(ButtonStyle.Success), + + // + listTicketsPageButton: (userId: number, page: number, label: string) => + new ButtonBuilder() + .setCustomId(listTicketsPageButton.id({ userId, page })) + .setLabel(label) + .setStyle(ButtonStyle.Secondary), }; export default TicketButtonPresets;
M apps/bot/src/feats/tickets/commands.tsapps/bot/src/feats/tickets/commands.ts

@@ -1,13 +1,13 @@

import { option } from "@purrkit/router"; import { Result } from "@sapphire/result"; -import { getTicketByGuild, getTicketByUser } from "@stealth-developers/db"; +import { getTicketByGuild, getTicketByUser, getTicketsByUser } from "@stealth-developers/db"; import { ActionRowBuilder, ButtonBuilder, ContainerBuilder, TextDisplayBuilder } from "discord.js"; import { PublicError, errorMessage, getTextChannel, handleError, logger } from "@/lib"; import { useGetTicketData } from "@/middleware"; import * as actions from "./actions"; -import { TicketButtonPresets } from "./buttons"; +import { TicketButtonPresets, getTicketsListPayload } from "./buttons"; import { hasAccessToTicket } from "./lib"; import { manageConfig } from "./lib/config"; import { TicketModalPresets } from "./modals";

@@ -254,6 +254,31 @@ );

ticketCommand.group( "manage", + { + description: "Manage your ticket data", + }, + (group) => { + group.subcommand("list", { + description: "List your tickets", + async run(interaction, _, { actor }) { + if (!actor) return errorMessage(interaction, "You must be logged in to run this command"); + + const tickets = await getTicketsByUser(actor.id); + if (tickets.length === 0) return errorMessage(interaction, "You have no tickets"); + + const payload = getTicketsListPayload(actor.id, 1, tickets); + + return interaction.reply({ + ...payload, + flags: ["Ephemeral", "IsComponentsV2"], + }); + }, + }); + }, +); + +ticketCommand.group( + "settings", { description: "Manage ticket settings", },
M pkgs/db/src/utils/ticket.tspkgs/db/src/utils/ticket.ts

@@ -149,6 +149,15 @@ if (!result) return undefined;

return await getTicketById(ticketId); } +export async function getTicketsByUser(userId: number): Promise<TicketWithRelations[]> { + return await db.query.tickets.findMany({ + where: { + openedBy: userId, + }, + with: ticketWithRelations, + }); +} + // standard ops export async function createTicket(data: { guildId: number;