Add per-staff metrics: StaffAction event log + /stats command
Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats command. Foundation for a future tickets-website analytics dashboard. Data: - StaffAction model (event log) + Ticket.game / Ticket.closedAt - STATS_ADMIN_IDS config (who may view others' stats) Recording (fire-and-forget, idempotent on real state transitions): - claim, response (channel reply + /response send), escalate, de-escalate, transfer, close (4 sites), reopen — each denormalizes ticketType, tier, priority, game, requester (senderEmail / creatorId), guildId - close events carry closerType / resolverId (claimer credit) / wasClaimed; transfer carries fromId / toId; reopen stamps resolverId - conditional close transition helper (atomic open->closed + closedAt) shared by all four close paths Query + command: - pure period parser (presets + free-text) and stats shaper (per-metric keys) - command-aware autocomplete dispatch - /stats: period (autocomplete) + member (admin-gated) + source (all/email/ discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed 288+ unit tests; timing/busiest-times data is collected but displayed later.
This commit is contained in:
@@ -17,6 +17,7 @@ const { mongoose } = require('../../db-connection');
|
||||
const { CONFIG } = require('../../config');
|
||||
const { replaceVariables } = require('../../utils');
|
||||
const { logError } = require('../../services/debugLog');
|
||||
const { recordAction } = require('../../services/staffStats');
|
||||
|
||||
const Tag = mongoose.model('Tag');
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
@@ -38,14 +39,18 @@ async function handleResponse(interaction) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponseSend(interaction) {
|
||||
async function handleResponseSend(interaction, _TagModel, _TicketModel, _recordAction) {
|
||||
const TTag = _TagModel || Tag;
|
||||
const TTicket = _TicketModel || Ticket;
|
||||
const record = _recordAction || recordAction;
|
||||
|
||||
const name = interaction.options.getString('name');
|
||||
const tag = await Tag.findOne({ name }).lean();
|
||||
const tag = await TTag.findOne({ name }).lean();
|
||||
if (!tag) {
|
||||
return interaction.reply({ content: `❌ Tag "${name}" not found.`, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const ticket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean();
|
||||
const ticket = await TTicket.findOne({ discordThreadId: interaction.channel.id }).lean();
|
||||
const context = {
|
||||
ticket: ticket || {},
|
||||
staff: {
|
||||
@@ -57,10 +62,14 @@ async function handleResponseSend(interaction) {
|
||||
};
|
||||
|
||||
const content = replaceVariables(tag.content, context);
|
||||
await Tag.updateOne({ name }, { $inc: { useCount: 1 } });
|
||||
await TTag.updateOne({ name }, { $inc: { useCount: 1 } });
|
||||
// Tag bodies are staff-authored but may include variable substitutions from user/ticket data.
|
||||
// Disable mention parsing so a `@everyone` in a tag body never pings.
|
||||
await interaction.reply({ content, allowedMentions: { parse: [] } });
|
||||
|
||||
if (ticket) {
|
||||
record(interaction.user.id, 'response', { ticket, guildId: interaction.guild?.id });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponseCreate(interaction) {
|
||||
@@ -146,9 +155,8 @@ const RESPONSE_SUBCOMMANDS = {
|
||||
list: handleResponseList
|
||||
};
|
||||
|
||||
/** Autocomplete handler. Currently only /response uses it. */
|
||||
/** Autocomplete handler for /response. Routed here by the dispatcher in index.js. */
|
||||
async function handleAutocomplete(interaction) {
|
||||
if (interaction.commandName !== 'response') return;
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
if (!['send', 'edit', 'delete'].includes(subcommand)) return;
|
||||
|
||||
@@ -162,4 +170,4 @@ async function handleAutocomplete(interaction) {
|
||||
await interaction.respond(filtered);
|
||||
}
|
||||
|
||||
module.exports = { handleResponse, handleAutocomplete };
|
||||
module.exports = { handleResponse, handleAutocomplete, handleResponseSend };
|
||||
|
||||
Reference in New Issue
Block a user