diff --git a/src/commands/admin/team/addmember.js b/src/commands/admin/team/addmember.js new file mode 100644 index 0000000..d001967 --- /dev/null +++ b/src/commands/admin/team/addmember.js @@ -0,0 +1,73 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { ConfigService } from '#services/configService.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('addmember') + .setDescription('Add a registered member to a team.') + .addStringOption((option) => + option + .setName('team') + .setDescription('The team to add the member to.') + .setRequired(true) + .setAutocomplete(true) + ) + .addUserOption((option) => + option.setName('member').setDescription('The member to add to the team.').setRequired(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + if (!interaction.member.permissions.has('Administrator')) { + const customRole = await ConfigService.getAdminRole(interaction.guildId); + if (customRole) { + const member = await interaction.guild.members.fetch(interaction.user.id); + if (!member.roles.cache.has(customRole)) { + await interaction.reply('You do not have permission to set the approval channel.'); + return; + } + } else { + await interaction.reply('You do not have permission to set the approval channel.'); + return; + } + } + const teamId = interaction.options.getString('team'); + const member = interaction.options.getUser('member'); + + // Check if the user is already in the team + const team = await TeamService.getTeamById(teamId); + const isMember = team.members.some((member) => member.user.discordId === member.Id); + if (isMember) { + await interaction.reply('This user is already a member of the team.'); + return; + } + // Check if the user is already in another team + if (await TeamService.isUserInTeam(member.Id)) { + await interaction.reply('This user is already in another team.'); + return; + } + try { + const team = await TeamService.addMemberToTeam(teamId, member.Id); + await interaction.reply(`Member <@${member.id}> added to team ${team.name} successfully!`); + logger.info( + `Member ${member.username} - ${member.id} added to team ${team.name} ${team.id} successfully!` + ); + } catch (error) { + logger.error('Error adding member to team', error); + await interaction.reply('An error occurred while adding the member to the team.'); + } +} + +/** + * @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/commands/admin/team/registerteam.js b/src/commands/admin/team/registerteam.js new file mode 100644 index 0000000..f5c7b50 --- /dev/null +++ b/src/commands/admin/team/registerteam.js @@ -0,0 +1,67 @@ +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('event') + .setDescription('Select the event to register the team to') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption((option) => + option + .setName('team') + .setDescription('Select the team to register') + .setRequired(true) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const eventId = interaction.options.getString('event'); + const teamId = interaction.options.getString('team'); + // Register the team to the event + const event = await EventService.getEventById(eventId); + if (!event) { + await interaction.reply('Event not found'); + return; + } + const team = await TeamService.getTeamById(teamId); + if (!team) { + await interaction.reply('Team not found'); + return; + } + try { + await EventService.registerTeamToEvent(eventId, teamId); + await interaction.reply(`Team ${team.name} registered to event ${event.name}`); + logger.info(`Team ${teamId} registered to event ${eventId}`); + } 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 focusedOption = interaction.options.getFocused(true); + if (focusedOption.name === 'event') { + const events = await EventService.getEvents(); + const filtered = events.filter((event) => event.name.startsWith(focusedOption.value)); + const choices = filtered.map((event) => event).slice(0, 25); + await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id }))); + } else if (focusedOption.name === 'team') { + const teams = await TeamService.getTeams(); + const filtered = teams.filter((team) => team.name.startsWith(focusedOption.value)); + 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/removemember.js b/src/commands/admin/team/removemember.js new file mode 100644 index 0000000..6466bcd --- /dev/null +++ b/src/commands/admin/team/removemember.js @@ -0,0 +1,46 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { ConfigService } from '#services/configService.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('removemember') + .setDescription('Remove a member from the team.') + .addUserOption((option) => + option.setName('member').setDescription('The member to remove from the team.').setRequired(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + if (!interaction.member.permissions.has('Administrator')) { + const customRole = await ConfigService.getAdminRole(interaction.guildId); + if (customRole) { + const member = await interaction.guild.members.fetch(interaction.user.id); + if (!member.roles.cache.has(customRole)) { + await interaction.reply('You do not have permission to set the approval channel.'); + return; + } + } else { + await interaction.reply('You do not have permission to set the approval channel.'); + return; + } + } + const member = interaction.options.getUser('member'); + + try { + // Check if the user is in a team + if (!(await TeamService.isUserInTeam(member.id))) { + await interaction.reply('This user is not in any team.'); + return; + } + // Remove member from all teams + await TeamService.removeMemberFromAllTeams(member.id); + await interaction.reply(`Member <@${member.id}> removed from the team successfully!`); + logger.info(`Member ${member.username} - ${member.id} removed from the team successfully!`); + } catch (error) { + logger.error('Error removing member from team', error); + await interaction.reply('An error occurred while removing the member from the team.'); + } +} diff --git a/src/commands/event/eventinfo.js b/src/commands/event/eventinfo.js new file mode 100644 index 0000000..40e4266 --- /dev/null +++ b/src/commands/event/eventinfo.js @@ -0,0 +1,53 @@ +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(true) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const eventId = interaction.options.getString('event'); + const event = await EventService.getEventById(eventId); + 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('\n') || '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..e363ed0 --- /dev/null +++ b/src/commands/event/teaminfo.js @@ -0,0 +1,52 @@ +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 }, + { + name: 'Members', + value: team.members.map((member) => `<@${member.user.discordId}>`).join('\n') || 'None', + inline: false, + }, + ], + color: 0x00ae86, + }; + 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..a25c009 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,28 @@ export class EventService { 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; + } + } } diff --git a/src/services/teamService.js b/src/services/teamService.js index 0a36f8b..3484329 100644 --- a/src/services/teamService.js +++ b/src/services/teamService.js @@ -68,11 +68,19 @@ export class TeamService { throw error; } } + /** + * @typedef {import('@prisma/client').Team & { + * leader: import('@prisma/client').DiscordUser, + * members: (import('@prisma/client').TeamMember & { + * user: import('@prisma/client').DiscordUser + * })[] + * }} TeamWithDetails + */ /** * 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 +88,14 @@ export class TeamService { where: { id: teamId, }, + include: { + leader: true, + members: { + include: { + user: true, + }, + }, + }, }); if (!team) { throw new Error('Team not found'); @@ -90,6 +106,7 @@ export class TeamService { throw error; } } + /** * Update a team by ID * @param {String} teamId - The ID of the team @@ -121,4 +138,79 @@ export class TeamService { throw error; } } + + /** + * Check if a user is in a team + * @param {String} userId - The discord Id of the user + */ + static async isUserInTeam(userId) { + try { + const team = await prisma.team.findFirst({ + where: { + members: { + some: { + user: { + discordId: userId, + }, + }, + }, + }, + }); + return !!team; + } catch (error) { + logger.error('Error checking if user is in team', error); + throw error; + } + } + + /** + * Add a member to a team + * @param {String} teamId - The ID of the team + * @param {String} userId - The ID of the user + * @returns {Promise} + */ + static async addMemberToTeam(teamId, userId) { + try { + return await prisma.team.update({ + where: { + id: teamId, + }, + data: { + members: { + create: { + user: { + connect: { + discordId: userId, + }, + }, + }, + }, + }, + }); + } catch (error) { + console.log(error); + logger.error('Error adding member to team', error); + throw error; + } + } + + /** + * Remove member from all teams + * @param {String} userId - The ID of the user + * @returns {Promise} + */ + static async removeMemberFromAllTeams(userId) { + try { + await prisma.teamMember.deleteMany({ + where: { + user: { + discordId: userId, + }, + }, + }); + } catch (error) { + logger.error('Error removing member from all teams', error); + throw error; + } + } }