From bb85b162baa37909e62793e1c385d46e68c3e949 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 27 Apr 2026 12:59:42 +0200 Subject: [PATCH 1/6] Staff Management System V1.1 --- locales/en.json | 28 +++++----- .../staff-management-system/commands/duty.js | 2 +- .../commands/staff-status.js | 12 ++-- .../configs/activity-checks.json | 39 +++++++++++-- .../configs/configuration.json | 2 +- .../configs/infractions.json | 26 ++++----- .../configs/promotions.json | 4 +- .../configs/reviews.json | 8 +-- .../staff-management.js | 56 ++++++++++++++----- 9 files changed, 117 insertions(+), 60 deletions(-) diff --git a/locales/en.json b/locales/en.json index b71ae7f..e8d33f7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1067,7 +1067,7 @@ "quota-fail": "❌ Quota Not Met", "duty-time-title": "Shift Time - %type", "duty-time-desc": "**Total Shifts:** %count\n**Total Duration:** %duration", - "btn-hist": "View History", + "btn-hist": "View Shift History", "err-no-lb": "ℹ️ No shift data found for **%type**.", "duty-lb-title": "Leaderboard - %type", "duty-lb-desc": "**%lookback Top Shifts**\n\n%lines", @@ -1084,7 +1084,6 @@ "err-not-on": "❌ You are not on a shift.", "err-hist-oth": "❌ You can only view your own history.", "mod-v-all-title": "Confirm: Void All Shifts", - "mod-v-all-lbl": "Type CONFIRM to delete all shift data", "err-conf-fail": "❌ Data deletion confirmation failed. You must type the phrase exactly.", "succ-v-all": "All shift data for <@%user> has been deleted successfully.", "mod-add-t": "Add Duty Time", @@ -1099,25 +1098,25 @@ "err-no-perm": "❌ You do not have permission to do this.", "err-no-mem": "❌ Could not find that member.", "ph-sel-type": "Select a Shift Type", - "msg-sel-type": "👇 Please choose your shift type:", + "msg-sel-type": "👇 Please choose your shift type below:", "err-prof-dis": "❌ Staff Profiles are disabled.", - "err-prof-cfg": "❌ Configuration is missing. Please make sure the message is not empty.", + "err-prof-cfg": "❌ Configuration is missing. Please make sure that the message is not empty.", "err-prof-no-own": "❌ You do not have a staff profile.", - "err-prof-no-tgt": "❌ That user does not have a profile.", - "rev-dis-text": "*Reviews disabled*", + "err-prof-no-tgt": "❌ That user does not have a staff profile.", + "rev-dis-text": "*Reviews are disabled*", "rev-no-rate": "No ratings yet", "stat-offl": "⚫ Offline", "stat-onl": "🟢 Online", "stat-idl": "🟡 Away", "stat-dnd": "🔴 Do Not Disturb", "stat-prof-ond": "⏱️ On duty", - "stat-prof-loa": "🌙 On LoA", - "stat-prof-ra": "⛱️ On RA", - "prof-no-intro": "*No introduction set.*", + "stat-prof-loa": "🌙 On Leave Of Absence (LOA)", + "stat-prof-ra": "⛱️ On Reduced Activity (RA)", + "prof-no-intro": "😕 *This user did not set an introduction.*", "err-prof-empty": "❌ Profile embed is empty.", "err-prof-perm": "❌ You must be a staff member to have a profile.", "prof-edit-title": "Edit Profile", - "prof-edit-nick": "Custom Nickname", + "prof-edit-nick": "Your custom nickname", "prof-edit-intro": "Introduction", "succ-prof-wipe": "✅ Profile wiped for %u.", "succ-prof-upd": "✅ Profile updated!", @@ -1138,7 +1137,7 @@ "succ-del-all": "✅ ALL data has been permanently wiped.", "err-del-time": "⏳ Data deletion timed out.", "succ-del-tgt": "✅ Target data has been permanently wiped.", - "err-gen-no-perm": "❌ You do not have permission.", + "err-gen-no-perm": "❌ You do not have permission to do this.", "err-no-req": "❌ Request not found.", "err-req-hndl": "❌ Request is already %status.", "mod-deny-req": "Deny Request", @@ -1167,8 +1166,8 @@ "log-info-hdr": "%label Information", "general-start": "Start", "general-end": "End", - "log-end-title": "%label ended for %username", - "log-end-desc": "%label ended for %mention.", + "log-end-title": "%username's %label has ended.", + "log-end-desc": "%mention's %label has ended.", "general-started": "Started", "general-ended": "Ended", "log-adj-title": "%label adjusted for %username", @@ -1434,6 +1433,7 @@ "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work.", - "status-expired-auto": "Ended automatically because the status expired." + "status-expired-auto": "Ended automatically because the status expired.", + "label-system": "System" } } diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index df2c050..45e4a42 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -1035,7 +1035,7 @@ async function handleDutyAdminVoidAll(client, interaction) { new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId('confirm') - .setLabel(localize('staff-management-system', 'mod-v-all-lbl')) + .setLabel(localize('staff-management-system', 'mod-del-lbl')) .setStyle(TextInputStyle.Short) .setPlaceholder(confirmPhrase) .setRequired(true) diff --git a/modules/staff-management-system/commands/staff-status.js b/modules/staff-management-system/commands/staff-status.js index e7e7be7..9eb2b69 100644 --- a/modules/staff-management-system/commands/staff-status.js +++ b/modules/staff-management-system/commands/staff-status.js @@ -514,6 +514,7 @@ async function handleStatusEndSubmit(client, interaction, type) { flags: MessageFlags.Ephemeral }); } + await interaction.deferUpdate(); const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); @@ -574,10 +575,9 @@ async function handleStatusEndSubmit(client, interaction, type) { .setEmoji('📜') .setStyle(ButtonStyle.Secondary) ); - return interaction.reply({ + return interaction.editReply({ embeds: [updatedEmbed.toJSON()], - components: [disabledRow.toJSON()], - flags: MessageFlags.Ephemeral + components: [disabledRow.toJSON()] }); } @@ -662,6 +662,7 @@ async function handleStatusExtendSubmit(client, interaction, type) { flags: MessageFlags.Ephemeral }); } + await interaction.deferUpdate(); const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); @@ -719,10 +720,9 @@ async function handleStatusExtendSubmit(client, interaction, type) { r: request.reason || localize('staff-management-system', 'info-none') }) }); - return interaction.reply({ + return interaction.editReply({ embeds: [updatedEmbed.toJSON()], - components: interaction.message.components.map(c => c.toJSON()), - flags: MessageFlags.Ephemeral + components: interaction.message.components.map(c => c.toJSON()) }); } diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json index 634c016..9d83f6c 100644 --- a/modules/staff-management-system/configs/activity-checks.json +++ b/modules/staff-management-system/configs/activity-checks.json @@ -29,7 +29,7 @@ "name": "enableActivityChecks", "category": "general", "humanName": "Enable Activity Checks", - "description": "Allows admins to start an activity check to see who is active.", + "description": "Allows admins to start an activity check to see who is active, and also set automatic activity checks.", "type": "boolean", "default": true, "elementToggle": true @@ -69,12 +69,43 @@ { "name": "duration", "description": "The configured duration in hours." + }, + { + "name": "staff-mention", + "description": "Mention of the configured staff role(s)." + }, + { + "name": "supervisor-mention", + "description": "Mention of the configured supervisor role(s)." + }, + { + "name": "management-mention", + "description": "Mention of the configured management role(s)." + }, + { + "name": "initiator", + "description": "The user who iniated the activity check. This will show 'system' if it was an automated check." } ], "default": { - "title": "📋 Staff Activity Check", - "description": "Please click the button below to confirm your activity before %endtime%.", - "color": "#3498db" + "_schema": "v3", + "content": "%staff-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %initiator%" + }, + "title": "📋 Staff Activity Check", + "description": "Please confirm your activity by clicking the button below before %end-time%. This activity check will stay open for %duration% hour(s), and members who do not respond before it ends may be marked as failed unless they qualify for an exception.", + "fields": [ + { + "name": "Quick info overview", + "value": "Ends at: %end-time%\nDuration: %duration% hour(s)" + } + ], + "color": "#3498db" + } + ] } }, { diff --git a/modules/staff-management-system/configs/configuration.json b/modules/staff-management-system/configs/configuration.json index 9b978d2..74f326f 100644 --- a/modules/staff-management-system/configs/configuration.json +++ b/modules/staff-management-system/configs/configuration.json @@ -28,7 +28,7 @@ "name": "supervisorRoles", "category": "roles", "humanName": "Supervisor Roles", - "description": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts, promote and infract users).", + "description": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts etc.).", "type": "array", "content": "roleID", "default": [] diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index 89a6bc1..1fd941a 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -44,11 +44,23 @@ "Under Investigation" ] }, + { + "name": "infractionLogChannel", + "category": "messages", + "humanName": "Infraction Log Channel", + "description": "Where should infractions and suspensions be announced?", + "type": "channelID", + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], + "default": "" + }, { "name": "enableSuspensions", "category": "suspensions", "humanName": "Enable Suspensions System", - "description": "Suspensions temporarily strip a staff member of their roles.", + "description": "Suspensions temporarily strip a staff member of their roles, and give them back after the specified duration.", "type": "boolean", "default": true }, @@ -137,18 +149,6 @@ ] } }, - { - "name": "infractionLogChannel", - "category": "messages", - "humanName": "Infraction Log Channel", - "description": "Where should infractions and suspensions be announced?", - "type": "channelID", - "channelTypes": [ - "GUILD_TEXT", - "GUILD_NEWS" - ], - "default": "" - }, { "name": "infractionMessage", "category": "messages", diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json index 9bb7055..e0a89fd 100644 --- a/modules/staff-management-system/configs/promotions.json +++ b/modules/staff-management-system/configs/promotions.json @@ -19,7 +19,7 @@ "name": "enablePromotions", "category": "logic", "humanName": "Enable Promotions System", - "description": "If disabled, the /staff-management promote command will not work.", + "description": "Enabling this allows staff members to promote users to higher ranks.", "type": "boolean", "default": true, "elementToggle": true @@ -30,7 +30,7 @@ "humanName": "Auto-Add New Role?", "description": "If enabled, the bot will automatically give the user the new rank role. Note: This makes your server prone to raids by promoting someone to a role with more dangerous permissions which can be used to do malicious actions. It is recommended to keep this setting disabled.", "type": "boolean", - "default": true + "default": false }, { "name": "promotionsChannel", diff --git a/modules/staff-management-system/configs/reviews.json b/modules/staff-management-system/configs/reviews.json index b23dd31..6065550 100644 --- a/modules/staff-management-system/configs/reviews.json +++ b/modules/staff-management-system/configs/reviews.json @@ -19,7 +19,7 @@ "name": "enableReviews", "category": "settings", "humanName": "Enable Reviews System", - "description": "Enabling this unlocks the staff review system, allowing users to submit ratings and feedback for staff members.", + "description": "Enabling this unlocks the staff review system, allowing users to submit ratings with feedback for staff members.", "type": "boolean", "default": true }, @@ -39,7 +39,7 @@ "name": "allowSelfRating", "category": "settings", "humanName": "Allow Self-Rating?", - "description": "If enabled, staff can review themselves. This is not recommended to keep a fair ratings system.", + "description": "If enabled, staff can review themselves. This is not recommended to keep a fair review system.", "type": "boolean", "default": false }, @@ -47,7 +47,7 @@ "name": "onlyAllowStaffReview", "category": "settings", "humanName": "Only let users review staff", - "description": "If enabled, only staff members can review other staff members.", + "description": "If enabled, users can only review staff members.", "type": "boolean", "default": true }, @@ -92,7 +92,7 @@ ], "default": { "_schema": "v3", - "content": "%staff%", + "content": "%staff-mention%", "embeds": [ { "title": "🌟 New Staff Rating", diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 9d96d2f..7aa2127 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -117,6 +117,11 @@ async function issueInfraction(client, interaction, targetMember, type, reason, }); } + const canInfract = checkStaffPermissions(interaction.member, config, 'staff'); + if (!canInfract) return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') + }); + if (type.toLowerCase() === 'suspension') { return interaction.editReply({ content: localize('staff-management-system', 'err-use-susp') @@ -250,6 +255,11 @@ async function issueSuspension(client, interaction, targetMember, durationInput, }); } + const canSuspend = checkStaffPermissions(interaction.member, config, 'staff'); + if (!canSuspend) return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') + }); + const durationDays = parseDurationToDays(durationInput); if (!durationDays) return interaction.editReply({ @@ -1391,27 +1401,43 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa const durationHours = config.timeframe || 24; const endTime = new Date(Date.now() + durationHours * 60 * 60 * 1000); + const generalConfig = getConfig(client, 'configuration') || {}; - let embedTemplate = typeof config.checkMessage === 'string' - ? JSON.parse(config.checkMessage) - : config.checkMessage; - let msgOpts = await embedTypeV2(embedTemplate, { - '%end-time%': ``, - '%duration%': durationHours.toString() - }); + const formatRoleMentions = (roles) => { + const roleIds = Array.isArray(roles) + ? roles + : (roles ? [roles] : []); - if (msgOpts?.content?.trim() === '') delete msgOpts.content; - msgOpts.components = [ - new ActionRowBuilder() - .addComponents( - new ButtonBuilder() + return roleIds.map(roleId => `<@&${roleId}>`).join(' '); + }; + const initiator = isAutomated + ? localize('staff-management-system', 'label-system') + : interactionOrChannel.user.toString(); + + const responseButtonRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() .setCustomId('staff-mgmt_ac-respond') .setLabel(localize('staff-management-system', 'ac-confirm-btn')) .setStyle(ButtonStyle.Success) .setEmoji('✅') - ) - .toJSON() - ]; + ) + .toJSON(); + + let msgOpts = await embedTypeV2(embedTemplate, { + '%end-time%': ``, + '%duration%': durationHours.toString(), + '%staff-mention%': formatRoleMentions(generalConfig.staffRoles), + '%supervisor-mention%': formatRoleMentions(generalConfig.supervisorRoles), + '%management-mention%': formatRoleMentions(generalConfig.managementRoles), + '%initiator%': initiator + }, + { + components: [responseButtonRow] + } + ); + + if (msgOpts?.content?.trim() === '') delete msgOpts.content; try { const checkMessage = await targetChannel.send(msgOpts); From d945767eb21b96e553a6518d5ed2b15a94b9016a Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 27 Apr 2026 21:57:52 +0200 Subject: [PATCH 2/6] Fixed CV2 for ending AC --- .../staff-management.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 7aa2127..388dbe1 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -1473,16 +1473,20 @@ async function endActivityCheckProcess(client, activeCheck) { try { const msg = await guild.channels.cache.get(activeCheck.channelId)?.messages.fetch(activeCheck.messageId); - if (msg && msg.embeds.length > 0) { - const originalEmbed = EmbedBuilder - .from(msg.embeds[0]) - .setColor('#ed4245'); - originalEmbed - .setTitle(localize('staff-management-system', 'ac-title-end')); - await msg.edit({ - embeds: [originalEmbed.toJSON()], + if (msg) { + const editPayload = { components: [] - }); + }; + + if (msg.embeds.length > 0) { + const originalEmbed = EmbedBuilder + .from(msg.embeds[0]) + .setColor('#ed4245') + .setTitle(localize('staff-management-system', 'ac-title-end')); + + editPayload.embeds = [originalEmbed.toJSON()]; + } + await msg.edit(editPayload); } } catch (e) {} From e6cdd47cd05151c1cb650091160dcc8858898cb4 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 28 Apr 2026 13:00:11 +0200 Subject: [PATCH 3/6] Utilised helper to format timestamp --- modules/staff-management-system/staff-management.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 388dbe1..197d4bd 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -6,7 +6,7 @@ const { ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js'); const { Op } = require('sequelize'); const schedule = require('node-schedule'); -const { embedTypeV2, safeSetFooter } = require('../../src/functions/helpers'); +const { embedTypeV2, safeSetFooter, dateToDiscordTimestamp } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); // --- Local helpers --- @@ -1425,7 +1425,7 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa .toJSON(); let msgOpts = await embedTypeV2(embedTemplate, { - '%end-time%': ``, + '%end-time%': dateToDiscordTimestamp(endTime, 'F'), '%duration%': durationHours.toString(), '%staff-mention%': formatRoleMentions(generalConfig.staffRoles), '%supervisor-mention%': formatRoleMentions(generalConfig.supervisorRoles), From 97efeb23f3a618611bd61195194692978f7672b1 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 28 Apr 2026 13:13:17 +0200 Subject: [PATCH 4/6] Used helper for timestamps in main logic file Co-authored-by: Copilot --- .../staff-management.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 197d4bd..9d320c2 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -162,7 +162,7 @@ async function issueInfraction(client, interaction, targetMember, type, reason, '%reason%': reason, '%case-id%': record.caseId.toString(), '%end-date%': expiresAt - ? `` + ? dateToDiscordTimestamp(expiresAt, 'F') : localize('staff-management-system', 'label-never') }; @@ -313,7 +313,7 @@ async function issueSuspension(client, interaction, targetMember, durationInput, '%duration%': durationString, '%reason%': reason, '%case-id%': record.caseId.toString(), - '%end-date%': `` + '%end-date%': dateToDiscordTimestamp(expiresAt, 'F') }; const channelId = getSafeChannelId(config.infractionLogChannel); @@ -494,10 +494,10 @@ async function generateInfractionHistoryResponse(client, targetUser, page = 1) { ? '🔴' : localize('staff-management-system', 'icon-voided'); const expiry = r.expiresAt - ? `\n**${localize('staff-management-system', 'label-exp')}:** ` + ? `\n**${localize('staff-management-system', 'label-exp')}:** ${dateToDiscordTimestamp(r.expiresAt, 'R')}` : ''; - return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'label-iss')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}${link}`; + return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** ${dateToDiscordTimestamp(r.createdAt, 'f')}\n**${localize('staff-management-system', 'label-iss')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}${link}`; }).join('\n\n'); embed.setDescription(desc); @@ -688,7 +688,7 @@ async function generatePromotionHistoryResponse(client, targetUser, page = 1) { const desc = rows.map((r, i) => { const link = r.messageUrl ? ` • [Jump](${r.messageUrl})` : ''; - return `**${offset + i + 1}. **\n**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${link}`; + return `**${offset + i + 1}. ${dateToDiscordTimestamp(r.createdAt, 'F')}**\n**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${link}`; }).join('\n\n'); embed.setDescription(desc); @@ -826,8 +826,8 @@ async function generatePanelInfractions(client, targetUser, page = 1) { } else { desc += rows.map(r => { const statusIcon = r.active ? '🔴' : localize('staff-management-system', 'icon-voided'); - const expiry = r.expiresAt ? `\n**${localize('staff-management-system', 'label-exp')}:** ` : ''; - return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}`; + const expiry = r.expiresAt ? `\n**${localize('staff-management-system', 'label-exp')}:** ${dateToDiscordTimestamp(r.expiresAt, 'R')}` : ''; + return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** ${dateToDiscordTimestamp(r.createdAt, 'f')}\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}`; }).join('\n\n'); } @@ -886,7 +886,7 @@ async function generatePanelPromotions(client, targetUser, page = 1) { if (rows.length === 0) { desc += localize('staff-management-system', 'p-no-hist'); } else { - desc += rows.map(r => `**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); + desc += rows.map(r => `**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'label-date')}:** ${dateToDiscordTimestamp(r.createdAt, 'R')}\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); } embed.setDescription(desc); @@ -989,7 +989,7 @@ async function generatePanelStatus(client, targetUser, page = 1) { const activeStatus = allStatuses.find(s => ['APPROVED', 'PENDING'].includes(s.status) && new Date(s.endDate) > new Date()); let activeText = localize('staff-management-system', 'info-none'); if (activeStatus) { - activeText = `**${activeStatus.type}** (${activeStatus.status})\n${localize('staff-management-system', 'label-end')}: `; + activeText = `**${activeStatus.type}** (${activeStatus.status})\n${localize('staff-management-system', 'label-end')}: ${dateToDiscordTimestamp(activeStatus.endDate, 'R')}`; } const embed = applyFooter(client, new EmbedBuilder() @@ -1018,7 +1018,7 @@ async function generatePanelStatus(client, targetUser, page = 1) { ENDED: '⏹️', PENDING: '🕐' }; - desc += rows.map(r => `**${icons[r.status] || '❓'} ${r.type} - ${r.status}**\n**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); + desc += rows.map(r => `**${icons[r.status] || '❓'} ${r.type} - ${r.status}**\n**${localize('staff-management-system', 'general-start')}:** ${dateToDiscordTimestamp(r.startDate, 'D')}\n**${localize('staff-management-system', 'general-end')}:** ${dateToDiscordTimestamp(r.endDate, 'D')}\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); } embed.setDescription(desc); @@ -1117,8 +1117,8 @@ async function generatePanelActivity(client, targetUser, page = 1) { desc += localize('staff-management-system', 'p-no-hist'); } else { desc += paginatedRows.map(r => - `**${localize('staff-management-system', 'label-chk')} **\n` + - `**${localize('staff-management-system', 'label-end')}:** \n` + + `**${localize('staff-management-system', 'label-chk')} ${dateToDiscordTimestamp(r.createdAt, 'D')}**\n` + + `**${localize('staff-management-system', 'label-end')}:** ${dateToDiscordTimestamp(r.endTime, 'F')}\n` + `**${localize('staff-management-system', 'label-chan')}:** <#${r.channelId}>` ).join('\n\n'); } From 43f58e2e1bf091a8d7b52769ca1927ec6a1bc51b Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 28 Apr 2026 20:23:47 +0200 Subject: [PATCH 5/6] Added ability to customize ac end message Co-authored-by: Copilot --- .../configs/activity-checks.json | 58 +++++++++++++++ .../models/ActivityCheck.js | 8 +++ .../staff-management.js | 70 +++++++++++-------- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json index 9d83f6c..5492fc8 100644 --- a/modules/staff-management-system/configs/activity-checks.json +++ b/modules/staff-management-system/configs/activity-checks.json @@ -108,6 +108,64 @@ ] } }, + { + "name": "endCheckMessage", + "category": "general", + "humanName": "Ended Activity Check Embed", + "description": "The message that will replace the activity check embed when it ends.", + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "end-time", + "description": "The Discord timestamp when the check ended." + }, + { + "name": "duration", + "description": "The configured duration in hours." + }, + { + "name": "staff-mention", + "description": "Mention of the configured staff role(s)." + }, + { + "name": "supervisor-mention", + "description": "Mention of the configured supervisor role(s)." + }, + { + "name": "management-mention", + "description": "Mention of the configured management role(s)." + }, + { + "name": "initiator", + "description": "The user who iniated the activity check. This will show 'system' if it was an automated check." + }, + { + "name": "responded-count", + "description": "The number or staff members who responed to the activity check." + } + ], + "default": { + "_schema": "v3", + "content": "%staff-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %initiator%" + }, + "title": "📋 Staff Activity Check (ended)", + "description": "This activity check has concluded.", + "fields": [ + { + "name": "Quick info overview", + "value": "Ended at: %end-time%\nDuration: %duration% hour(s)\nTotal responses: %responded-count%" + } + ], + "color": "#FF0000" + } + ] + } + }, { "name": "sendingChannel", "category": "general", diff --git a/modules/staff-management-system/models/ActivityCheck.js b/modules/staff-management-system/models/ActivityCheck.js index 5d0dace..92f9169 100644 --- a/modules/staff-management-system/models/ActivityCheck.js +++ b/modules/staff-management-system/models/ActivityCheck.js @@ -31,6 +31,14 @@ module.exports = class StaffManagementActivityCheck extends Model { status: { type: DataTypes.STRING, defaultValue: 'ACTIVE' + }, + initiatorId: { + type: DataTypes.STRING, + allowNull: true + }, + isAutomated: { + type: DataTypes.BOOLEAN, + defaultValue: false } }, { tableName: 'staff_management_activity_checks', diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 9d320c2..c4964dc 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -39,6 +39,14 @@ const applyFooter = (client, embed) => { return embed; }; +const formatRoleMentions = (roles) => { + const roleIds = Array.isArray(roles) + ? roles + : (roles ? [roles] : []); + + return roleIds.map(roleId => `<@&${roleId}>`).join(' '); +}; + function checkStaffPermissions(member, config, level = 'staff') { if (!member) return false; if (member.permissions?.has('Administrator')) return true; @@ -1402,14 +1410,6 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa const durationHours = config.timeframe || 24; const endTime = new Date(Date.now() + durationHours * 60 * 60 * 1000); const generalConfig = getConfig(client, 'configuration') || {}; - - const formatRoleMentions = (roles) => { - const roleIds = Array.isArray(roles) - ? roles - : (roles ? [roles] : []); - - return roleIds.map(roleId => `<@&${roleId}>`).join(' '); - }; const initiator = isAutomated ? localize('staff-management-system', 'label-system') : interactionOrChannel.user.toString(); @@ -1453,7 +1453,9 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa channelId: targetChannel.id, endTime, targetRoles: JSON.stringify(rolesToCheck), - status: 'ACTIVE' + status: 'ACTIVE', + initiatorId: isAutomated ? null : interactionOrChannel.user.id, + isAutomated }); schedule.scheduleJob(endTime, async () => { const currentCheck = await ActivityCheck.findByPk(record.id); @@ -1471,25 +1473,6 @@ async function endActivityCheckProcess(client, activeCheck) { const guild = client.guilds.cache.get(client.guildID); if (!guild) return; - try { - const msg = await guild.channels.cache.get(activeCheck.channelId)?.messages.fetch(activeCheck.messageId); - if (msg) { - const editPayload = { - components: [] - }; - - if (msg.embeds.length > 0) { - const originalEmbed = EmbedBuilder - .from(msg.embeds[0]) - .setColor('#ed4245') - .setTitle(localize('staff-management-system', 'ac-title-end')); - - editPayload.embeds = [originalEmbed.toJSON()]; - } - await msg.edit(editPayload); - } - } catch (e) {} - const config = getConfig(client, 'activity-checks'); const logChannel = guild.channels.cache.get(getSafeChannelId(config.logChannel) || getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel)); if (!logChannel) return; @@ -1511,6 +1494,9 @@ async function endActivityCheckProcess(client, activeCheck) { userId: {[Op.in]: expectedIds} } }); + const initiator = activeCheck.isAutomated + ? localize('staff-management-system', 'label-system') + : `<@${activeCheck.initiatorId}>`; expectedMembers.forEach(member => { if (respondedUserIds.has(member.id)) return responded.push(member); @@ -1530,6 +1516,34 @@ async function endActivityCheckProcess(client, activeCheck) { : failed.push(member); }); + try { + const msg = await guild.channels.cache.get(activeCheck.channelId)?.messages.fetch(activeCheck.messageId); + if (msg) { + const endTemplate = config.endCheckMessage; + const endedMessage = await embedTypeV2( + endTemplate, + { + '%end-time%': dateToDiscordTimestamp(new Date(), 'F'), + '%duration%': (config.timeframe || 24).toString(), + '%staff-mention%': formatRoleMentions(config.staffRoles), + '%supervisor-mention%': formatRoleMentions(config.supervisorRoles), + '%management-mention%': formatRoleMentions(config.managementRoles), + '%initiator%': initiator, + '%responded-count%': responded.length.toString() + }, + { + components: [] + } + ); + + if (endedMessage?.content?.trim() === '') { + delete endedMessage.content; + } + + await msg.edit(endedMessage); + } + } catch (e) {} + const embed = applyFooter(client, new EmbedBuilder() .setTitle(localize('staff-management-system', 'ac-res-title')) .setColor('Blurple') From 6f3cd478d71f49a23a7c7a0919b382001b2cc1eb Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 28 Apr 2026 21:13:33 +0200 Subject: [PATCH 6/6] DB migration added Co-authored-by: Copilot --- .../events/botReady.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/modules/staff-management-system/events/botReady.js b/modules/staff-management-system/events/botReady.js index e2d42ca..6d2405a 100644 --- a/modules/staff-management-system/events/botReady.js +++ b/modules/staff-management-system/events/botReady.js @@ -1,11 +1,69 @@ const schedule = require('node-schedule'); const { localize } = require('../../../src/functions/localize'); const { Op } = require('sequelize'); +const { + migrationStart, + migrationEnd +} = require('../../../main'); const {scheduleStatusExpiry} = require('../commands/staff-status.js'); const { initActivityCheckAutomation } = require('../staff-management'); const suspension_check_job = 'staff-management-checks'; module.exports.run = async (client) => { + const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({ + where: { + model: 'staff-management-system_ActivityCheck', + version: 'V1' + } + }); + + if (!dbVersion) { + migrationStart(); + try { + client.logger.info('[staff-management-system] Running V1 migration (adding initiatorId and isAutomated)...'); + + const data = await client.models['staff-management-system']['ActivityCheck'].findAll({ + attributes: [ + 'id', + 'messageId', + 'channelId', + 'endTime', + 'targetRoles', + 'respondedUsers', + 'status', + 'createdAt', + 'updatedAt' + ] + }); + + await client.models['staff-management-system']['ActivityCheck'].sync({ force: true }); + + for (const row of data) { + await client.models['staff-management-system']['ActivityCheck'].create({ + id: row.id, + messageId: row.messageId, + channelId: row.channelId, + endTime: row.endTime, + targetRoles: row.targetRoles, + respondedUsers: row.respondedUsers, + status: row.status, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + initiatorId: null, + isAutomated: false + }); + } + + client.logger.info('[staff-management-system] V1 migration complete.'); + await client.models['DatabaseSchemeVersion'].create({ + model: 'staff-management-system_ActivityCheck', + version: 'V1' + }); + } finally { + migrationEnd(); + } + } + const guild = client.guilds.cache.get(client.guildID); try { const LoaRequest = client.models['staff-management-system']['LoaRequest'];