diff --git a/app/components/scoring/MatchSchedule.tsx b/app/components/scoring/MatchSchedule.tsx index b6da163..ed6edf2 100644 --- a/app/components/scoring/MatchSchedule.tsx +++ b/app/components/scoring/MatchSchedule.tsx @@ -89,10 +89,14 @@ function MatchRow({ match }: { match: MatchWithRelations }) { ); } +// Groups by matchday number when available, or by UTC calendar date (YYYY-MM-DD) for date-based sports. +// UTC date keys ensure consistent grouping regardless of the viewer's timezone. function groupByMatchday(matches: MatchWithRelations[]) { const groups = new Map(); for (const m of matches) { - const key = m.matchday != null ? `Matchday ${m.matchday}` : formatDate(m.scheduledAt)?.split(",")[0] ?? "TBD"; + const key = m.matchday != null + ? `Matchday ${m.matchday}` + : m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD"; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(m); } @@ -108,35 +112,21 @@ export function MatchSchedule({ matches, title = "Schedule" }: MatchScheduleProp ); } - const hasMatchdays = matches.some((m) => m.matchday != null); - const grouped = hasMatchdays ? groupByMatchday(matches) : null; - - if (grouped) { - return ( -
- {title &&

{title}

} - {[...grouped.entries()].map(([label, group]) => ( -
-
- {label} -
- {group.map((m) => ( - - ))} -
- ))} -
- ); - } + const grouped = groupByMatchday(matches); return (
{title &&

{title}

} -
- {matches.map((m) => ( - - ))} -
+ {[...grouped.entries()].map(([label, group]) => ( +
+
+ {label} +
+ {group.map((m) => ( + + ))} +
+ ))}
); } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 7a10943..1dab669 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -27,6 +27,7 @@ import { processMatchResult, recalculateAffectedLeagues, recalculateStandings, + autoCompleteRoundIfDone, } from "~/models/scoring-calculator"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; @@ -211,39 +212,6 @@ export async function action({ request, params }: Route.ActionArgs) { } } - /** - * Auto-complete a round when every match in it is marked complete. - * Updates scoringEvents.playoffRound and calls processPlayoffEvent so that - * round placements are recorded and probabilities are refreshed. - * This replaces the manual "Complete Round" button. - * - * skipRecalculate=true because the caller already sent a Discord notification - * scoped to the current batch of matches — we don't want a second one that - * would show all previously-completed matches in the event. - */ - async function autoCompleteRoundIfDone( - eventId: string, - round: string, - sportsSeasonId: string, - db: ReturnType - ): Promise { - const allMatches = await findPlayoffMatchesByEventId(eventId); - const roundMatches = allMatches.filter((m) => m.round === round); - if (roundMatches.length === 0) return; - const allDone = roundMatches.every((m) => m.isComplete); - if (!allDone) return; - - await db - .update(schema.scoringEvents) - .set({ playoffRound: round, updatedAt: new Date() }) - .where(eq(schema.scoringEvents.id, eventId)); - - // skipProbabilities=true: callers (set-winner / set-round-winners) already ran - // updateProbabilitiesAfterResult before reaching here, so re-running would be wasted. - await processPlayoffEvent(eventId, db, { skipRecalculate: true, skipProbabilities: true }); - logger.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`); - } - if (intent === "set-winner") { const matchId = formData.get("matchId"); const winnerId = formData.get("winnerId"); diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx index 4ba21d1..ed9e90b 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx @@ -11,6 +11,7 @@ import { clearCs2StageAssignments, } from '~/models/cs2-major-stage'; import { findSeasonMatchesByScoringEventId } from '~/models/season-match'; +import { syncMatches } from '~/services/match-sync'; import { Button } from '~/components/ui/button'; import { Card, @@ -100,6 +101,17 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true, message: `Marked ${eliminations.length} eliminations.` }; } + if (intent === 'sync-matches') { + try { + const result = await syncMatches(params.id); + const total = result.swissCreated + result.swissUpdated; + const summary = `Synced: ${total} Swiss match(es), ${result.playoffUpdated} playoff match(es).`; + return { success: true, message: summary }; + } catch (err) { + return { success: false, message: err instanceof Error ? err.message : 'Sync failed.' }; + } + } + return { success: false, message: 'Unknown action.' }; } @@ -336,7 +348,8 @@ export default function AdminCs2Setup() { Swiss Rounds - + +