Adding match endpoints, services and providers, additionally added a page in the frontend that displays the data

This commit is contained in:
2026-03-12 15:05:13 +01:00
parent 1402c16f21
commit bc35ec9c01
8 changed files with 675 additions and 34 deletions

View File

@@ -5,6 +5,7 @@ import 'dotenv/config';
import {TournamentService} from './services/tournament-service';
import {UserService} from './services/user-service';
import {TeamService} from './services/team-service';
import {MatchService} from './services/match-service';
import {authMiddleware} from './middlewares/auth-middleware';
import loggingMiddleware from './middlewares/logger';
import {Database} from 'sqlite3';
@@ -21,6 +22,7 @@ const db = new Database(dbFilename);
const tournamentService = new TournamentService(db);
const userService = new UserService(db);
const teamService = new TeamService(db);
const matchService = new MatchService(db);
const port = process.env.PORT || 3000;
const app = express();
@@ -68,6 +70,58 @@ app.delete('/tournaments/:id', authMiddleware, async (req: Request, res: Respons
res.status(200).send({message: 'Tournament deleted successfully'});
});
app.post('/tournaments/:id/bracket', authMiddleware, async (req: Request, res: Response) => {
try {
const tournamentId = +req.params.id;
const teams = await teamService.getTeamsByTournamentId(tournamentId);
const teamIds = teams.map(team => team.id);
if (teamIds.length < 2) {
return res.status(400).send({error: 'At least 2 teams are required to initialize bracket'});
}
await matchService.initializeBracket(tournamentId, teamIds);
res.status(201).send({message: 'Bracket initialized successfully'});
} catch (err) {
console.log(err);
res.status(400).send({error: 'Failed to initialize bracket'});
}
});
app.get('/tournaments/:id/matches', async (req: Request, res: Response) => {
try {
const matches = await matchService.getMatchesByTournament(+req.params.id);
res.send(matches);
} catch (err) {
console.log(err);
res.status(400).send({error: 'Failed to get matches'});
}
});
app.put('/matches/:id/winner', authMiddleware, async (req: Request, res: Response) => {
try {
const {winnerId} = req.body;
if (!winnerId) {
return res.status(400).send({error: 'winnerId is required'});
}
await matchService.setMatchWinner(+req.params.id, +winnerId);
res.status(200).send({message: 'Winner set successfully'});
} catch (err: any) {
console.log(err);
res.status(400).send({error: err.message || 'Failed to set winner'});
}
});
app.delete('/matches/:id/winner', authMiddleware, async (req: Request, res: Response) => {
try {
await matchService.resetMatch(+req.params.id);
res.status(200).send({message: 'Match reset successfully'});
} catch (err) {
console.log(err);
res.status(400).send({error: 'Failed to reset match'});
}
});
app.get('/teams', async (req: Request, res: Response) => {
const teams = await teamService.getAllTeams();
res.send(teams);

View File

@@ -0,0 +1,9 @@
export interface Match {
id: number;
tournamentId: number;
round: number;
matchNumber: number;
team1Id: number | null;
team2Id: number | null;
winnerId: number | null;
}

View File

@@ -0,0 +1,220 @@
import { Match } from '../models/match';
import { Database } from 'sqlite3';
export class MatchService {
private db: Database;
constructor(db: Database) {
this.db = db;
this.db.serialize(() => {
this.db.run(`CREATE TABLE IF NOT EXISTS Matches
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
tournamentId INTEGER NOT NULL,
round INTEGER NOT NULL,
matchNumber INTEGER NOT NULL,
team1Id INTEGER,
team2Id INTEGER,
winnerId INTEGER,
FOREIGN KEY (tournamentId) REFERENCES Tournaments(id) ON DELETE CASCADE,
FOREIGN KEY (team1Id) REFERENCES Teams(id) ON DELETE SET NULL,
FOREIGN KEY (team2Id) REFERENCES Teams(id) ON DELETE SET NULL,
FOREIGN KEY (winnerId) REFERENCES Teams(id) ON DELETE SET NULL
)`);
});
}
/**
* Initialize bracket for a tournament based on registered teams
*/
initializeBracket(tournamentId: number, teamIds: number[]): Promise<void> {
return new Promise((resolve, reject) => {
// Determine bracket size (2, 4, or 8)
const teamCount = teamIds.length;
let bracketSize: number;
if (teamCount <= 2) bracketSize = 2;
else if (teamCount <= 4) bracketSize = 4;
else bracketSize = 8;
// Calculate number of rounds
let roundCount: number;
if (bracketSize === 2) roundCount = 1;
else if (bracketSize === 4) roundCount = 2;
else roundCount = 3;
// First, check if matches already exist for this tournament
this.db.get(
'SELECT COUNT(*) as count FROM Matches WHERE tournamentId = ?',
[tournamentId],
(err, row: any) => {
if (err) return reject(err);
if (row.count > 0) {
// Matches already initialized
return resolve();
}
// Create first round matches
const statement = this.db.prepare(
'INSERT INTO Matches (tournamentId, round, matchNumber, team1Id, team2Id, winnerId) VALUES (?, ?, ?, ?, ?, ?)'
);
const firstRoundMatches = bracketSize / 2;
for (let i = 0; i < firstRoundMatches; i++) {
const team1Id = i * 2 < teamIds.length ? teamIds[i * 2] : null;
const team2Id = i * 2 + 1 < teamIds.length ? teamIds[i * 2 + 1] : null;
statement.run(tournamentId, 0, i, team1Id, team2Id, null);
}
// Create placeholder matches for subsequent rounds
for (let round = 1; round < roundCount; round++) {
const matchesInRound = bracketSize / Math.pow(2, round + 1);
for (let i = 0; i < matchesInRound; i++) {
statement.run(tournamentId, round, i, null, null, null);
}
}
// Create winner slot (final round)
statement.run(tournamentId, roundCount, 0, null, null, null, (err: Error | null) => {
if (err) return reject(err);
statement.finalize();
resolve();
});
}
);
});
}
/**
* Get all matches for a tournament
*/
getMatchesByTournament(tournamentId: number): Promise<Match[]> {
return new Promise((resolve, reject) => {
this.db.all(
'SELECT * FROM Matches WHERE tournamentId = ? ORDER BY round, matchNumber',
[tournamentId],
(err: Error | null, rows: any[]) => {
if (err) return reject(err);
const matches: Match[] = rows.map(row => ({
id: row.id,
tournamentId: row.tournamentId,
round: row.round,
matchNumber: row.matchNumber,
team1Id: row.team1Id,
team2Id: row.team2Id,
winnerId: row.winnerId,
}));
resolve(matches);
}
);
});
}
/**
* Set the winner of a match and progress them to the next round
*/
setMatchWinner(matchId: number, winnerId: number): Promise<void> {
return new Promise((resolve, reject) => {
// First, get the match details
this.db.get(
'SELECT * FROM Matches WHERE id = ?',
[matchId],
(err, match: any) => {
if (err) return reject(err);
if (!match) return reject(new Error('Match not found'));
// Validate that the winnerId is one of the participants
if (match.team1Id !== winnerId && match.team2Id !== winnerId) {
return reject(new Error('Winner must be one of the match participants'));
}
// Update the match with winner
this.db.run(
'UPDATE Matches SET winnerId = ? WHERE id = ?',
[winnerId, matchId],
(err) => {
if (err) return reject(err);
// Progress winner to next round
const nextRound = match.round + 1;
const nextMatchNumber = Math.floor(match.matchNumber / 2);
// Find the next match
this.db.get(
'SELECT * FROM Matches WHERE tournamentId = ? AND round = ? AND matchNumber = ?',
[match.tournamentId, nextRound, nextMatchNumber],
(err, nextMatch: any) => {
if (err) return reject(err);
if (!nextMatch) {
// This was the final match, no next round
return resolve();
}
// Determine if winner goes to team1 or team2 slot
const isEvenMatch = match.matchNumber % 2 === 0;
const field = isEvenMatch ? 'team1Id' : 'team2Id';
// Update next match with winner
this.db.run(
`UPDATE Matches SET ${field} = ? WHERE id = ?`,
[winnerId, nextMatch.id],
(err) => {
if (err) return reject(err);
resolve();
}
);
}
);
}
);
}
);
});
}
/**
* Reset a match (clear winner and remove from subsequent rounds)
*/
resetMatch(matchId: number): Promise<void> {
return new Promise((resolve, reject) => {
// Get match details
this.db.get(
'SELECT * FROM Matches WHERE id = ?',
[matchId],
(err, match: any) => {
if (err) return reject(err);
if (!match) return reject(new Error('Match not found'));
const previousWinnerId = match.winnerId;
// Clear the winner
this.db.run(
'UPDATE Matches SET winnerId = NULL WHERE id = ?',
[matchId],
(err) => {
if (err) return reject(err);
if (!previousWinnerId) {
return resolve();
}
// Remove winner from next round
const nextRound = match.round + 1;
const nextMatchNumber = Math.floor(match.matchNumber / 2);
const isEvenMatch = match.matchNumber % 2 === 0;
const field = isEvenMatch ? 'team1Id' : 'team2Id';
this.db.run(
`UPDATE Matches SET ${field} = NULL WHERE tournamentId = ? AND round = ? AND matchNumber = ?`,
[match.tournamentId, nextRound, nextMatchNumber],
(err) => {
if (err) return reject(err);
resolve();
}
);
}
);
}
);
});
}
}

View File

@@ -100,3 +100,13 @@ Folgende Dateien wurden in diesem Prompt verändert:
Folgende Dateien wurden in diesem Prompt verändert:
- frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart (neu erstellt)
- frontend_splatournament_manager/lib/pages/tournament_detail_page.dart
- add the ability to progress in a tournament<br><br>
Folgende Dateien wurden in diesem Prompt verändert:
- backend_splatournament_manager/src/models/match.ts (neu erstellt)
- backend_splatournament_manager/src/services/match-service.ts (neu erstellt)
- backend_splatournament_manager/src/app.ts
- frontend_splatournament_manager/lib/models/match.dart (neu erstellt)
- frontend_splatournament_manager/lib/providers/match_provider.dart (neu erstellt)
- frontend_splatournament_manager/lib/main.dart
- frontend_splatournament_manager/lib/pages/tournament_bracket_page.dart

View File

@@ -3,6 +3,7 @@ import 'package:frontend_splatournament_manager/pages/home_page.dart';
import 'package:frontend_splatournament_manager/pages/login_page.dart';
import 'package:frontend_splatournament_manager/pages/settings_page.dart';
import 'package:frontend_splatournament_manager/providers/auth_provider.dart';
import 'package:frontend_splatournament_manager/providers/match_provider.dart';
import 'package:frontend_splatournament_manager/providers/team_provider.dart';
import 'package:frontend_splatournament_manager/providers/theme_provider.dart';
import 'package:frontend_splatournament_manager/providers/tournament_provider.dart';
@@ -17,6 +18,7 @@ void main() {
ChangeNotifierProvider(create: (_) => TournamentProvider()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => TeamProvider()),
ChangeNotifierProvider(create: (_) => MatchProvider()),
],
child: const SplatournamentApp(),
),

View File

@@ -0,0 +1,46 @@
class Match {
final int id;
final int tournamentId;
final int round;
final int matchNumber;
final int? team1Id;
final int? team2Id;
final int? winnerId;
Match({
required this.id,
required this.tournamentId,
required this.round,
required this.matchNumber,
this.team1Id,
this.team2Id,
this.winnerId,
});
factory Match.fromJson(Map<String, dynamic> json) {
return Match(
id: json['id'],
tournamentId: json['tournamentId'],
round: json['round'],
matchNumber: json['matchNumber'],
team1Id: json['team1Id'],
team2Id: json['team2Id'],
winnerId: json['winnerId'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'tournamentId': tournamentId,
'round': round,
'matchNumber': matchNumber,
'team1Id': team1Id,
'team2Id': team2Id,
'winnerId': winnerId,
};
}
bool get hasWinner => winnerId != null;
bool get canBePlayed => team1Id != null && team2Id != null;
}

View File

@@ -1,14 +1,21 @@
import 'package:flutter/material.dart';
import 'package:frontend_splatournament_manager/models/match.dart';
import 'package:frontend_splatournament_manager/models/team.dart';
import 'package:frontend_splatournament_manager/models/tournament.dart';
import 'package:frontend_splatournament_manager/providers/match_provider.dart';
import 'package:frontend_splatournament_manager/providers/team_provider.dart';
import 'package:provider/provider.dart';
class TournamentBracketPage extends StatelessWidget {
class TournamentBracketPage extends StatefulWidget {
final Tournament tournament;
const TournamentBracketPage({super.key, required this.tournament});
@override
State<TournamentBracketPage> createState() => _TournamentBracketPageState();
}
class _TournamentBracketPageState extends State<TournamentBracketPage> {
int _bracketSize(int n) {
if (n <= 2) return 2;
if (n <= 4) return 4;
@@ -21,14 +28,95 @@ class TournamentBracketPage extends StatelessWidget {
return 4;
}
Future<void> _initializeBracket() async {
final matchProvider = Provider.of<MatchProvider>(context, listen: false);
try {
await matchProvider.initializeBracket(widget.tournament.id);
if (mounted) {
setState(() {});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bracket initialized successfully')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to initialize bracket: $e')),
);
}
}
}
Future<void> _showWinnerDialog(Match match, Team team1, Team team2) async {
final result = await showDialog<int>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Select Winner'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(team1.name),
onTap: () => Navigator.pop(context, team1.id),
),
ListTile(
title: Text(team2.name),
onTap: () => Navigator.pop(context, team2.id),
),
],
),
actions: [
if (match.hasWinner)
TextButton(
onPressed: () => Navigator.pop(context, -1), // Reset signal
child: const Text('Reset'),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
),
);
if (result != null && mounted) {
final matchProvider = Provider.of<MatchProvider>(context, listen: false);
try {
if (result == -1) {
await matchProvider.resetMatch(match.id);
} else {
await matchProvider.setMatchWinner(match.id, result);
}
setState(() {});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result == -1 ? 'Match reset' : 'Winner set successfully'),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
final teamProvider = Provider.of<TeamProvider>(context, listen: false);
final matchProvider = Provider.of<MatchProvider>(context, listen: false);
return Scaffold(
appBar: AppBar(title: Text(tournament.name)),
body: FutureBuilder<List<Team>>(
future: teamProvider.getTeamsByTournament(tournament.id),
appBar: AppBar(title: Text(widget.tournament.name)),
body: FutureBuilder<List<dynamic>>(
future: Future.wait([
teamProvider.getTeamsByTournament(widget.tournament.id),
matchProvider.getMatchesByTournament(widget.tournament.id),
]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
@@ -37,10 +125,43 @@ class TournamentBracketPage extends StatelessWidget {
return Center(child: Text('Error: ${snapshot.error}'));
}
final teams = snapshot.data ?? [];
final bracketSize = _bracketSize(tournament.maxTeamAmount);
final teams = snapshot.data![0] as List<Team>;
final matches = snapshot.data![1] as List<Match>;
// Check if bracket is initialized
if (matches.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Bracket not initialized yet',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: teams.length >= 2 ? _initializeBracket : null,
child: const Text('Initialize Bracket'),
),
if (teams.length < 2)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'Need at least 2 teams',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
final bracketSize = _bracketSize(teams.length);
final roundCount = _roundCount(bracketSize);
// Create a map of team IDs to Team objects for easy lookup
final teamMap = {for (var team in teams) team.id: team};
return Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
@@ -49,9 +170,11 @@ class TournamentBracketPage extends StatelessWidget {
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: _BracketBoard(
teams: teams,
matches: matches,
teamMap: teamMap,
bracketSize: bracketSize,
roundCount: roundCount,
onMatchTap: _showWinnerDialog,
),
),
),
@@ -63,9 +186,11 @@ class TournamentBracketPage extends StatelessWidget {
}
class _BracketBoard extends StatelessWidget {
final List<Team> teams;
final List<Match> matches;
final Map<int, Team> teamMap;
final int bracketSize;
final int roundCount;
final Function(Match, Team, Team) onMatchTap;
static const double _cardWidth = 112;
static const double _cardHeight = 96;
@@ -75,9 +200,11 @@ class _BracketBoard extends StatelessWidget {
static const double _lineThickness = 2;
const _BracketBoard({
required this.teams,
required this.matches,
required this.teamMap,
required this.bracketSize,
required this.roundCount,
required this.onMatchTap,
});
String _roundLabel(int round) {
@@ -96,6 +223,16 @@ class _BracketBoard extends StatelessWidget {
double _cardCenterY(int round, int index) =>
_cardTop(round, index) + _cardHeight / 2;
Match? _findMatch(int round, int matchNumber) {
try {
return matches.firstWhere(
(m) => m.round == round && m.matchNumber == matchNumber,
);
} catch (_) {
return null;
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -107,10 +244,12 @@ class _BracketBoard extends StatelessWidget {
final children = <Widget>[];
// Build bracket
for (int round = 0; round < roundCount; round++) {
final cardsInRound = bracketSize ~/ (1 << round);
final left = round * (_cardWidth + _connectorWidth);
// Round label
children.add(
Positioned(
left: left,
@@ -131,24 +270,43 @@ class _BracketBoard extends StatelessWidget {
),
);
// Match cards
for (int i = 0; i < cardsInRound; i++) {
final String? label = (round == 0 && i < teams.length)
? teams[i].name
: null;
final match = _findMatch(round, i);
children.add(
Positioned(
left: left,
top: _cardTop(round, i),
width: _cardWidth,
height: _cardHeight,
child: _TeamCard(label: label),
child: _MatchCard(
match: match,
teamMap: teamMap,
onTap: match != null && match.canBePlayed && !match.hasWinner
? () {
final team1 = teamMap[match.team1Id];
final team2 = teamMap[match.team2Id];
if (team1 != null && team2 != null) {
onMatchTap(match, team1, team2);
}
}
: match != null && match.hasWinner
? () {
final team1 = teamMap[match.team1Id];
final team2 = teamMap[match.team2Id];
if (team1 != null && team2 != null) {
onMatchTap(match, team1, team2);
}
}
: null,
),
),
);
}
if (round == roundCount - 1) {
continue;
}
// Draw connectors
if (round == roundCount - 1) continue;
final connectorLeft = left + _cardWidth;
final matches = cardsInRound ~/ 2;
@@ -159,6 +317,7 @@ class _BracketBoard extends StatelessWidget {
final yBottom = _cardCenterY(round, i * 2 + 1);
final yMiddle = (yTop + yBottom) / 2;
// Top horizontal line
children.add(
Positioned(
left: connectorLeft,
@@ -169,6 +328,7 @@ class _BracketBoard extends StatelessWidget {
),
);
// Bottom horizontal line
children.add(
Positioned(
left: connectorLeft,
@@ -179,6 +339,7 @@ class _BracketBoard extends StatelessWidget {
),
);
// Vertical line
children.add(
Positioned(
left: connectorLeft + halfConnector - _lineThickness / 2,
@@ -189,6 +350,7 @@ class _BracketBoard extends StatelessWidget {
),
);
// Middle horizontal line to next round
children.add(
Positioned(
left: connectorLeft + halfConnector,
@@ -209,36 +371,122 @@ class _BracketBoard extends StatelessWidget {
}
}
class _TeamCard extends StatelessWidget {
final String? label;
class _MatchCard extends StatelessWidget {
final Match? match;
final Map<int, Team> teamMap;
final VoidCallback? onTap;
const _TeamCard({this.label});
const _MatchCard({
this.match,
required this.teamMap,
this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isTbd = label == null;
return Card(
elevation: isTbd ? 1 : 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
if (match == null) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Center(
child: Text(
isTbd ? '?' : label!,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
'?',
style: TextStyle(
fontSize: 13,
fontWeight: isTbd ? FontWeight.w400 : FontWeight.w600,
color: isTbd
? colorScheme.onSurface.withValues(alpha: 0.38)
: colorScheme.onSurface,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface.withValues(alpha: 0.38),
),
),
),
);
}
final team1 = match!.team1Id != null ? teamMap[match!.team1Id] : null;
final team2 = match!.team2Id != null ? teamMap[match!.team2Id] : null;
final winner = match!.winnerId != null ? teamMap[match!.winnerId] : null;
return GestureDetector(
onTap: onTap,
child: Card(
elevation: match!.hasWinner ? 3 : 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: match!.hasWinner
? BorderSide(color: colorScheme.primary, width: 2)
: BorderSide.none,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (team1 != null && team2 != null) ...[
_TeamLabel(
team: team1,
isWinner: match!.winnerId == team1.id,
colorScheme: colorScheme,
),
const SizedBox(height: 4),
Text(
'vs',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 4),
_TeamLabel(
team: team2,
isWinner: match!.winnerId == team2.id,
colorScheme: colorScheme,
),
] else if (winner != null) ...[
_TeamLabel(
team: winner,
isWinner: true,
colorScheme: colorScheme,
),
] else
Text(
'?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w400,
color: colorScheme.onSurface.withValues(alpha: 0.38),
),
),
],
),
),
),
);
}
}
class _TeamLabel extends StatelessWidget {
final Team team;
final bool isWinner;
final ColorScheme colorScheme;
const _TeamLabel({
required this.team,
required this.isWinner,
required this.colorScheme,
});
@override
Widget build(BuildContext context) {
return Text(
team.name,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: isWinner ? FontWeight.w700 : FontWeight.w500,
color: isWinner ? colorScheme.primary : colorScheme.onSurface,
),
);
}

View File

@@ -0,0 +1,52 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:frontend_splatournament_manager/models/match.dart';
import 'package:frontend_splatournament_manager/services/api_client.dart';
class MatchProvider extends ChangeNotifier {
Future<void> initializeBracket(int tournamentId) async {
final response = await ApiClient.post('/tournaments/$tournamentId/bracket', {});
if (response.statusCode != HttpStatus.created) {
throw Exception('Failed to initialize bracket (${response.statusCode})');
}
notifyListeners();
}
Future<List<Match>> getMatchesByTournament(int tournamentId) async {
final response = await ApiClient.get('/tournaments/$tournamentId/matches');
if (response.statusCode != HttpStatus.ok) {
throw Exception('Failed to load matches (${response.statusCode})');
}
final List<dynamic> list = json.decode(response.body);
return list.map((json) => Match.fromJson(json)).toList();
}
Future<void> setMatchWinner(int matchId, int winnerId) async {
final response = await ApiClient.put('/matches/$matchId/winner', {
'winnerId': winnerId,
});
if (response.statusCode != HttpStatus.ok) {
final error = json.decode(response.body);
throw Exception(error['error'] ?? 'Failed to set winner');
}
notifyListeners();
}
Future<void> resetMatch(int matchId) async {
final response = await ApiClient.delete('/matches/$matchId/winner');
if (response.statusCode != HttpStatus.ok) {
throw Exception('Failed to reset match (${response.statusCode})');
}
notifyListeners();
}
}