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.
55 lines
1.8 KiB
JavaScript
55 lines
1.8 KiB
JavaScript
'use strict';
|
|
|
|
const mongoose = require('mongoose');
|
|
const { logError } = require('./debugLog');
|
|
|
|
// Derives ticketType from gmailThreadId prefix.
|
|
// discord-* and discord-msg-* → 'discord'; everything else → 'email'.
|
|
function deriveTicketType(gmailThreadId) {
|
|
if (!gmailThreadId) return 'email';
|
|
if (gmailThreadId.startsWith('discord-')) return 'discord';
|
|
return 'email';
|
|
}
|
|
|
|
// Extracts the standard event fields from a ticket document.
|
|
// Returns a plain object — does NOT include guildId (call-site only).
|
|
function denormalizeTicket(ticket) {
|
|
if (!ticket) return {};
|
|
return {
|
|
ticketType: deriveTicketType(ticket.gmailThreadId),
|
|
tier: ticket.escalationTier ?? 0,
|
|
priority: ticket.priority,
|
|
game: ticket.game,
|
|
senderEmail: ticket.senderEmail,
|
|
creatorId: ticket.creatorId,
|
|
gmailThreadId: ticket.gmailThreadId
|
|
};
|
|
}
|
|
|
|
// recordAction(staffId, type, payload)
|
|
//
|
|
// payload may carry:
|
|
// ticket — a Ticket doc to denormalize standard fields from
|
|
// guildId — must come from the call site (not on the Ticket schema)
|
|
// any other StaffAction field — these override denormalized values
|
|
//
|
|
// Fire-and-forget: never throws, never blocks the caller.
|
|
// The outer try/catch catches synchronous errors (e.g. model not registered
|
|
// during early boot); the inner .catch handles async DB rejections.
|
|
function recordAction(staffId, type, payload) {
|
|
try {
|
|
const { ticket, ...overrides } = payload || {};
|
|
const base = denormalizeTicket(ticket);
|
|
const doc = { staffId, type, ...base, ...overrides };
|
|
|
|
const StaffAction = mongoose.model('StaffAction');
|
|
StaffAction.create(doc).catch(err => {
|
|
logError('staffStats.recordAction', err);
|
|
});
|
|
} catch (err) {
|
|
logError('staffStats.recordAction', err);
|
|
}
|
|
}
|
|
|
|
module.exports = { recordAction, denormalizeTicket, deriveTicketType };
|