import { format } from "date-fns"; import type { AuditLogEntry } from "~/models/audit-log"; export const AUDIT_ACTION_LABELS: Record = { league_settings_changed: "League Settings Changed", draft_settings_changed: "Draft Settings Changed", scoring_rules_changed: "Scoring Rules Changed", sports_changed: "Sports Changed", draft_order_set: "Draft Order Set", draft_order_randomized: "Draft Order Randomized", draft_started: "Draft Started", draft_paused: "Draft Paused", draft_resumed: "Draft Resumed", draft_reset: "Draft Reset", draft_rollback: "Draft Rolled Back", force_autopick: "Forced Auto-Pick", force_manual_pick: "Forced Manual Pick", draft_pick_changed: "Pick Replaced", time_bank_edited: "Time Bank Edited", brackt_resolved: "Brackt Resolved", }; const SCORING_FIELD_LABELS: Record = { pointsFor1st: "1st", pointsFor2nd: "2nd", pointsFor3rd: "3rd", pointsFor4th: "4th", pointsFor5th: "5th", pointsFor6th: "6th", pointsFor7th: "7th", pointsFor8th: "8th", }; const DRAFT_FIELD_LABELS: Record = { draftRounds: "Rounds", draftDateTime: "Draft date", draftInitialTime: "Initial time", draftIncrementTime: "Increment", draftTimerMode: "Timer mode", }; const LEAGUE_FIELD_LABELS: Record = { name: "Name", isPublicDraftBoard: "Public draft board", discordWebhookUrl: "Discord webhook", }; function formatDuration(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; if (m === 0) return `${s}s`; if (s === 0) return `${m}m`; return `${m}m ${s}s`; } function formatDraftField(key: string, value: unknown): string { if (value === null || value === undefined) return "none"; if (key === "draftInitialTime" || key === "draftIncrementTime") { return formatDuration(Number(value)); } if (key === "draftDateTime") { return format(new Date(value as string), "MMM d, yyyy h:mm a"); } if (key === "draftTimerMode") { return value === "chess_clock" ? "Chess clock" : "Standard"; } // draftRounds and any other numeric fields render as plain numbers return String(value); } function formatLeagueField(key: string, value: unknown): string { if (value === null || value === undefined || value === "") return "none"; if (key === "isPublicDraftBoard") return value ? "Yes" : "No"; if (key === "discordWebhookUrl") return "set"; return String(value); } function diffLines( fields: string[], prev: Record, next: Record, labels: Record, formatter: (key: string, value: unknown) => string ): string { if (fields.length === 0) return ""; return fields .map((f) => { const label = labels[f] ?? f; const from = prev[f]; const to = next[f]; // `from` is undefined when the log entry predates the addition of previousValues — // in that case just show the new value without an arrow. if (from !== undefined) { return `${label}: ${formatter(f, from)} → ${formatter(f, to)}`; } return `${label}: ${formatter(f, to)}`; }) .join(", "); } function truncateList(names: string[], max = 3): string { if (names.length <= max) return names.join(", "); return `${names.slice(0, max).join(", ")} +${names.length - max} more`; } function formatSeconds(seconds: number): string { const m = Math.floor(Math.abs(seconds) / 60); const s = Math.abs(seconds) % 60; const sign = seconds < 0 ? "-" : "+"; if (m === 0) return `${sign}${s}s`; return `${sign}${m}m ${s}s`; } function formatTimeRemaining(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${String(s).padStart(2, "0")}`; } /** * Returns a concise human-readable summary of an audit log entry. * Shared between the league home page summary widget and the full audit log page. */ export function formatAuditDetail(entry: AuditLogEntry): string { const d = (entry.details ?? {}) as Record; switch (entry.action) { case "draft_rollback": return `Rolled back to pick #${d.rolledBackToPickNumber}`; case "time_bank_edited": { const adj = Number(d.adjustment ?? 0); const remaining = Number(d.newTimeRemaining ?? 0); return `${formatSeconds(adj)} for ${d.teamName ?? "team"} (now ${formatTimeRemaining(remaining)})`; } case "draft_pick_changed": return `Pick #${d.pickNumber}: ${d.oldParticipantName} → ${d.newParticipantName}`; case "force_autopick": return `Auto-picked ${d.participantName} for ${d.teamName} at pick #${d.pickNumber}`; case "force_manual_pick": return `Picked ${d.participantName} for ${d.teamName} at pick #${d.pickNumber}`; case "draft_order_set": return "Draft order manually set"; case "draft_order_randomized": return "Draft order randomized"; case "draft_reset": { const prev = d.previousPickNumber; return prev !== null && prev !== undefined ? `Draft reset (was at pick #${prev})` : "Draft reset"; } case "draft_started": return "Draft started"; case "draft_paused": return d.pickNumber !== null && d.pickNumber !== undefined ? `Draft paused at pick #${d.pickNumber}` : "Draft paused"; case "draft_resumed": return d.pickNumber !== null && d.pickNumber !== undefined ? `Draft resumed at pick #${d.pickNumber}` : "Draft resumed"; case "league_settings_changed": { const fields = (d.changedFields as string[] | undefined) ?? []; const prev = (d.previousValues as Record | undefined) ?? {}; const next = (d.newValues as Record | undefined) ?? {}; const detail = diffLines(fields, prev, next, LEAGUE_FIELD_LABELS, formatLeagueField); return detail || "League settings updated"; } case "draft_settings_changed": { const fields = (d.changedFields as string[] | undefined) ?? []; const prev = (d.previousValues as Record | undefined) ?? {}; const next = (d.newValues as Record | undefined) ?? {}; const detail = diffLines(fields, prev, next, DRAFT_FIELD_LABELS, formatDraftField); return detail || "Draft settings updated"; } case "scoring_rules_changed": { const fields = (d.changedFields as string[] | undefined) ?? []; const prev = (d.previousValues as Record | undefined) ?? {}; const next = (d.newValues as Record | undefined) ?? {}; const detail = diffLines(fields, prev, next, SCORING_FIELD_LABELS, (_, v) => String(v ?? "")); return detail ? `Scoring updated: ${detail}` : "Scoring rules updated"; } case "sports_changed": { const added = (d.added as string[] | undefined) ?? []; const removed = (d.removed as string[] | undefined) ?? []; const parts: string[] = []; if (added.length > 0) parts.push(`Added: ${truncateList(added)}`); if (removed.length > 0) parts.push(`Removed: ${truncateList(removed)}`); return parts.length > 0 ? parts.join(" | ") : "Sports updated"; } case "brackt_resolved": return d.trigger === "auto" ? "Brackt resolved automatically" : "Brackt resolution retried"; default: return (entry.action as string).replace(/_/g, " "); } }