import { MemberModel } from "../../database/Member"; import Partner, { PartnerModel, PartnerCommissionType, PartnerRoleType, PartnerDepartment, PartnerTitle, } from "../../database/Partner"; import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; import { ChatInputCommandInteraction, InteractionContextType, PermissionFlagsBits, } from "discord.js"; //TODO: ad email validation //TODO: cover all partner model properties in cooresponding sub commands const partnerTitles: PartnerTitle[] = [ "Director of Engineering", "Director of Operations", "Deputy Director of Engineering", "Deputy Director of Operations", "Services Manager", "Project Manager", "Engineering Core Partner", "Operations Core Partner", "Community Moderator", "Technician", ]; export default class PartnerCommand extends DiscordInteractionCommand { constructor() { super("partner", "Manipulates partner information."); this.builder .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setContexts(InteractionContextType.Guild); this.builder.addSubcommand((c) => c .setName("add") .setDescription("test") .addUserOption((option) => option.setName("partner").setDescription("the partner you want to add.").setRequired(true) ) .addStringOption((option) => option.setName("email").setDescription("their email address.").setRequired(true) ) .addStringOption((option) => option .setName("role-type") .setDescription("their roleType.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerRoleType)) .setRequired(true) ) .addStringOption((option) => option .setName("commission-type") .setDescription("their commissionType.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerCommissionType)) .setRequired(true) ) .addStringOption((option) => option .setName("title") .setDescription("their title.") .setChoices(this.formatPartnerTitlesArrayForDiscord()) .setRequired(true) ) .addStringOption((option) => option .setName("department") .setDescription("their department.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerDepartment)) .setRequired(true) ) .addUserOption((option) => option.setName("direct-report").setDescription("their direct report.") ) ); this.builder.addSubcommand((c) => c .setName("get") .setDescription("get") .addUserOption((option) => option .setName("partner") .setDescription("the partner you want to get info abouts.") .setRequired(true) ) ); this.builder.addSubcommand((c) => c .setName("delete") .setDescription("delete") .addUserOption((option) => option .setName("partner") .setDescription("the partner you want to delete.") .setRequired(true) ) ); this.builder.addSubcommand((c) => c .setName("update") .setDescription("update") .addUserOption((option) => option .setName("partner") .setDescription("the partner you want to update.") .setRequired(true) ) .addUserOption((option) => option.setName("direct-report").setDescription("their direct report.") ) .addStringOption((option) => option.setName("email").setDescription("their email address.")) .addStringOption((option) => option .setName("role-type") .setDescription("their roleType.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerRoleType)) ) .addStringOption((option) => option .setName("commission-type") .setDescription("their commissionType.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerCommissionType)) ) .addStringOption((option) => option .setName("title") .setDescription("their title.") .setChoices(this.formatPartnerTitlesArrayForDiscord()) ) .addStringOption((option) => option .setName("department") .setDescription("their department.") .setChoices(this.formatOptionsForDiscordFromEnum(PartnerDepartment)) ) ); } public async execute(interaction: ChatInputCommandInteraction): Promise { const subcommandName = interaction.options.getSubcommand(true); switch (subcommandName) { case "get": await this.handleGetSubcommand(interaction); break; case "add": await this.handleAddSubcommand(interaction); break; case "delete": await this.handleDeleteSubcommand(interaction); break; case "update": await this.handleUpdateSubcommand(interaction); break; default: //discord does not allow parent/main commands to be excutable, this range is limited to options entered via commandbuiler break; } } async handleAddSubcommand(interaction: ChatInputCommandInteraction) { const partnerOption = interaction.options.getUser("partner", true); const directReport = interaction.options.getUser("direct-report", false); const partnerOptionEmailAddress = interaction.options.getString("email", true); const partnerOptionRoleType = interaction.options.getString("role-type", true); const partnerOptionCommisioComissionType = interaction.options.getString( "commission-type", true ); const partnerOptionDepartment = interaction.options.getString("department", true); const partnerOptionTitle = interaction.options.getString("title", true); const partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); if (partner) return interaction.reply({ content: "The specified user already has a partner entry.", ephemeral: false, }); /* const member = await MemberModel.findOne({ discordID: partnerOption.id }).exec(); if (!member) return interaction.reply({ content: "The specified partner does not have a base member entry.", ephemeral: false, }); */ let directReportPartnerDocumentFromDb; if (directReport) { directReportPartnerDocumentFromDb = await PartnerModel.findOne({ discordID: directReport.id, }).exec(); if (!directReportPartnerDocumentFromDb) return interaction.reply({ content: `the specified directReport ${directReport.username} does not have an entry in partner database, please add them first them before assigning subordinates`, ephemeral: false, }); } let newPartner = new PartnerModel({ discordID: partnerOption.id, emailAddress: partnerOptionEmailAddress, roleType: partnerOptionRoleType, commissionType: partnerOptionCommisioComissionType, department: partnerOptionDepartment, title: partnerOptionTitle, directReport: directReport && directReportPartnerDocumentFromDb ? directReportPartnerDocumentFromDb._id : null, }); await newPartner.save(); return interaction.reply({ content: `\`\`\`\n${JSON.stringify(newPartner, null, 2)}\n\`\`\``, ephemeral: false, }); } async handleGetSubcommand(interaction: ChatInputCommandInteraction) { const partnerOption = interaction.options.getUser("partner", true); let partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); if (!partner) return interaction.reply({ content: "The specified partner does not an entry in the database.", ephemeral: false, }); if (partner.directReport) await partner.populate("directReport"); if (partner.directReport && partner.directReport instanceof Partner) { console.log(partner.directReport); return interaction.reply({ content: `Raw entry \`\`\`\n${JSON.stringify(partner, null, 2)}\n\`\`\`\n\nDirect report: \`\`\`\n${JSON.stringify(partner.directReport, null, 2)}\n\`\`\``, ephemeral: false, }); } return interaction.reply({ content: `Raw entry \`\`\`\n${JSON.stringify(partner, null, 2)}\n\`\`\``, ephemeral: false, }); } async handleDeleteSubcommand(interaction: ChatInputCommandInteraction) { const partnerOption = interaction.options.getUser("partner", true); const partner = await PartnerModel.findOne({ discordID: partnerOption.id }) .populate("directReport") .exec(); if (!partner) return interaction.reply({ content: "The specified user does not have an entry.", ephemeral: false, }); if ( partner.directReport && partner.directReport instanceof Partner && interaction.user.id !== partner.directReport.discordID ) return interaction.reply({ content: "You're not authorized to delete this partner's information, only their direct report can.", ephemeral: false, }); await PartnerModel.findByIdAndDelete(partner.id); return interaction.reply({ content: `removed partner entry from the database.`, ephemeral: false, }); } async handleUpdateSubcommand(interaction: ChatInputCommandInteraction) { const partnerOption = interaction.options.getUser("partner", true); const directReport = interaction.options.getUser("direct-report"); const partnerOptionEmailAddress = interaction.options.getString("email"); const partnerOptionRoleType = interaction.options.getString("role-type"); const partnerOptionCommisioComissionType = interaction.options.getString("commission-type"); const partnerOptionDepartment = interaction.options.getString("department"); const partnerOptionTitle = interaction.options.getString("title"); if ( !directReport && !partnerOptionEmailAddress && !partnerOptionEmailAddress && !partnerOptionRoleType && !partnerOptionCommisioComissionType && !partnerOptionDepartment && !partnerOptionTitle ) { return interaction.reply({ content: "You need to select atleast one option to update", ephemeral: false, }); } let partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); if (!partner) return interaction.reply({ content: "The specified partner does not have an entry.", ephemeral: false, }); if (partner.directReport) partner = await partner.populate("directReport"); console.log(partner.directReport); if ( partner.directReport instanceof PartnerModel && interaction.user.id !== partner.directReport.discordID ) return interaction.reply({ content: "You're not authorized to update this partner's information, only their direct report can.", ephemeral: false, }); let directReportPartnerDocumentFromDb; if (directReport) { directReportPartnerDocumentFromDb = await PartnerModel.findOne({ discordID: directReport.id, }).exec(); if (!directReportPartnerDocumentFromDb) return interaction.reply({ content: `the specified directReport ${directReport.username} does not have an entry in partner database, please add them first them before assigning subordinates`, ephemeral: false, }); } let updateObj = { discordID: partnerOption.id, emailAddress: partnerOptionEmailAddress, roleType: partnerOptionRoleType, commissionType: partnerOptionCommisioComissionType, department: partnerOptionDepartment, title: partnerOptionTitle, directReport: directReport && directReportPartnerDocumentFromDb ? directReportPartnerDocumentFromDb.id : null, }; try { let updatedPartner = await this.updatePartnerInfo(partner.id, updateObj); return interaction.reply({ content: `updated partner!\n\n\`\`\`\n${JSON.stringify(updatedPartner, null, 2)}\n\`\`\``, ephemeral: false, }); } catch (error) { return interaction.reply({ content: `an error occured !\n\n\`\`\`\n${JSON.stringify(error, null, 2)}\n\`\`\``, ephemeral: false, }); } } private formatPartnerTitlesArrayForDiscord(): { name: PartnerTitle; value: string }[] { return partnerTitles.map((title) => ({ name: title, value: title, })); } private formatOptionsForDiscordFromEnum(args: any): { name: string; value: string }[] { return Object.entries(args) .filter(([key, value]) => typeof value === "number") // Filter out reverse mappings .map(([key, value]) => ({ name: key, value: (value as number).toString(), })); } private async updatePartnerInfo(id: string, updateObj: Partial) { // Remove keys with falsy values (undefined, null, etc.) const filteredUpdate = Object.fromEntries( Object.entries(updateObj).filter(([key, value]) => key !== "discordID" && value) ); if (Object.keys(filteredUpdate).length === 0) { throw new Error( "Error in Partner update command, no options specified on update, you can safely ignore this error." ); } // Find and update the document by ID with only the valid fields return await PartnerModel.findByIdAndUpdate(id, filteredUpdate, { new: true }); } }