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:
@@ -7,21 +7,26 @@ const { extractRawEmail, isStaff, getCleanBody } = require('../utils');
|
||||
const { getGmailClient, sendGmailReply } = require('../services/gmail');
|
||||
const { getNotifyDm } = require('../services/staffSettings');
|
||||
const { logError } = require('../services/debugLog');
|
||||
const { recordAction } = require('../services/staffStats');
|
||||
|
||||
const Ticket = mongoose.model('Ticket');
|
||||
|
||||
/**
|
||||
* Handle a Discord message in a ticket channel → relay to Gmail (email tickets only).
|
||||
*/
|
||||
async function handleDiscordReply(m) {
|
||||
async function handleDiscordReply(m, _TicketModel, _recordAction, _isStaff) {
|
||||
const T = _TicketModel || Ticket;
|
||||
const record = _recordAction || recordAction;
|
||||
const checkIsStaff = _isStaff || isStaff;
|
||||
|
||||
if (m.author.bot || m.interaction) return;
|
||||
|
||||
const ticket = await Ticket.findOne({ discordThreadId: m.channel.id }).lean();
|
||||
const ticket = await T.findOne({ discordThreadId: m.channel.id }).lean();
|
||||
if (!ticket) return;
|
||||
|
||||
const memberForCheck = await m.guild.members.fetch(m.author.id).catch(() => null);
|
||||
const isStaffMember = isStaff(memberForCheck);
|
||||
Ticket.updateOne(
|
||||
const isStaffMember = checkIsStaff(memberForCheck);
|
||||
T.updateOne(
|
||||
{ discordThreadId: m.channel.id },
|
||||
{ $set: { lastActivity: new Date() } }
|
||||
).catch(err => logError('updateActivity', err).catch(() => {}));
|
||||
@@ -46,6 +51,10 @@ async function handleDiscordReply(m) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isStaffMember) {
|
||||
record(m.author.id, 'response', { ticket, guildId: m.guild?.id });
|
||||
}
|
||||
|
||||
if (ticket.gmailThreadId.startsWith('discord-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user