feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
|
|
|
|
import { redirect } from "react-router";
|
|
|
|
|
import { findSportsSeasonById } from "~/models/sports-season";
|
|
|
|
|
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
2025-11-04 22:09:44 -08:00
|
|
|
import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event";
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
import {
|
|
|
|
|
findPlayoffMatchesByEventId,
|
2025-11-04 22:09:44 -08:00
|
|
|
generateBracketFromTemplate,
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
setMatchWinner,
|
2025-11-04 22:09:44 -08:00
|
|
|
advanceWinnerTemplate,
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
findPlayoffMatchById,
|
2026-02-14 22:30:12 -08:00
|
|
|
assignParticipantsToKnockout,
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
} from "~/models/playoff-match";
|
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
|
|
|
import {
|
|
|
|
|
createGame,
|
|
|
|
|
updateGame,
|
|
|
|
|
deleteGame,
|
|
|
|
|
type PlayoffMatchGameStatus,
|
|
|
|
|
} from "~/models/playoff-match-game";
|
|
|
|
|
import {
|
|
|
|
|
upsertMatchOdds,
|
|
|
|
|
deleteOddsForParticipant,
|
|
|
|
|
} from "~/models/playoff-match-odds";
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
import {
|
|
|
|
|
processPlayoffEvent,
|
|
|
|
|
processMatchResult,
|
|
|
|
|
recalculateAffectedLeagues,
|
|
|
|
|
} from "~/models/scoring-calculator";
|
|
|
|
|
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
2026-03-15 21:52:47 -07:00
|
|
|
import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
|
2026-02-14 22:30:12 -08:00
|
|
|
import {
|
|
|
|
|
createGroupsForEvent,
|
|
|
|
|
addMembersToGroup,
|
|
|
|
|
findGroupsByEventId,
|
|
|
|
|
toggleMemberEliminated,
|
|
|
|
|
getAdvancingParticipantIds,
|
|
|
|
|
getEliminatedParticipantIds,
|
|
|
|
|
} from "~/models/tournament-group";
|
2025-11-03 13:16:37 -08:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2025-11-21 22:05:50 -08:00
|
|
|
import { eq, and } from "drizzle-orm";
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
|
|
|
|
export async function loader({ params }: Route.LoaderArgs) {
|
|
|
|
|
const sportsSeason = await findSportsSeasonById(params.id);
|
|
|
|
|
|
|
|
|
|
if (!sportsSeason) {
|
|
|
|
|
throw new Response("Sports season not found", { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
|
|
|
|
|
if (!event) {
|
|
|
|
|
throw new Response("Event not found", { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.eventType !== "playoff_game") {
|
|
|
|
|
throw new Response("This event is not a playoff event", { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const participants = await findParticipantsBySportsSeasonId(params.id);
|
|
|
|
|
const matches = await findPlayoffMatchesByEventId(params.eventId);
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
// Fetch tournament groups if this event has a group-stage template
|
|
|
|
|
const tournamentGroups = await findGroupsByEventId(params.eventId);
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
// The matches already include participant relations from the model query
|
|
|
|
|
return {
|
|
|
|
|
sportsSeason: sportsSeason as typeof sportsSeason & {
|
|
|
|
|
sport: { id: string; name: string; type: string; slug: string };
|
|
|
|
|
},
|
|
|
|
|
event,
|
|
|
|
|
participants,
|
|
|
|
|
matches: matches as Array<typeof matches[0] & {
|
|
|
|
|
participant1: { id: string; name: string } | null;
|
|
|
|
|
participant2: { id: string; name: string } | null;
|
|
|
|
|
winner: { id: string; name: string } | null;
|
|
|
|
|
loser: { id: string; name: string } | null;
|
|
|
|
|
}>,
|
2026-02-14 22:30:12 -08:00
|
|
|
tournamentGroups,
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function action({ request, params }: Route.ActionArgs) {
|
|
|
|
|
const formData = await request.formData();
|
|
|
|
|
const intent = formData.get("intent");
|
|
|
|
|
|
|
|
|
|
if (intent === "generate-bracket") {
|
2025-11-04 22:09:44 -08:00
|
|
|
const templateId = formData.get("templateId");
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
if (typeof templateId !== "string" || !templateId) {
|
|
|
|
|
return { error: "Template ID is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const template = getBracketTemplate(templateId);
|
|
|
|
|
if (!template) {
|
|
|
|
|
return { error: "Invalid bracket template" };
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-15 21:52:47 -07:00
|
|
|
// Parse per-event region config for NCAA-style brackets
|
|
|
|
|
let regionOverride: BracketRegion[] | undefined;
|
|
|
|
|
if (template.regions && template.regions.length > 0) {
|
|
|
|
|
const regions: BracketRegion[] = [];
|
|
|
|
|
for (let r = 0; r < template.regions.length; r++) {
|
|
|
|
|
const name = (formData.get(`regionName${r}`) as string | null)?.trim()
|
|
|
|
|
|| `Region ${r + 1}`;
|
|
|
|
|
const seedsRaw = (formData.get(`regionPlayInSeeds${r}`) as string | null) || "";
|
|
|
|
|
const playInSeeds = seedsRaw
|
|
|
|
|
.split(",")
|
|
|
|
|
.map((s) => parseInt(s.trim(), 10))
|
|
|
|
|
.filter((n) => !isNaN(n) && n >= 1 && n <= 16);
|
|
|
|
|
const playInSet = new Set(playInSeeds);
|
|
|
|
|
regions.push({
|
|
|
|
|
name,
|
|
|
|
|
directSeeds: ALL_16_SEEDS.filter((s) => !playInSet.has(s)),
|
|
|
|
|
playIns: playInSeeds.map((s) => ({ seedSlot: s, teams: 2 as const })),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// Validate total = template.totalTeams
|
|
|
|
|
const total = regions.reduce(
|
|
|
|
|
(sum, r) => sum + r.directSeeds.length + r.playIns.length * 2,
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
if (total !== template.totalTeams) {
|
|
|
|
|
return {
|
|
|
|
|
error: `Region config accounts for ${total} team slots but template requires ${template.totalTeams}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
regionOverride = regions;
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
const participantIds: string[] = [];
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
for (let i = 0; i < template.totalTeams; i++) {
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
const participantId = formData.get(`participant${i}`);
|
|
|
|
|
if (typeof participantId !== "string" || !participantId) {
|
|
|
|
|
return { error: `Participant ${i + 1} is required` };
|
|
|
|
|
}
|
|
|
|
|
participantIds.push(participantId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for duplicates
|
|
|
|
|
const uniqueParticipants = new Set(participantIds);
|
|
|
|
|
if (uniqueParticipants.size !== participantIds.length) {
|
|
|
|
|
return { error: "Each participant can only be selected once" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-15 21:52:47 -07:00
|
|
|
await generateBracketFromTemplate(params.eventId, templateId, participantIds, regionOverride);
|
2025-11-04 22:09:44 -08:00
|
|
|
|
2025-11-21 22:05:50 -08:00
|
|
|
// PHASE 5.3: Mark participants NOT in the bracket as eliminated
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (event) {
|
|
|
|
|
// Get all participants in the sports season
|
|
|
|
|
const allParticipants = await findParticipantsBySportsSeasonId(params.id);
|
|
|
|
|
|
|
|
|
|
// Create a set of participants in the bracket for fast lookup
|
|
|
|
|
const participantsInBracket = new Set(participantIds);
|
|
|
|
|
|
|
|
|
|
// Mark participants NOT in the bracket as eliminated
|
|
|
|
|
const { setParticipantResult } = await import("~/models/participant-result");
|
|
|
|
|
for (const participant of allParticipants) {
|
|
|
|
|
if (!participantsInBracket.has(participant.id)) {
|
|
|
|
|
await setParticipantResult(
|
|
|
|
|
participant.id,
|
|
|
|
|
event.sportsSeasonId,
|
|
|
|
|
0 // 0 = didn't make playoffs, eliminated
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[BracketGeneration] Marked ${allParticipants.length - participantIds.length} participants as eliminated`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 21:52:47 -07:00
|
|
|
// Update the event to store the template ID, scoring start round, and region config
|
2025-11-04 22:09:44 -08:00
|
|
|
await updateScoringEvent(params.eventId, {
|
|
|
|
|
bracketTemplateId: templateId,
|
|
|
|
|
scoringStartsAtRound: template.scoringStartsAtRound,
|
2026-03-15 21:52:47 -07:00
|
|
|
bracketRegionConfig: regionOverride,
|
2025-11-04 22:09:44 -08:00
|
|
|
});
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
return { success: "Bracket generated successfully" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error generating bracket:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error
|
|
|
|
|
? error.message
|
|
|
|
|
: "Failed to generate bracket",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "set-winner") {
|
|
|
|
|
const matchId = formData.get("matchId");
|
|
|
|
|
const winnerId = formData.get("winnerId");
|
|
|
|
|
|
|
|
|
|
if (typeof matchId !== "string" || !matchId) {
|
|
|
|
|
return { error: "Match ID is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof winnerId !== "string" || !winnerId) {
|
|
|
|
|
return { error: "Winner ID is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const match = await findPlayoffMatchById(matchId);
|
|
|
|
|
if (!match) {
|
|
|
|
|
return { error: "Match not found" };
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
// Get the event to determine the template
|
|
|
|
|
const event = await getScoringEventById(match.scoringEventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
// Determine loser
|
|
|
|
|
const loserId =
|
|
|
|
|
match.participant1Id === winnerId
|
|
|
|
|
? match.participant2Id
|
|
|
|
|
: match.participant1Id;
|
|
|
|
|
|
|
|
|
|
if (!loserId) {
|
|
|
|
|
return { error: "Could not determine loser" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the winner
|
|
|
|
|
await setMatchWinner(matchId, winnerId, loserId);
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
// Try to advance the winner to the next round using template
|
|
|
|
|
if (event.bracketTemplateId) {
|
|
|
|
|
const template = getBracketTemplate(event.bracketTemplateId);
|
|
|
|
|
if (template) {
|
|
|
|
|
try {
|
|
|
|
|
await advanceWinnerTemplate(matchId, winnerId, template);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Only ignore "already filled" errors, re-throw unexpected errors
|
|
|
|
|
if (
|
|
|
|
|
error instanceof Error &&
|
|
|
|
|
error.message.includes("already filled")
|
|
|
|
|
) {
|
|
|
|
|
console.warn("Match already filled, skipping advancement");
|
|
|
|
|
} else {
|
|
|
|
|
throw error; // Re-throw unexpected errors
|
|
|
|
|
}
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
// Immediately score this match: loser gets their final placement,
|
|
|
|
|
// winner gets provisional floor points (isPartialScore=true).
|
|
|
|
|
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
|
|
|
|
|
await processMatchResult({
|
|
|
|
|
round: match.round,
|
|
|
|
|
winnerId,
|
|
|
|
|
loserId,
|
|
|
|
|
isScoring: match.isScoring ?? true,
|
|
|
|
|
sportsSeasonId: event.sportsSeasonId,
|
|
|
|
|
bracketTemplateId: event.bracketTemplateId,
|
2026-03-17 11:16:36 -07:00
|
|
|
eventId: event.id,
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
eventName: event.name ?? undefined,
|
|
|
|
|
});
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
return { success: "Winner set successfully" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error setting winner:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to set winner",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
if (intent === "set-round-winners") {
|
|
|
|
|
const round = formData.get("round");
|
|
|
|
|
|
|
|
|
|
if (typeof round !== "string" || !round) {
|
|
|
|
|
return { error: "Round is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get all winner assignments from form data
|
|
|
|
|
const winnerAssignments: Array<{ matchId: string; winnerId: string }> = [];
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of formData.entries()) {
|
|
|
|
|
if (key.startsWith("winner-") && typeof value === "string") {
|
|
|
|
|
const matchId = key.replace("winner-", "");
|
|
|
|
|
winnerAssignments.push({ matchId, winnerId: value });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (winnerAssignments.length === 0) {
|
|
|
|
|
return { error: "No winners selected" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the event to determine the template
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
|
|
|
|
|
|
|
|
|
// Process each winner assignment
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const { matchId, winnerId } of winnerAssignments) {
|
|
|
|
|
try {
|
|
|
|
|
const match = await findPlayoffMatchById(matchId);
|
|
|
|
|
if (!match) {
|
|
|
|
|
errors.push(`Match ${matchId} not found`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
// Guard: ensure the match actually belongs to the submitted round.
|
|
|
|
|
if (match.round !== round) {
|
|
|
|
|
errors.push(`Match ${match.matchNumber} is in round "${match.round}", not "${round}"`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
// Determine loser
|
|
|
|
|
const loserId =
|
|
|
|
|
match.participant1Id === winnerId
|
|
|
|
|
? match.participant2Id
|
|
|
|
|
: match.participant1Id;
|
|
|
|
|
|
|
|
|
|
if (!loserId) {
|
|
|
|
|
errors.push(`Could not determine loser for match ${match.matchNumber}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the winner
|
|
|
|
|
await setMatchWinner(matchId, winnerId, loserId);
|
|
|
|
|
|
|
|
|
|
// Try to advance the winner to the next round using template
|
|
|
|
|
if (template) {
|
|
|
|
|
try {
|
|
|
|
|
await advanceWinnerTemplate(matchId, winnerId, template);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Only ignore "already filled" errors
|
|
|
|
|
if (
|
|
|
|
|
error instanceof Error &&
|
|
|
|
|
error.message.includes("already filled")
|
|
|
|
|
) {
|
|
|
|
|
console.warn("Match already filled, skipping advancement");
|
|
|
|
|
} else {
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
// Score this match without triggering standings/probability recalc yet —
|
|
|
|
|
// we batch those side effects into a single call after the loop.
|
|
|
|
|
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
|
|
|
|
|
await processMatchResult({
|
|
|
|
|
round: match.round,
|
|
|
|
|
winnerId,
|
|
|
|
|
loserId,
|
|
|
|
|
isScoring: match.isScoring ?? true,
|
|
|
|
|
sportsSeasonId: event.sportsSeasonId,
|
|
|
|
|
bracketTemplateId: event.bracketTemplateId,
|
|
|
|
|
eventName: event.name ?? undefined,
|
|
|
|
|
skipSideEffects: true,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
successCount++;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error setting winner for match ${matchId}:`, error);
|
|
|
|
|
errors.push(
|
|
|
|
|
`Match ${matchId}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
// Run side effects once for the whole batch (avoids N redundant recalcs).
|
|
|
|
|
if (successCount > 0) {
|
|
|
|
|
const db = database();
|
2026-03-17 11:16:36 -07:00
|
|
|
await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined });
|
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
|
|
|
try {
|
|
|
|
|
await updateProbabilitiesAfterResult(event.sportsSeasonId, true);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error updating probabilities after batch round winners:`, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
if (errors.length > 0 && successCount === 0) {
|
|
|
|
|
return { error: `Failed to set winners: ${errors.join(", ")}` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
return {
|
|
|
|
|
success: `Set ${successCount} winner(s) successfully`,
|
|
|
|
|
error: `Some errors occurred: ${errors.join(", ")}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { success: `Successfully set ${successCount} winner(s) for ${round}` };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error setting round winners:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to set winners",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
if (intent === "complete-round") {
|
|
|
|
|
const round = formData.get("round");
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
if (typeof round !== "string" || !round) {
|
|
|
|
|
return { error: "Round is required" };
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Get the event and update playoffRound to the completed round
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify all matches in this round are complete
|
|
|
|
|
const matches = await findPlayoffMatchesByEventId(params.eventId);
|
2025-11-04 22:09:44 -08:00
|
|
|
|
|
|
|
|
// Validate that the round exists in this event's matches
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
const roundMatches = matches.filter((m) => m.round === round);
|
2025-11-04 22:09:44 -08:00
|
|
|
if (roundMatches.length === 0) {
|
|
|
|
|
return { error: `Round "${round}" not found in this bracket` };
|
|
|
|
|
}
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
|
|
|
|
const allComplete = roundMatches.every((m) => m.isComplete);
|
|
|
|
|
if (!allComplete) {
|
|
|
|
|
return { error: `Not all matches in ${round} are complete` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate round order: ensure previous rounds are complete
|
|
|
|
|
const { findParticipantResultsBySportsSeasonId } = await import(
|
|
|
|
|
"~/models/participant-result"
|
|
|
|
|
);
|
|
|
|
|
const existingResults = await findParticipantResultsBySportsSeasonId(
|
|
|
|
|
params.id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (round === "Finals") {
|
|
|
|
|
// Finals requires Semifinals to be complete (3rd/4th place results exist)
|
|
|
|
|
const hasSemifinalResults = existingResults.some(
|
|
|
|
|
(r) => r.finalPosition === 3 || r.finalPosition === 4
|
|
|
|
|
);
|
|
|
|
|
if (!hasSemifinalResults) {
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
"Semifinals must be completed before Finals (no 3rd/4th place results found)",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} else if (round === "Semifinals") {
|
|
|
|
|
// Semifinals requires Quarterfinals to be complete if QF matches exist
|
|
|
|
|
const hasQuarterfinals = matches.some((m) => m.round === "Quarterfinals");
|
|
|
|
|
if (hasQuarterfinals) {
|
|
|
|
|
const hasQuarterfinalsResults = existingResults.some(
|
|
|
|
|
(r) =>
|
|
|
|
|
r.finalPosition === 5 ||
|
|
|
|
|
r.finalPosition === 6 ||
|
|
|
|
|
r.finalPosition === 7 ||
|
|
|
|
|
r.finalPosition === 8
|
|
|
|
|
);
|
|
|
|
|
if (!hasQuarterfinalsResults) {
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
"Quarterfinals must be completed before Semifinals (no 5th-8th place results found)",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the sports season to find a fantasy season for scoring rules
|
|
|
|
|
const sportsSeason = await findSportsSeasonById(params.id);
|
|
|
|
|
if (!sportsSeason) {
|
|
|
|
|
return { error: "Sports season not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the playoff event to calculate placements
|
2025-11-03 13:16:37 -08:00
|
|
|
// TODO Phase 2.7: Refactor processPlayoffEvent to use template system
|
|
|
|
|
// For now, we still need to set playoffRound for the old scoring logic
|
|
|
|
|
// even though we removed it from the create event UI
|
|
|
|
|
const db = database();
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.scoringEvents)
|
|
|
|
|
.set({ playoffRound: round, updatedAt: new Date() })
|
|
|
|
|
.where(eq(schema.scoringEvents.id, params.eventId));
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
|
|
|
|
|
// Get a fantasy season ID for scoring calculation
|
|
|
|
|
const { findSeasonSportsBySportsSeasonId } = await import(
|
|
|
|
|
"~/models/season-sport"
|
|
|
|
|
);
|
|
|
|
|
const seasonSports = await findSeasonSportsBySportsSeasonId(params.id);
|
|
|
|
|
if (seasonSports.length === 0) {
|
|
|
|
|
return {
|
|
|
|
|
error: "No fantasy seasons found for this sports season",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process playoff event to update participant results
|
|
|
|
|
await processPlayoffEvent(params.eventId);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: `${round} completed and placements calculated successfully`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error completing round:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to complete round",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 22:05:50 -08:00
|
|
|
if (intent === "reprocess-eliminations") {
|
|
|
|
|
try {
|
|
|
|
|
// Get the event
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all matches in the bracket
|
|
|
|
|
const matches = await findPlayoffMatchesByEventId(params.eventId);
|
|
|
|
|
if (matches.length === 0) {
|
|
|
|
|
return { error: "No bracket exists for this event" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all participants in the sports season
|
|
|
|
|
const allParticipants = await findParticipantsBySportsSeasonId(params.id);
|
|
|
|
|
|
|
|
|
|
// Collect all participant IDs that appear in ANY match
|
|
|
|
|
const participantsInBracket = new Set<string>();
|
|
|
|
|
for (const match of matches) {
|
|
|
|
|
if (match.participant1Id) participantsInBracket.add(match.participant1Id);
|
|
|
|
|
if (match.participant2Id) participantsInBracket.add(match.participant2Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark participants NOT in the bracket as eliminated
|
|
|
|
|
const { setParticipantResult } = await import("~/models/participant-result");
|
|
|
|
|
let eliminatedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const participant of allParticipants) {
|
|
|
|
|
if (!participantsInBracket.has(participant.id)) {
|
|
|
|
|
await setParticipantResult(
|
|
|
|
|
participant.id,
|
|
|
|
|
event.sportsSeasonId,
|
|
|
|
|
0 // 0 = didn't make playoffs, eliminated
|
|
|
|
|
);
|
|
|
|
|
eliminatedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[ReprocessEliminations] Marked ${eliminatedCount} participants as eliminated`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: `Successfully marked ${eliminatedCount} participant(s) as eliminated`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error reprocessing eliminations:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to reprocess eliminations",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 20:01:21 -08:00
|
|
|
if (intent === "finalize-bracket") {
|
|
|
|
|
try {
|
|
|
|
|
// Get the event
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all matches
|
|
|
|
|
const matches = await findPlayoffMatchesByEventId(params.eventId);
|
|
|
|
|
if (matches.length === 0) {
|
|
|
|
|
return { error: "No bracket exists for this event" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get template to determine round order
|
|
|
|
|
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
|
|
|
|
if (!template) {
|
|
|
|
|
return { error: "Bracket template not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify ALL matches are complete
|
|
|
|
|
const incompleteMatches = matches.filter((m) => !m.isComplete);
|
|
|
|
|
if (incompleteMatches.length > 0) {
|
|
|
|
|
return {
|
|
|
|
|
error: `Cannot finalize: ${incompleteMatches.length} match(es) still incomplete`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all participants in this sports season
|
|
|
|
|
const { findParticipantsBySportsSeasonId } = await import("~/models/participant");
|
|
|
|
|
const allParticipants = await findParticipantsBySportsSeasonId(params.id);
|
|
|
|
|
|
|
|
|
|
// Get participants in matches
|
|
|
|
|
const participantsInMatches = new Set(
|
|
|
|
|
matches.flatMap((m) => [m.participant1Id, m.participant2Id].filter(Boolean))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Process all rounds in order
|
|
|
|
|
const db = database();
|
|
|
|
|
for (const round of template.rounds) {
|
|
|
|
|
const roundMatches = matches.filter((m) => m.round === round.name);
|
|
|
|
|
if (roundMatches.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
// Set playoffRound and process this round
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.scoringEvents)
|
|
|
|
|
.set({ playoffRound: round.name, updatedAt: new Date() })
|
|
|
|
|
.where(eq(schema.scoringEvents.id, params.eventId));
|
|
|
|
|
|
|
|
|
|
await processPlayoffEvent(params.eventId, db);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assign 0 points to participants not in the bracket (Q20)
|
|
|
|
|
const { setParticipantResult } = await import("~/models/participant-result");
|
|
|
|
|
for (const participant of allParticipants) {
|
|
|
|
|
if (!participantsInMatches.has(participant.id)) {
|
|
|
|
|
await setParticipantResult(
|
|
|
|
|
participant.id,
|
|
|
|
|
params.id,
|
|
|
|
|
0 // 0 placement = 0 points
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark event as complete
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.scoringEvents)
|
|
|
|
|
.set({ isComplete: true, completedAt: new Date(), updatedAt: new Date() })
|
|
|
|
|
.where(eq(schema.scoringEvents.id, params.eventId));
|
|
|
|
|
|
|
|
|
|
// Recalculate standings for all affected fantasy seasons
|
|
|
|
|
const { recalculateStandings } = await import("~/models/scoring-calculator");
|
|
|
|
|
const { findSeasonSportsBySportsSeasonId } = await import("~/models/season-sport");
|
|
|
|
|
const seasonSports = await findSeasonSportsBySportsSeasonId(params.id);
|
|
|
|
|
|
|
|
|
|
for (const seasonSport of seasonSports) {
|
|
|
|
|
await recalculateStandings(seasonSport.seasonId, db);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: `Bracket finalized! All placements calculated and standings updated.`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error finalizing bracket:", error);
|
|
|
|
|
return {
|
|
|
|
|
error:
|
|
|
|
|
error instanceof Error ? error.message : "Failed to finalize bracket",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
if (intent === "generate-groups") {
|
|
|
|
|
const templateId = formData.get("templateId");
|
|
|
|
|
|
|
|
|
|
if (typeof templateId !== "string" || !templateId) {
|
|
|
|
|
return { error: "Template ID is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const template = getBracketTemplate(templateId);
|
|
|
|
|
if (!template || !template.groupStage) {
|
|
|
|
|
return { error: "Invalid template or template has no group stage" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { groupStage } = template;
|
|
|
|
|
const totalTeams = groupStage.groupCount * groupStage.teamsPerGroup;
|
|
|
|
|
|
|
|
|
|
// Collect participant IDs from form
|
|
|
|
|
const participantIds: string[] = [];
|
|
|
|
|
for (let i = 0; i < totalTeams; i++) {
|
|
|
|
|
const participantId = formData.get(`participant${i}`);
|
|
|
|
|
if (typeof participantId !== "string" || !participantId) {
|
|
|
|
|
return { error: `Participant ${i + 1} is required` };
|
|
|
|
|
}
|
|
|
|
|
participantIds.push(participantId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for duplicates
|
|
|
|
|
const uniqueParticipants = new Set(participantIds);
|
|
|
|
|
if (uniqueParticipants.size !== participantIds.length) {
|
|
|
|
|
return { error: "Each participant can only be selected once" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Update the event with template info
|
|
|
|
|
await updateScoringEvent(params.eventId, {
|
|
|
|
|
bracketTemplateId: templateId,
|
|
|
|
|
scoringStartsAtRound: template.scoringStartsAtRound,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create tournament groups
|
|
|
|
|
const groups = await createGroupsForEvent(params.eventId, groupStage.groupLabels);
|
|
|
|
|
|
|
|
|
|
// Distribute participants into groups (in selection order)
|
|
|
|
|
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
|
|
|
const startIdx = groupIndex * groupStage.teamsPerGroup;
|
|
|
|
|
const groupParticipantIds = participantIds.slice(
|
|
|
|
|
startIdx,
|
|
|
|
|
startIdx + groupStage.teamsPerGroup
|
|
|
|
|
);
|
|
|
|
|
await addMembersToGroup(groups[groupIndex].id, groupParticipantIds);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate the empty knockout bracket structure
|
|
|
|
|
await generateBracketFromTemplate(params.eventId, templateId);
|
|
|
|
|
|
|
|
|
|
return { success: "Groups and knockout bracket structure created successfully" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error generating groups:", error);
|
|
|
|
|
return {
|
|
|
|
|
error: error instanceof Error ? error.message : "Failed to generate groups",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "toggle-elimination") {
|
|
|
|
|
const memberId = formData.get("memberId");
|
|
|
|
|
|
|
|
|
|
if (typeof memberId !== "string" || !memberId) {
|
|
|
|
|
return { error: "Member ID is required" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const updated = await toggleMemberEliminated(memberId);
|
|
|
|
|
return {
|
|
|
|
|
success: updated.eliminated
|
|
|
|
|
? "Team marked as eliminated"
|
|
|
|
|
: "Team reinstated",
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error toggling elimination:", error);
|
|
|
|
|
return {
|
|
|
|
|
error: error instanceof Error ? error.message : "Failed to toggle elimination",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "populate-knockout") {
|
|
|
|
|
try {
|
|
|
|
|
const event = await getScoringEventById(params.eventId);
|
|
|
|
|
if (!event) {
|
|
|
|
|
return { error: "Event not found" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse match assignments from form
|
|
|
|
|
const assignments: Array<{
|
|
|
|
|
matchNumber: number;
|
|
|
|
|
slot: "participant1Id" | "participant2Id";
|
|
|
|
|
participantId: string;
|
|
|
|
|
}> = [];
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of formData.entries()) {
|
|
|
|
|
const matchPattern = /^match-(\d+)-(participant1Id|participant2Id)$/;
|
|
|
|
|
const match = key.match(matchPattern);
|
|
|
|
|
if (match && typeof value === "string" && value) {
|
|
|
|
|
assignments.push({
|
|
|
|
|
matchNumber: parseInt(match[1], 10),
|
|
|
|
|
slot: match[2] as "participant1Id" | "participant2Id",
|
|
|
|
|
participantId: value,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (assignments.length !== 32) {
|
|
|
|
|
return { error: `Expected 32 assignments but got ${assignments.length}` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate all assignments are unique participants
|
|
|
|
|
const assignedParticipantIds = new Set(assignments.map((a) => a.participantId));
|
|
|
|
|
if (assignedParticipantIds.size !== 32) {
|
|
|
|
|
return { error: "All 32 knockout slots must have unique participants" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate all assigned participants are non-eliminated
|
|
|
|
|
const advancingIds = new Set(await getAdvancingParticipantIds(params.eventId));
|
|
|
|
|
for (const participantId of assignedParticipantIds) {
|
|
|
|
|
if (!advancingIds.has(participantId)) {
|
|
|
|
|
return { error: "All assigned participants must be non-eliminated group members" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assign participants to knockout bracket
|
|
|
|
|
await assignParticipantsToKnockout(params.eventId, assignments);
|
|
|
|
|
|
|
|
|
|
// Mark eliminated group participants with finalPosition = 0
|
|
|
|
|
const eliminatedIds = await getEliminatedParticipantIds(params.eventId);
|
|
|
|
|
const { setParticipantResult } = await import("~/models/participant-result");
|
|
|
|
|
for (const participantId of eliminatedIds) {
|
|
|
|
|
await setParticipantResult(participantId, event.sportsSeasonId, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`[PopulateKnockout] Assigned 32 participants to knockout, marked ${eliminatedIds.length} as eliminated`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { success: "Knockout bracket populated successfully" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error populating knockout:", error);
|
|
|
|
|
return {
|
|
|
|
|
error: error instanceof Error ? error.message : "Failed to populate knockout",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
|
|
|
// ── Game scheduling ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
if (intent === "add-game") {
|
|
|
|
|
const matchId = formData.get("matchId");
|
|
|
|
|
const gameNumber = formData.get("gameNumber");
|
|
|
|
|
const scheduledAt = formData.get("scheduledAt");
|
|
|
|
|
const notes = formData.get("notes");
|
|
|
|
|
|
|
|
|
|
if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" };
|
|
|
|
|
if (typeof gameNumber !== "string" || !gameNumber) return { error: "gameNumber is required" };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await createGame({
|
|
|
|
|
playoffMatchId: matchId,
|
|
|
|
|
gameNumber: parseInt(gameNumber, 10),
|
|
|
|
|
scheduledAt: scheduledAt ? new Date(scheduledAt as string) : null,
|
|
|
|
|
notes: notes ? (notes as string) : null,
|
|
|
|
|
});
|
|
|
|
|
return { success: "Game added" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Failed to add game" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "update-game") {
|
|
|
|
|
const gameId = formData.get("gameId");
|
|
|
|
|
const scheduledAt = formData.get("scheduledAt");
|
|
|
|
|
const status = formData.get("status");
|
|
|
|
|
const participant1Score = formData.get("participant1Score");
|
|
|
|
|
const participant2Score = formData.get("participant2Score");
|
|
|
|
|
const winnerId = formData.get("winnerId");
|
|
|
|
|
const notes = formData.get("notes");
|
|
|
|
|
|
|
|
|
|
if (typeof gameId !== "string" || !gameId) return { error: "gameId is required" };
|
|
|
|
|
|
|
|
|
|
const validStatuses: PlayoffMatchGameStatus[] = ["scheduled", "complete", "postponed"];
|
|
|
|
|
if (status && !validStatuses.includes(status as PlayoffMatchGameStatus)) {
|
|
|
|
|
return { error: `Invalid status: must be one of ${validStatuses.join(", ")}` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const updated = await updateGame(gameId, {
|
|
|
|
|
...(scheduledAt !== null && { scheduledAt: scheduledAt ? new Date(scheduledAt as string) : null }),
|
|
|
|
|
...(status && { status: status as PlayoffMatchGameStatus }),
|
|
|
|
|
...(participant1Score !== null && { participant1Score: (participant1Score as string) || null }),
|
|
|
|
|
...(participant2Score !== null && { participant2Score: (participant2Score as string) || null }),
|
|
|
|
|
...(winnerId !== null && { winnerId: (winnerId as string) || null }),
|
|
|
|
|
...(notes !== null && { notes: (notes as string) || null }),
|
|
|
|
|
});
|
|
|
|
|
if (!updated) return { error: "Game not found" };
|
|
|
|
|
return { success: "Game updated" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Failed to update game" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "delete-game") {
|
|
|
|
|
const gameId = formData.get("gameId");
|
|
|
|
|
if (typeof gameId !== "string" || !gameId) return { error: "gameId is required" };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await deleteGame(gameId);
|
|
|
|
|
return { success: "Game deleted" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Failed to delete game" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Odds management ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
if (intent === "upsert-odds") {
|
|
|
|
|
const matchId = formData.get("matchId");
|
|
|
|
|
const participantId = formData.get("participantId");
|
|
|
|
|
const moneylineOdds = formData.get("moneylineOdds");
|
|
|
|
|
const oddsSource = formData.get("oddsSource");
|
|
|
|
|
|
|
|
|
|
if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" };
|
|
|
|
|
if (typeof participantId !== "string" || !participantId) return { error: "participantId is required" };
|
|
|
|
|
if (typeof moneylineOdds !== "string" || !moneylineOdds) return { error: "moneylineOdds is required" };
|
|
|
|
|
|
|
|
|
|
const moneylineInt = parseInt(moneylineOdds, 10);
|
|
|
|
|
if (isNaN(moneylineInt)) return { error: "moneylineOdds must be an integer" };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await upsertMatchOdds(matchId, participantId, {
|
|
|
|
|
moneylineOdds: moneylineInt,
|
|
|
|
|
oddsSource: oddsSource ? (oddsSource as string) : undefined,
|
|
|
|
|
});
|
|
|
|
|
return { success: "Odds updated" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Failed to update odds" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (intent === "delete-odds") {
|
|
|
|
|
const matchId = formData.get("matchId");
|
|
|
|
|
const participantId = formData.get("participantId");
|
|
|
|
|
if (typeof matchId !== "string" || !matchId) return { error: "matchId is required" };
|
|
|
|
|
if (typeof participantId !== "string" || !participantId) return { error: "participantId is required" };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await deleteOddsForParticipant(matchId, participantId);
|
|
|
|
|
return { success: "Odds deleted" };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : "Failed to delete odds" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Implement bracket expansion plan to support various tournament structures
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
2025-11-03 09:36:16 -08:00
|
|
|
return { error: "Invalid action" };
|
|
|
|
|
}
|