Fix First Four appearing out of order in NCAA bracket display

Use bracket template's canonical round order instead of sorting by match
count, which placed First Four (4 matches) after Elite Eight (also 4
matches). Extract getOrderedRoundsFromMatches() into bracket-templates.ts
to eliminate identical logic duplicated in the admin bracket component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-16 12:25:03 -07:00
parent 7f9b564a5d
commit b4b4fc7e06
3 changed files with 33 additions and 20 deletions

View file

@ -582,6 +582,32 @@ export function getAllBracketTemplates(): BracketTemplate[] {
return Object.values(BRACKET_TEMPLATES);
}
/**
* Returns round names in chronological order (earliest first) for the given matches.
* When a template is provided its defined order is used; otherwise rounds are sorted
* by descending match count (more matches = earlier round).
*/
export function getOrderedRoundsFromMatches(
matches: Array<{ round: string }>,
template?: BracketTemplate
): string[] {
const roundsInMatches = new Set(matches.map((m) => m.round));
if (template) {
return template.rounds
.map((r) => r.name)
.filter((name) => roundsInMatches.has(name));
}
const roundMatchCounts = new Map<string, number>();
for (const m of matches) {
roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1);
}
return Array.from(roundMatchCounts.keys()).sort(
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
);
}
/**
* Helper to determine scoring round type based on match count in scoring rounds
* Used for determining placement sharing (5-8th, 3-4th, 1-2nd)

View file

@ -22,7 +22,7 @@ import {
SelectValue,
} from "~/components/ui/select";
import { ArrowLeft, Plus, Trophy, X, Check, ChevronDown, ChevronRight, Calendar, TrendingUp, Trash2 } from "lucide-react";
import { getAllBracketTemplates, getBracketTemplate, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
import { getAllBracketTemplates, getBracketTemplate, getOrderedRoundsFromMatches, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
import {
Table,
TableBody,
@ -277,18 +277,8 @@ export default function EventBracket({
// Memoized: Get available rounds in chronological order
const availableRounds = useMemo(() => {
const roundsInMatches = Array.from(new Set(matches.map(m => m.round)));
if (event.bracketTemplateId) {
const eventTemplate = getBracketTemplate(event.bracketTemplateId);
if (eventTemplate) {
return eventTemplate.rounds
.map((r) => r.name)
.filter((roundName: string) => roundsInMatches.includes(roundName));
}
}
return roundsInMatches;
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined;
return getOrderedRoundsFromMatches(matches, template);
}, [matches, event.bracketTemplateId]);
// Check if all matches are complete

View file

@ -14,6 +14,7 @@ import {
getUpcomingScoringEvents,
getRecentCompletedEvents,
} from "~/models/scoring-event";
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, inArray } from "drizzle-orm";
@ -160,13 +161,9 @@ export async function loader(args: Route.LoaderArgs) {
});
playoffMatches = matches;
const roundMatchCounts = new Map<string, number>();
for (const m of matches) {
roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1);
}
playoffRounds = Array.from(roundMatchCounts.keys()).sort(
(a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0)
);
const templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId;
const template = templateId ? getBracketTemplate(templateId) : undefined;
playoffRounds = getOrderedRoundsFromMatches(matches, template);
}
// Fetch group-stage losers and all participant results for bracket scoring