diff --git a/README.md b/README.md index 436e70d..7129edc 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,20 @@ npm start ## Available Commands - `/ping` - Bot responds with "Pong!" + +## Admin commands + +- `/adminrole` - Set an administrator role to use admin commands +- `/approvechannel` - Set the channel for submissions to be forwarded to for approval +- `/create-event` `/edit-event` - Create/Edit an event +- `/create-team` `/edit-team` - Create/Edit a team +- `/register-team` - Register a team for an event + +## Public commands + +- `/event-info` - Information for an event and teams +- `/team-info` - Information for a team and members + +## Event commands (for participants) + +- `/submit` - Submit an image to the event diff --git a/prisma/migrations/20250415182835_active_event/migration.sql b/prisma/migrations/20250415182835_active_event/migration.sql new file mode 100644 index 0000000..dbd4ac3 --- /dev/null +++ b/prisma/migrations/20250415182835_active_event/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Event" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "startDate" DATETIME NOT NULL, + "endDate" DATETIME, + "status" TEXT NOT NULL DEFAULT 'PLANNED', + "active" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Event" ("createdAt", "description", "endDate", "id", "name", "startDate", "status", "updatedAt") SELECT "createdAt", "description", "endDate", "id", "name", "startDate", "status", "updatedAt" FROM "Event"; +DROP TABLE "Event"; +ALTER TABLE "new_Event" RENAME TO "Event"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6182403..43ff4ca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,6 +33,7 @@ model Event { startDate DateTime endDate DateTime? status String @default("PLANNED") // PLANNED, ONGOING, COMPLETED, CANCELLED + active Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/commands/admin/event/activateevent.js b/src/commands/admin/event/activateevent.js new file mode 100644 index 0000000..027bd5e --- /dev/null +++ b/src/commands/admin/event/activateevent.js @@ -0,0 +1,39 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { EventService } from '#services/eventService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('activate-event') + .setDescription('Set the event to active for the bot context') + .addStringOption((option) => + option + .setName('event') + .setDescription('The event to activate') + .setRequired(true) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const eventId = interaction.options.getString('event'); + try { + const event = await EventService.activateEvent(eventId); + await interaction.reply(`Event ${event.name} has been activated.`); + } catch (error) { + logger.error('Error activating event', error); + await interaction.reply('An error occurred while activating the event.'); + } +} + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function autocomplete(interaction) { + const focusedValue = interaction.options.getFocused(); + const events = await EventService.getEvents(); + const filtered = events.filter((event) => event.name.startsWith(focusedValue)); + const choices = filtered.map((event) => event).slice(0, 25); + await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id }))); +} diff --git a/src/commands/admin/team/registerteam.js b/src/commands/admin/team/registerteam.js new file mode 100644 index 0000000..1ded16d --- /dev/null +++ b/src/commands/admin/team/registerteam.js @@ -0,0 +1,52 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { EventService } from '#services/eventService.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('register-team') + .setDescription('Register team to the event') + .addStringOption((option) => + option + .setName('team') + .setDescription('Select the team to register') + .setRequired(true) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const teamId = interaction.options.getString('team'); + // Register the team to the event + const event = await EventService.getActiveEvent(); + if (!event) { + await interaction.reply('No active event found'); + return; + } + const team = await TeamService.getTeamById(teamId); + if (!team) { + await interaction.reply('Team not found'); + return; + } + try { + await EventService.registerTeamToEvent(event.id, teamId); + await interaction.reply(`Team ${team.name} registered to event ${event.name}`); + logger.info(`Team ${teamId} registered to event ${event.id}`); + } catch (error) { + logger.error('Error registering team to event', error); + await interaction.reply('An error occurred while registering the team to the event.'); + } +} + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function autocomplete(interaction) { + const focusedValue = interaction.options.getFocused(); + const teams = await TeamService.getTeams(); + const filtered = teams.filter((team) => team.name.startsWith(focusedValue)); + const choices = filtered.map((event) => event).slice(0, 25); + await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id }))); +} diff --git a/src/commands/event/eventinfo.js b/src/commands/event/eventinfo.js new file mode 100644 index 0000000..9e82062 --- /dev/null +++ b/src/commands/event/eventinfo.js @@ -0,0 +1,55 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { EventService } from '#services/eventService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('event-info') + .setDescription('Get Event information') + .addStringOption((option) => + option + .setName('event') + .setDescription('Select the event to get information about') + .setRequired(false) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const eventId = interaction.options.getString('event'); + const event = eventId + ? await EventService.getEventById(eventId) + : await EventService.getActiveEvent(); + if (!event) { + await interaction.reply('Event not found'); + return; + } + const embed = { + title: event.name, + description: event.description, + fields: [ + { name: 'Start Date', value: event.startDate.toString(), inline: true }, + { name: 'End Date', value: event.endDate.toString(), inline: true }, + { name: 'Status', value: event.status, inline: true }, + { + name: 'Teams', + value: event.teams.map((team) => team.name).join(', ') || 'None', + inline: false, + }, + ], + }; + await interaction.reply({ embeds: [embed] }); + logger.info(`Event info requested: ${event.name}`); +} + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function autocomplete(interaction) { + const focusedValue = interaction.options.getFocused(); + const events = await EventService.getEvents(); + const filtered = events.filter((event) => event.name.startsWith(focusedValue)); + const choices = filtered.map((event) => event).slice(0, 25); + await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id }))); +} diff --git a/src/commands/event/teaminfo.js b/src/commands/event/teaminfo.js new file mode 100644 index 0000000..de47891 --- /dev/null +++ b/src/commands/event/teaminfo.js @@ -0,0 +1,44 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('team-info') + .setDescription('Get Team information') + .addStringOption((option) => + option + .setName('team') + .setDescription('Select the team to get information about') + .setRequired(true) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const teamId = interaction.options.getString('team'); + const team = await TeamService.getTeamById(teamId); + if (!team) { + await interaction.reply('Team not found'); + return; + } + const embed = { + title: team.name, + description: team.description, + fields: [{ name: 'Leader', value: `<@${team.leader.discordId}>`, inline: true }], + }; + await interaction.reply({ embeds: [embed] }); + logger.info(`Team info requested: ${team.name}`); +} + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function autocomplete(interaction) { + const focusedValue = interaction.options.getFocused(); + const events = await TeamService.getTeams(); + const filtered = events.filter((event) => event.name.startsWith(focusedValue)); + const choices = filtered.map((event) => event).slice(0, 25); + await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id }))); +} diff --git a/src/services/eventService.js b/src/services/eventService.js index a7a3d66..da23718 100644 --- a/src/services/eventService.js +++ b/src/services/eventService.js @@ -68,6 +68,31 @@ export class EventService { } } + /** + * Get an event by ID + * @param {String} eventId - The ID of the event + * @return {Promise} The event object + */ + static async getEventById(eventId) { + try { + const event = await prisma.event.findUnique({ + where: { + id: eventId, + }, + include: { + teams: true, + }, + }); + if (!event) { + throw new Error(`Event with ID ${eventId} not found`); + } + return event; + } catch (error) { + logger.error(`Error getting event with ID ${eventId}:`, error); + throw error; + } + } + /** * Update an event by ID * @param {String} eventId - The ID of the event @@ -95,4 +120,76 @@ export class EventService { throw error; } } + + /** + * Activate event + */ + static async activateEvent(eventId) { + try { + // Deactivate all other events + await prisma.event.updateMany({ + where: { + active: true, + }, + data: { + active: false, + }, + }); + return await prisma.event.update({ + where: { + id: eventId, + }, + data: { + active: true, + }, + }); + } catch (error) { + logger.error(`Error activating event with ID ${eventId}:`, error); + throw error; + } + } + + /** + * Regsiter team to event + */ + static async registerTeamToEvent(eventId, teamId) { + try { + await prisma.event.update({ + where: { + id: eventId, + }, + data: { + teams: { + connect: { + id: teamId, + }, + }, + }, + }); + } catch (error) { + console.log(error); + logger.error(`Error registering team ${teamId} to event ${eventId}:`, error); + throw error; + } + } + + /** + * Get active event + */ + static async getActiveEvent() { + try { + const event = await prisma.event.findFirst({ + where: { + active: true, + }, + include: { + teams: true, + }, + }); + return event; + } catch (error) { + logger.error('Error getting active event', error); + throw error; + } + } } diff --git a/src/services/teamService.js b/src/services/teamService.js index 0a36f8b..be21ff2 100644 --- a/src/services/teamService.js +++ b/src/services/teamService.js @@ -72,7 +72,7 @@ export class TeamService { /** * Get a team by ID * @param {String} teamId - The ID of the team - * @returns {Promise} The team object + * @returns {Promise} The team object */ static async getTeamById(teamId) { try { @@ -80,6 +80,10 @@ export class TeamService { where: { id: teamId, }, + include: { + leader: true, + members: true, + }, }); if (!team) { throw new Error('Team not found'); @@ -90,6 +94,7 @@ export class TeamService { throw error; } } + /** * Update a team by ID * @param {String} teamId - The ID of the team