Commissioners can add Brackt to any league. Each team drafts one manager from their own league; at season end, that manager's final overall fantasy ranking earns placement points. Resolution fires automatically when all other sports finalize. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
211 lines
7.3 KiB
TypeScript
211 lines
7.3 KiB
TypeScript
import { format } from "date-fns";
|
|
import type { AuditLogEntry } from "~/models/audit-log";
|
|
|
|
export const AUDIT_ACTION_LABELS: Record<string, string> = {
|
|
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<string, string> = {
|
|
pointsFor1st: "1st",
|
|
pointsFor2nd: "2nd",
|
|
pointsFor3rd: "3rd",
|
|
pointsFor4th: "4th",
|
|
pointsFor5th: "5th",
|
|
pointsFor6th: "6th",
|
|
pointsFor7th: "7th",
|
|
pointsFor8th: "8th",
|
|
};
|
|
|
|
const DRAFT_FIELD_LABELS: Record<string, string> = {
|
|
draftRounds: "Rounds",
|
|
draftDateTime: "Draft date",
|
|
draftInitialTime: "Initial time",
|
|
draftIncrementTime: "Increment",
|
|
draftTimerMode: "Timer mode",
|
|
};
|
|
|
|
const LEAGUE_FIELD_LABELS: Record<string, string> = {
|
|
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<string, unknown>,
|
|
next: Record<string, unknown>,
|
|
labels: Record<string, string>,
|
|
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<string, unknown>;
|
|
|
|
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<string, unknown> | undefined) ?? {};
|
|
const next = (d.newValues as Record<string, unknown> | 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<string, unknown> | undefined) ?? {};
|
|
const next = (d.newValues as Record<string, unknown> | 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<string, unknown> | undefined) ?? {};
|
|
const next = (d.newValues as Record<string, unknown> | 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, " ");
|
|
}
|
|
}
|