regionals
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { tournamentInputSchema } from "@/features/tournaments/types";
|
||||
import { tournamentInputSchema, GroupStanding } from "@/features/tournaments/types";
|
||||
import { logger } from ".";
|
||||
import { z } from "zod";
|
||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||
import { fa } from "zod/v4/locales";
|
||||
import brackets from "@/features/bracket/utils";
|
||||
import { MatchInput } from "@/features/matches/types";
|
||||
import { generateSingleEliminationBracket } from "./utils/bracket-generator";
|
||||
|
||||
export const listTournaments = createServerFn()
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
@@ -384,6 +387,215 @@ export const confirmTeamAssignments = createServerFn()
|
||||
})
|
||||
);
|
||||
|
||||
async function calculateGroupStandings(groupId: string): Promise<GroupStanding[]> {
|
||||
const group = await pbAdmin.getGroup(groupId);
|
||||
if (!group) {
|
||||
throw new Error("Group not found");
|
||||
}
|
||||
|
||||
const matches = await pbAdmin.getMatchesByGroup(groupId);
|
||||
const completedMatches = matches.filter(m => m.status === "ended");
|
||||
|
||||
const standings = new Map<string, GroupStanding>();
|
||||
|
||||
for (const team of group.teams || []) {
|
||||
standings.set(team.id, {
|
||||
team,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
cups_for: 0,
|
||||
cups_against: 0,
|
||||
cup_differential: 0,
|
||||
rank: 0,
|
||||
});
|
||||
}
|
||||
|
||||
for (const match of completedMatches) {
|
||||
if (!match.home || !match.away) continue;
|
||||
|
||||
const homeStanding = standings.get(match.home.id);
|
||||
const awayStanding = standings.get(match.away.id);
|
||||
|
||||
if (homeStanding && awayStanding) {
|
||||
homeStanding.cups_for += match.home_cups;
|
||||
homeStanding.cups_against += match.away_cups;
|
||||
awayStanding.cups_for += match.away_cups;
|
||||
awayStanding.cups_against += match.home_cups;
|
||||
|
||||
if (match.home_cups > match.away_cups) {
|
||||
homeStanding.wins++;
|
||||
awayStanding.losses++;
|
||||
} else {
|
||||
awayStanding.wins++;
|
||||
homeStanding.losses++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const standing of standings.values()) {
|
||||
standing.cup_differential = standing.cups_for - standing.cups_against;
|
||||
}
|
||||
|
||||
const sortedStandings = Array.from(standings.values()).sort((a, b) => {
|
||||
if (b.wins !== a.wins) return b.wins - a.wins;
|
||||
if (b.cup_differential !== a.cup_differential) return b.cup_differential - a.cup_differential;
|
||||
return b.cups_for - a.cups_for;
|
||||
});
|
||||
|
||||
sortedStandings.forEach((standing, index) => {
|
||||
standing.rank = index + 1;
|
||||
});
|
||||
|
||||
return sortedStandings;
|
||||
}
|
||||
|
||||
export const getGroupStandings = createServerFn()
|
||||
.inputValidator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: groupId }) =>
|
||||
toServerResult(() => calculateGroupStandings(groupId))
|
||||
);
|
||||
|
||||
export const generateKnockoutBracket = createServerFn()
|
||||
.inputValidator(z.object({
|
||||
tournamentId: z.string(),
|
||||
}))
|
||||
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult(async () => {
|
||||
logger.info('Generating knockout bracket', {
|
||||
tournamentId: data.tournamentId,
|
||||
});
|
||||
|
||||
const tournament = await pbAdmin.getTournament(data.tournamentId);
|
||||
if (!tournament) {
|
||||
throw new Error("Tournament not found");
|
||||
}
|
||||
|
||||
if (tournament.phase !== "group_stage") {
|
||||
throw new Error("Tournament must be in group_stage phase to generate knockout bracket");
|
||||
}
|
||||
|
||||
if (!tournament.group_config) {
|
||||
throw new Error("Tournament must have group_config");
|
||||
}
|
||||
|
||||
const groups = await pbAdmin.getGroupsByTournament(data.tournamentId);
|
||||
if (!groups || groups.length === 0) {
|
||||
throw new Error("No groups found for tournament");
|
||||
}
|
||||
|
||||
const qualifiedTeams: { teamId: string; groupOrder: number; rank: number }[] = [];
|
||||
|
||||
for (const group of groups) {
|
||||
const standings = await calculateGroupStandings(group.id);
|
||||
|
||||
const topTeams = standings.slice(0, tournament.group_config.advance_per_group);
|
||||
for (const standing of topTeams) {
|
||||
qualifiedTeams.push({
|
||||
teamId: standing.team.id,
|
||||
groupOrder: group.order,
|
||||
rank: standing.rank,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const orderedTeamIds: string[] = [];
|
||||
const maxRank = tournament.group_config.advance_per_group;
|
||||
|
||||
for (let rank = 1; rank <= maxRank; rank++) {
|
||||
const teamsAtRank = qualifiedTeams
|
||||
.filter(t => t.rank === rank)
|
||||
.sort((a, b) => a.groupOrder - b.groupOrder);
|
||||
orderedTeamIds.push(...teamsAtRank.map(t => t.teamId));
|
||||
}
|
||||
|
||||
const teamCount = orderedTeamIds.length;
|
||||
|
||||
let bracketTemplate: any;
|
||||
if (Object.keys(brackets).includes(teamCount.toString())) {
|
||||
bracketTemplate = brackets[teamCount as keyof typeof brackets];
|
||||
} else {
|
||||
bracketTemplate = generateSingleEliminationBracket(teamCount);
|
||||
}
|
||||
|
||||
const seedToTeamId = new Map<number, string>();
|
||||
orderedTeamIds.forEach((teamId, index) => {
|
||||
seedToTeamId.set(index + 1, teamId);
|
||||
});
|
||||
|
||||
const matchInputs: MatchInput[] = [];
|
||||
let matchLid = 1000;
|
||||
|
||||
bracketTemplate.winners.forEach((round: any[]) => {
|
||||
round.forEach((match: any) => {
|
||||
const matchInput: MatchInput = {
|
||||
lid: matchLid++,
|
||||
round: match.round,
|
||||
order: match.order || 0,
|
||||
reset: false,
|
||||
bye: match.bye || false,
|
||||
home_cups: 0,
|
||||
away_cups: 0,
|
||||
ot_count: 0,
|
||||
home_from_lid: match.home_from_lid === null ? -1 : (match.home_from_lid + 1000),
|
||||
away_from_lid: match.away_from_lid === null ? -1 : (match.away_from_lid + 1000),
|
||||
home_from_loser: false,
|
||||
away_from_loser: false,
|
||||
is_losers_bracket: false,
|
||||
match_type: "knockout",
|
||||
status: "tbd",
|
||||
tournament: data.tournamentId,
|
||||
};
|
||||
|
||||
if (match.home_seed) {
|
||||
const teamId = seedToTeamId.get(match.home_seed);
|
||||
if (teamId) {
|
||||
matchInput.home = teamId;
|
||||
matchInput.home_seed = match.home_seed;
|
||||
}
|
||||
}
|
||||
|
||||
if (match.away_seed) {
|
||||
const teamId = seedToTeamId.get(match.away_seed);
|
||||
if (teamId) {
|
||||
matchInput.away = teamId;
|
||||
matchInput.away_seed = match.away_seed;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchInput.home && matchInput.away) {
|
||||
matchInput.status = "ready";
|
||||
}
|
||||
|
||||
matchInputs.push(matchInput);
|
||||
});
|
||||
});
|
||||
|
||||
const createdMatches = await pbAdmin.createMatches(matchInputs);
|
||||
|
||||
const existingMatchIds = tournament.matches?.map(m => m.id) || [];
|
||||
const newMatchIds = createdMatches.map(m => m.id);
|
||||
await pbAdmin.updateTournamentMatches(data.tournamentId, [...existingMatchIds, ...newMatchIds]);
|
||||
|
||||
await pbAdmin.updateTournament(data.tournamentId, {
|
||||
phase: "knockout"
|
||||
});
|
||||
|
||||
logger.info('Knockout bracket generated', {
|
||||
tournamentId: data.tournamentId,
|
||||
matchCount: createdMatches.length,
|
||||
qualifiedTeamCount: qualifiedTeams.length
|
||||
});
|
||||
|
||||
return {
|
||||
tournament,
|
||||
matchCount: createdMatches.length,
|
||||
matches: createdMatches
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const adminEnrollPlayer = createServerFn()
|
||||
.inputValidator(z.object({
|
||||
playerId: z.string(),
|
||||
@@ -448,8 +660,7 @@ export const generateGroupStage = createServerFn()
|
||||
await pbAdmin.deleteGroupsByTournament(data.tournamentId);
|
||||
|
||||
const createdGroups = [];
|
||||
const allMatches = [];
|
||||
let matchLid = 1;
|
||||
const groupStageMatches = [];
|
||||
|
||||
for (const assignment of data.teamAssignments) {
|
||||
const group = await pbAdmin.createGroup({
|
||||
@@ -464,10 +675,10 @@ export const generateGroupStage = createServerFn()
|
||||
const teamIds = assignment.teamIds;
|
||||
for (let i = 0; i < teamIds.length; i++) {
|
||||
for (let j = i + 1; j < teamIds.length; j++) {
|
||||
allMatches.push({
|
||||
lid: matchLid++,
|
||||
groupStageMatches.push({
|
||||
lid: -1,
|
||||
round: -1,
|
||||
order: allMatches.length,
|
||||
order: groupStageMatches.length + 1,
|
||||
reset: false,
|
||||
bye: false,
|
||||
home: teamIds[i],
|
||||
@@ -489,6 +700,43 @@ export const generateGroupStage = createServerFn()
|
||||
}
|
||||
}
|
||||
|
||||
const knockoutTeamCount = data.groupConfig.num_groups * data.groupConfig.advance_per_group;
|
||||
let bracketTemplate: any;
|
||||
|
||||
if (Object.keys(brackets).includes(knockoutTeamCount.toString())) {
|
||||
bracketTemplate = brackets[knockoutTeamCount as keyof typeof brackets];
|
||||
} else {
|
||||
bracketTemplate = generateSingleEliminationBracket(knockoutTeamCount);
|
||||
}
|
||||
|
||||
const knockoutMatches: any[] = [];
|
||||
|
||||
bracketTemplate.winners.forEach((round: any[]) => {
|
||||
round.forEach((match: any) => {
|
||||
knockoutMatches.push({
|
||||
lid: match.lid,
|
||||
round: match.round,
|
||||
order: match.order,
|
||||
reset: false,
|
||||
bye: match.bye || false,
|
||||
home_seed: match.home_seed,
|
||||
away_seed: match.away_seed,
|
||||
home_cups: 0,
|
||||
away_cups: 0,
|
||||
ot_count: 0,
|
||||
home_from_lid: match.home_from_lid !== null ? match.home_from_lid : -1,
|
||||
away_from_lid: match.away_from_lid !== null ? match.away_from_lid : -1,
|
||||
home_from_loser: false,
|
||||
away_from_loser: false,
|
||||
is_losers_bracket: false,
|
||||
match_type: "knockout" as const,
|
||||
status: "tbd" as const,
|
||||
tournament: data.tournamentId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const allMatches = [...groupStageMatches, ...knockoutMatches];
|
||||
const createdMatches = await pbAdmin.createMatches(allMatches);
|
||||
|
||||
const matchIds = createdMatches.map((match) => match.id);
|
||||
@@ -499,10 +747,12 @@ export const generateGroupStage = createServerFn()
|
||||
group_config: data.groupConfig
|
||||
});
|
||||
|
||||
logger.info('Group stage generated', {
|
||||
logger.info('Group stage and knockout bracket generated', {
|
||||
tournamentId: data.tournamentId,
|
||||
groupCount: createdGroups.length,
|
||||
matchCount: createdMatches.length
|
||||
groupMatchCount: groupStageMatches.length,
|
||||
knockoutMatchCount: knockoutMatches.length,
|
||||
totalMatchCount: createdMatches.length
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user