import 'package:drift/drift.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/tables/game_table.dart'; import 'package:game_tracker/data/db/tables/group_table.dart'; import 'package:game_tracker/data/db/tables/match_table.dart'; import 'package:game_tracker/data/db/tables/player_match_table.dart'; import 'package:game_tracker/data/dto/game.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; part 'match_dao.g.dart'; @DriftAccessor(tables: [MatchTable, GameTable, GroupTable, PlayerMatchTable]) class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { MatchDao(super.db); /// Retrieves all matches from the database. Future> getAllMatches() async { final query = select(matchTable); final result = await query.get(); return Future.wait( result.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); Group? group; if (row.groupId != null) { group = await db.groupDao.getGroupById(groupId: row.groupId!); } final players = await db.playerMatchDao.getPlayersOfMatch( matchId: row.id, ); return Match( id: row.id, name: row.name ?? '', game: game, group: group, players: players, notes: row.notes, createdAt: row.createdAt, ); }), ); } /// Retrieves a [Match] by its [matchId]. Future getMatchById({required String matchId}) async { final query = select(matchTable)..where((g) => g.id.equals(matchId)); final result = await query.getSingle(); final game = await db.gameDao.getGameById(gameId: result.gameId); Group? group; if (result.groupId != null) { group = await db.groupDao.getGroupById(groupId: result.groupId!); } List? players; if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) { players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); } return Match( id: result.id, name: result.name ?? '', game: game, group: group, players: players, notes: result.notes, createdAt: result.createdAt, ); } /// Adds a new [Match] to the database. Also adds players associations. /// This method assumes that the game and group (if any) are already present /// in the database. Future addMatch({required Match match}) async { if (match.game == null) { throw ArgumentError('Match must have a game associated with it'); } await db.transaction(() async { await into(matchTable).insert( MatchTableCompanion.insert( id: match.id, gameId: match.game!.id, groupId: Value(match.group?.id), name: Value(match.name), notes: Value(match.notes), createdAt: match.createdAt, ), mode: InsertMode.insertOrReplace, ); if (match.players != null) { for (final p in match.players!) { await db.playerMatchDao.addPlayerToMatch( matchId: match.id, playerId: p.id, ); } } }); } /// Adds multiple [Match]es to the database in a batch operation. /// Also adds associated players and groups if they exist. /// If the [matches] list is empty, the method returns immediately. /// This method should only be used to import matches from a different device. Future addMatchAsList({required List matches}) async { if (matches.isEmpty) return; await db.transaction(() async { // Add all games first (deduplicated) final uniqueGames = {}; for (final match in matches) { if (match.game != null) { uniqueGames[match.game!.id] = match.game!; } } if (uniqueGames.isNotEmpty) { await db.batch( (b) => b.insertAll( db.gameTable, uniqueGames.values .map( (game) => GameTableCompanion.insert( id: game.id, name: game.name, ruleset: game.ruleset ?? '', description: Value(game.description), color: Value(game.color?.toString()), icon: Value(game.icon), createdAt: game.createdAt, ), ) .toList(), mode: InsertMode.insertOrIgnore, ), ); } // Add all groups of the matches in batch await db.batch( (b) => b.insertAll( db.groupTable, matches .where((match) => match.group != null) .map( (match) => GroupTableCompanion.insert( id: match.group!.id, name: match.group!.name, description: Value(match.group!.description), createdAt: match.group!.createdAt, ), ) .toList(), mode: InsertMode.insertOrIgnore, ), ); // Add all matches in batch await db.batch( (b) => b.insertAll( matchTable, matches .where((match) => match.game != null) .map( (match) => MatchTableCompanion.insert( id: match.id, gameId: match.game!.id, groupId: Value(match.group?.id), name: Value(match.name), notes: Value(match.notes), createdAt: match.createdAt, ), ) .toList(), mode: InsertMode.insertOrReplace, ), ); // Add all players of the matches in batch (unique) final uniquePlayers = {}; for (final match in matches) { if (match.players != null) { for (final p in match.players!) { uniquePlayers[p.id] = p; } } // Also include members of groups if (match.group != null) { for (final m in match.group!.members) { uniquePlayers[m.id] = m; } } } if (uniquePlayers.isNotEmpty) { await db.batch( (b) => b.insertAll( db.playerTable, uniquePlayers.values .map( (p) => PlayerTableCompanion.insert( id: p.id, name: p.name, description: Value(p.description), createdAt: p.createdAt, ), ) .toList(), mode: InsertMode.insertOrIgnore, ), ); } // Add all player-match associations in batch await db.batch((b) { for (final match in matches) { if (match.players != null) { for (final p in match.players!) { b.insert( db.playerMatchTable, PlayerMatchTableCompanion.insert( matchId: match.id, playerId: p.id, score: 0, ), mode: InsertMode.insertOrIgnore, ); } } } }); // Add all player-group associations in batch await db.batch((b) { for (final match in matches) { if (match.group != null) { for (final m in match.group!.members) { b.insert( db.playerGroupTable, PlayerGroupTableCompanion.insert( playerId: m.id, groupId: match.group!.id, ), mode: InsertMode.insertOrIgnore, ); } } } }); }); } /// Deletes the match with the given [matchId] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteMatch({required String matchId}) async { final query = delete(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.go(); return rowsAffected > 0; } /// Retrieves the number of matches in the database. Future getMatchCount() async { final count = await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) .map((row) => row.read(matchTable.id.count())) .getSingle(); return count ?? 0; } /// Checks if a match with the given [matchId] exists in the database. /// Returns `true` if the match exists, otherwise `false`. Future matchExists({required String matchId}) async { final query = select(matchTable)..where((g) => g.id.equals(matchId)); final result = await query.getSingleOrNull(); return result != null; } /// Deletes all matches from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllMatches() async { final query = delete(matchTable); final rowsAffected = await query.go(); return rowsAffected > 0; } /// Updates the notes of the match with the given [matchId]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchNotes({ required String matchId, required String? notes, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(notes: Value(notes)), ); return rowsAffected > 0; } /// Changes the name of the match with the given [matchId] to [newName]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchName({ required String matchId, required String newName, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(name: Value(newName)), ); return rowsAffected > 0; } /// Updates the game of the match with the given [matchId]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchGame({ required String matchId, required String gameId, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(gameId: Value(gameId)), ); return rowsAffected > 0; } /// Updates the group of the match with the given [matchId]. /// Pass null to remove the group association. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchGroup({ required String matchId, required String? groupId, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(groupId: Value(groupId)), ); return rowsAffected > 0; } // ============================================================ // TEMPORARY: Winner methods - these are stubs and do not persist data // TODO: Implement proper winner handling // ============================================================ /// TEMPORARY: Checks if a match has a winner. /// Currently returns true if the match has any players. Future hasWinner({required String matchId}) async { final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); return players?.isNotEmpty ?? false; } /// TEMPORARY: Gets the winner of a match. /// Currently returns the first player in the match's player list. Future getWinner({required String matchId}) async { final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); return (players?.isNotEmpty ?? false) ? players!.first : null; } /// TEMPORARY: Sets the winner of a match. /// Currently does nothing - winner is not persisted. Future setWinner({ required String matchId, required String winnerId, }) async { // TODO: Implement winner persistence return true; } /// TEMPORARY: Removes the winner of a match. /// Currently does nothing - winner is not persisted. Future removeWinner({required String matchId}) async { // TODO: Implement winner persistence return true; } }