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">