diff --git a/backend/app/logic.py b/backend/app/logic.py index b812f09..b40db12 100644 --- a/backend/app/logic.py +++ b/backend/app/logic.py @@ -4,6 +4,7 @@ from datetime import timedelta from uuid import uuid4 from sqlalchemy.orm import Session +from sqlalchemy.orm.attributes import flag_modified from . import models from .constants import BracketTypes, MatchStatus, TournamentTypes @@ -190,8 +191,11 @@ def advance_winner(db: Session, match: models.Match, winner_id: int): reset_match = match.winner_next_match if reset_match.winner_team_id: undo_advancement(db, reset_match) - reset_match.winner_team_id = None - reset_match.sets = [] + + reset_match.winner_team_id = None + reset_match.sets = [] + flag_modified(reset_match, "sets") + reset_match.p1_team_id = None reset_match.p2_team_id = None reset_match.status = MatchStatus.SCHEDULED @@ -205,6 +209,18 @@ def advance_winner(db: Session, match: models.Match, winner_id: int): ): if not next_match or target_slot is None: return + current_team = ( + next_match.p1_team_id if target_slot == 0 else next_match.p2_team_id + ) + + if current_team and current_team != team_id: + if next_match.winner_team_id: + undo_advancement(db, next_match) + next_match.winner_team_id = None + next_match.sets = [] + flag_modified(next_match, "sets") + next_match.status = MatchStatus.SCHEDULED + if target_slot == 0: next_match.p1_team_id = team_id elif target_slot == 1: @@ -239,8 +255,10 @@ def undo_advancement(db: Session, match: models.Match): if next_match.winner_team_id: undo_advancement(db, next_match) - next_match.winner_team_id = None - next_match.sets = [] + + next_match.winner_team_id = None + next_match.sets = [] + flag_modified(next_match, "sets") if target_slot == 0: next_match.p1_team_id = None diff --git a/backend/app/routes/tournaments/report.py b/backend/app/routes/tournaments/report.py index d4ca6e1..a6f2186 100644 --- a/backend/app/routes/tournaments/report.py +++ b/backend/app/routes/tournaments/report.py @@ -42,13 +42,17 @@ async def report_score( if not report.sets: raise HTTPException(400, "No sets submitted") + if match.winner_team_id: + logic.undo_advancement(db, match) + _apply_score(match, report.sets) flag_modified(match, "sets") - db.commit() if match.winner_team_id: logic.advance_winner(db, match, match.winner_team_id) + db.commit() + await send_ws_update(id) return SUCCESS @@ -77,11 +81,12 @@ async def edit_score( _apply_score(match, report.sets) flag_modified(match, "sets") - db.commit() if match.winner_team_id: logic.advance_winner(db, match, match.winner_team_id) + db.commit() + await send_ws_update(id) return SUCCESS @@ -107,7 +112,10 @@ async def clear_score( logic.undo_advancement(db, match) match.winner_team_id = None - match.status = MatchStatus.PENDING + if match.p1_team_id and match.p2_team_id: + match.status = MatchStatus.PENDING + else: + match.status = MatchStatus.SCHEDULED match.sets = [] flag_modified(match, "sets") diff --git a/frontend/src/components/Bracket/BracketView.jsx b/frontend/src/components/Bracket/BracketView.jsx index 4da3660..6e77f22 100644 --- a/frontend/src/components/Bracket/BracketView.jsx +++ b/frontend/src/components/Bracket/BracketView.jsx @@ -70,28 +70,10 @@ export default function BracketView({ matches, onMatchClick }) { const renderRound = (list) => { const rounds = {}; list.forEach(m => (rounds[m.round] = rounds[m.round] || []).push(m)); - const roundKeys = Object.keys(rounds).sort((a, b) => Number(a) - Number(b)); - let prevRoundMap = new Map(); - - return roundKeys.map((r, rIdx) => { + return roundKeys.map((r) => { let matchesInRound = rounds[r]; - if (rIdx === 0) { - matchesInRound.sort((a, b) => a.number - b.number); - } else { - matchesInRound.sort((a, b) => { - const getSourceAvg = (match) => { - const sources = list.filter(x => x.winner_next_match_id === match.id); - if (sources.length === 0) return 9999; - const indices = sources.map(s => prevRoundMap.get(s.id)).filter(i => i !== undefined); - if (indices.length === 0) return 9999; - return indices.reduce((sum, val) => sum + val, 0) / indices.length; - }; - return getSourceAvg(a) - getSourceAvg(b); - }); - } - matchesInRound.forEach((m, idx) => prevRoundMap.set(m.id, idx)); - + matchesInRound.sort((a, b) => a.number - b.number); return (
{matchesInRound.map(m => )} diff --git a/frontend/src/components/Tournament/ScoreModal.jsx b/frontend/src/components/Tournament/ScoreModal.jsx index 2306763..8323694 100644 --- a/frontend/src/components/Tournament/ScoreModal.jsx +++ b/frontend/src/components/Tournament/ScoreModal.jsx @@ -1,8 +1,7 @@ // frontend/src/components/Tournament/ScoreModal.jsx -import React, { useState } from 'react'; -import { Eraser, Clock, MapPin, Trophy } from 'lucide-react'; -import { stringToColor } from '../../utils/helpers'; +import { Clock, Eraser, MapPin, Trophy } from 'lucide-react'; +import { useState } from 'react'; import Modal from '../UI/Modal'; const ScoreForm = ({ match, isAdmin, onSubmit, onClear }) => { diff --git a/frontend/src/pages/Tournament.jsx b/frontend/src/pages/Tournament.jsx index 7057100..c631508 100644 --- a/frontend/src/pages/Tournament.jsx +++ b/frontend/src/pages/Tournament.jsx @@ -29,6 +29,17 @@ export default function Tournament() { const processMatches = (rawMatches, courts, teams) => { if (!rawMatches) return []; + const gf1 = rawMatches.find(m => + m.bracket_type === 'Finals' && + m.winner_next_match_id && + m.winner_next_match_id === m.loser_next_match_id + ); + + let skippedResetMatchId = null; + if (gf1 && gf1.status === 'Finished' && gf1.winner_team_id === gf1.p1_team_id) { + skippedResetMatchId = gf1.winner_next_match_id; + } + const courtMap = Object.fromEntries(courts.map(c => [c.id, c.name])); const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name])); @@ -43,37 +54,39 @@ export default function Tournament() { } }); - return rawMatches.map(m => { - const sources = incoming[m.id] || []; + return rawMatches + .filter(m => m.id !== skippedResetMatchId) + .map(m => { + const sources = incoming[m.id] || []; - let sourceIndex = 0; - const p1 = m.p1_team_id ? teamMap[m.p1_team_id] : (sources[sourceIndex++]?.label || 'TBD'); - const p2 = m.p2_team_id ? teamMap[m.p2_team_id] : (sources[sourceIndex++]?.label || 'TBD'); + let sourceIndex = 0; + const p1 = m.p1_team_id ? teamMap[m.p1_team_id] : (sources[sourceIndex++]?.label || 'TBD'); + const p2 = m.p2_team_id ? teamMap[m.p2_team_id] : (sources[sourceIndex++]?.label || 'TBD'); - const hasTeams = !!(m.p1_team_id && m.p2_team_id); - const isFinished = m.status === "Finished"; - const isReady = hasTeams && !isFinished; - const winnerName = m.winner_team_id ? teamMap[m.winner_team_id] : null; + const hasTeams = !!(m.p1_team_id && m.p2_team_id); + const isFinished = m.status === "Finished"; + const isReady = hasTeams && !isFinished; + const winnerName = m.winner_team_id ? teamMap[m.winner_team_id] : null; - return { - ...m, - bracket: m.bracket_type, - round: m.round_number, - number: m.match_number, - p1, - p2, - winnerName, - p1_is_real: !!m.p1_team_id, - p2_is_real: !!m.p2_team_id, - isReady, - court: courtMap[m.court_id] || 'TBD', - time: m.start_time ? new Date(m.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '', - p1_sets: m.sets?.filter(s => s.p1 > s.p2).length || 0, - p2_sets: m.sets?.filter(s => s.p2 > s.p1).length || 0, - hasTeams, - isFinished - }; - }); + return { + ...m, + bracket: m.bracket_type, + round: m.round_number, + number: m.match_number, + p1, + p2, + winnerName, + p1_is_real: !!m.p1_team_id, + p2_is_real: !!m.p2_team_id, + isReady, + court: courtMap[m.court_id] || 'TBD', + time: m.start_time ? new Date(m.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '', + p1_sets: m.sets?.filter(s => s.p1 > s.p2).length || 0, + p2_sets: m.sets?.filter(s => s.p2 > s.p1).length || 0, + hasTeams, + isFinished + }; + }); }; const fetchData = async () => { @@ -133,9 +146,19 @@ export default function Tournament() { {scoreMatch && ( setScoreMatch(null)} match={scoreMatch} isAdmin={isAdmin} - onClear={async (mid, c) => { await api.delete(`/tournaments/${id}/matches/${mid}/score?code=${encodeURIComponent(c || '')}`); setScoreMatch(null); }} - onSubmit={async (mid, s, c) => { await api.post(`/tournaments/${id}/matches/${mid}/score`, { sets: s, code: c }); setScoreMatch(null); }} + isOpen={!!scoreMatch} + onClose={() => setScoreMatch(null)} + match={scoreMatch} + isAdmin={isAdmin} + onClear={async (mid, c) => { + await api.delete(`/tournaments/${id}/matches/${mid}/score?code=${encodeURIComponent(c || '')}`); + setScoreMatch(null); + }} + onSubmit={async (mid, s, c) => { + const method = scoreMatch.isFinished ? 'patch' : 'post'; + await api[method](`/tournaments/${id}/matches/${mid}/score`, { sets: s, code: c }); + setScoreMatch(null); + }} /> )} setShowSettings(false)} title="Edit Tournament">