From 8e20fe103479d242b41d409917747930a5fd4346 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 7 Mar 2026 23:34:43 +0100 Subject: [PATCH 01/14] fix dart analysis issues --- lib/presentation/views/main_menu/custom_navigation_bar.dart | 2 +- .../views/main_menu/group_view/create_group_view.dart | 2 +- lib/presentation/widgets/app_skeleton.dart | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index cbea02a..3e1f865 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -141,7 +141,7 @@ class _CustomNavigationBarState extends State } /// Returns the title of the current tab based on [currentIndex]. - String _currentTabTitle(context) { + String _currentTabTitle(BuildContext context) { final loc = AppLocalizations.of(context); switch (currentIndex) { case 0: diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 42a11f3..acb3c0b 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -147,7 +147,7 @@ class _CreateGroupViewState extends State { (selectedPlayers.length < 2)) ? null : () async { - Group? updatedGroup = null; + Group? updatedGroup; bool successfullNameChange = true; bool successfullMemberChange = true; late bool success; diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 98f2ca7..8a21320 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -47,10 +47,7 @@ class _AppSkeletonState extends State { : (Widget? currentChild, List previousChildren) { return Stack( alignment: Alignment.topCenter, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], + children: [...previousChildren, ?currentChild], ); }, ), From 4ae14329436aa694bf893f68fa36d9af3b79f7cd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 11:07:13 +0100 Subject: [PATCH 02/14] excluded license file from linter rules --- analysis_options.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 04172d4..c0978e6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,4 +11,8 @@ linter: prefer_const_literals_to_create_immutables: true unnecessary_const: true lines_longer_than_80_chars: false - constant_identifier_names: false \ No newline at end of file + constant_identifier_names: false + +analyzer: + exclude: + - lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart From de0344d63df60eb887511172079410f97487238f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 8 Mar 2026 11:10:06 +0100 Subject: [PATCH 03/14] Added game fix --- .../main_menu/match_view/create_match/create_match_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 1bf732c..8138957 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -363,6 +363,7 @@ class _CreateMatchViewState extends State { final match = widget.matchToEdit!; _matchNameController.text = match.name; selectedPlayers = match.players; + selectedGameIndex = 0; if (match.group != null) { selectedGroup = match.group; From b83719f16d1f1f866fbd2b3beb90d89fd0a8c859 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 8 Mar 2026 11:27:18 +0100 Subject: [PATCH 04/14] Refactor group saving logic into a separate method --- .../group_view/create_group_view.dart | 106 +++++++++--------- 1 file changed, 56 insertions(+), 50 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index acb3c0b..18c159b 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -146,56 +146,7 @@ class _CreateGroupViewState extends State { (_groupNameController.text.isEmpty || (selectedPlayers.length < 2)) ? null - : () async { - Group? updatedGroup; - bool successfullNameChange = true; - bool successfullMemberChange = true; - late bool success; - if (widget.groupToEdit == null) { - success = await db.groupDao.addGroup( - group: Group( - name: _groupNameController.text.trim(), - description: '', - members: selectedPlayers, - ), - ); - } else { - updatedGroup = Group( - id: widget.groupToEdit!.id, - name: _groupNameController.text.trim(), - description: '', - members: selectedPlayers, - ); - if (widget.groupToEdit!.name != - _groupNameController.text.trim()) { - successfullNameChange = await db.groupDao - .updateGroupName( - groupId: widget.groupToEdit!.id, - newName: _groupNameController.text.trim(), - ); - } - - if (widget.groupToEdit!.members != selectedPlayers) { - successfullMemberChange = await db.groupDao - .replaceGroupPlayers( - groupId: widget.groupToEdit!.id, - newPlayers: selectedPlayers, - ); - } - success = - successfullNameChange && successfullMemberChange; - } - if (!context.mounted) return; - if (success) { - Navigator.pop(context, updatedGroup); - } else { - showSnackbar( - message: widget.groupToEdit == null - ? loc.error_creating_group - : loc.error_editing_group, - ); - } - }, + : _saveGroup, ), const SizedBox(height: 20), ], @@ -205,6 +156,61 @@ class _CreateGroupViewState extends State { ); } + /// Saves the group by creating a new one or updating the existing one, + /// depending on whether the widget is in edit mode. + Future _saveGroup() async { + final loc = AppLocalizations.of(context); + Group? updatedGroup; + bool successfullNameChange = true; + bool successfullMemberChange = true; + late bool success; + + final groupName = _groupNameController.text.trim(); + + if (widget.groupToEdit == null) { + success = await db.groupDao.addGroup( + group: Group( + name: groupName, + description: '', + members: selectedPlayers, + ), + ); + } else { + updatedGroup = Group( + id: widget.groupToEdit!.id, + name: groupName, + description: '', + members: selectedPlayers, + ); + if (widget.groupToEdit!.name != groupName) { + successfullNameChange = await db.groupDao.updateGroupName( + groupId: widget.groupToEdit!.id, + newName: groupName, + ); + } + + if (widget.groupToEdit!.members != selectedPlayers) { + successfullMemberChange = await db.groupDao.replaceGroupPlayers( + groupId: widget.groupToEdit!.id, + newPlayers: selectedPlayers, + ); + } + success = successfullNameChange && successfullMemberChange; + } + + if (!mounted) return; + + if (success) { + Navigator.pop(context, updatedGroup); + } else { + showSnackbar( + message: widget.groupToEdit == null + ? loc.error_creating_group + : loc.error_editing_group, + ); + } + } + /// Displays a snackbar with the given message and optional action. /// /// [message] The message to display in the snackbar. From 2214ea8e7d6b6da1d3011ae8922422dc477a62d0 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 8 Mar 2026 15:10:31 +0100 Subject: [PATCH 05/14] Refactor group creation and editing logic into separate methods --- .../group_view/create_group_view.dart | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 18c159b..97a49e4 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -160,42 +160,15 @@ class _CreateGroupViewState extends State { /// depending on whether the widget is in edit mode. Future _saveGroup() async { final loc = AppLocalizations.of(context); - Group? updatedGroup; - bool successfullNameChange = true; - bool successfullMemberChange = true; late bool success; - - final groupName = _groupNameController.text.trim(); + Group? updatedGroup; if (widget.groupToEdit == null) { - success = await db.groupDao.addGroup( - group: Group( - name: groupName, - description: '', - members: selectedPlayers, - ), - ); + success = await _createGroup(); } else { - updatedGroup = Group( - id: widget.groupToEdit!.id, - name: groupName, - description: '', - members: selectedPlayers, - ); - if (widget.groupToEdit!.name != groupName) { - successfullNameChange = await db.groupDao.updateGroupName( - groupId: widget.groupToEdit!.id, - newName: groupName, - ); - } - - if (widget.groupToEdit!.members != selectedPlayers) { - successfullMemberChange = await db.groupDao.replaceGroupPlayers( - groupId: widget.groupToEdit!.id, - newPlayers: selectedPlayers, - ); - } - success = successfullNameChange && successfullMemberChange; + final result = await _editGroup(); + success = result.$1; + updatedGroup = result.$2; } if (!mounted) return; @@ -211,6 +184,51 @@ class _CreateGroupViewState extends State { } } + /// Handles creating a new group and returns whether the operation was successful. + Future _createGroup() async { + final groupName = _groupNameController.text.trim(); + + final success = await db.groupDao.addGroup( + group: Group(name: groupName, description: '', members: selectedPlayers), + ); + + return success; + } + + /// Handles editing an existing group and returns a tuple of + /// (success, updatedGroup). + Future<(bool, Group?)> _editGroup() async { + final groupName = _groupNameController.text.trim(); + + Group? updatedGroup = Group( + id: widget.groupToEdit!.id, + name: groupName, + description: '', + members: selectedPlayers, + ); + + bool successfullNameChange = true; + bool successfullMemberChange = true; + + if (widget.groupToEdit!.name != groupName) { + successfullNameChange = await db.groupDao.updateGroupName( + groupId: widget.groupToEdit!.id, + newName: groupName, + ); + } + + if (widget.groupToEdit!.members != selectedPlayers) { + successfullMemberChange = await db.groupDao.replaceGroupPlayers( + groupId: widget.groupToEdit!.id, + newPlayers: selectedPlayers, + ); + } + + final success = successfullNameChange && successfullMemberChange; + + return (success, updatedGroup); + } + /// Displays a snackbar with the given message and optional action. /// /// [message] The message to display in the snackbar. From 6c50eaefc7fdd4a152cf4a6aa5194bd503ef1d40 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 15:27:15 +0100 Subject: [PATCH 06/14] Add deleteMatchGroup method & tests --- lib/data/dao/match_dao.dart | 11 ++++++ test/db_tests/aggregates/match_test.dart | 47 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index cc30b03..a822634 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -338,6 +338,17 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } + /// Entfernt die Gruppen-Verknüpfung des Matches mit der gegebenen [matchId]. + /// Setzt die groupId auf null. + /// Gibt `true` zurück, wenn mehr als 0 Zeilen betroffen waren, ansonsten `false`. + Future deleteMatchGroup({required String matchId}) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + const MatchTableCompanion(groupId: Value(null)), + ); + return rowsAffected > 0; + } + /// Updates the createdAt timestamp of the match with the given [matchId]. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchCreatedAt({ diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index ea80369..757987a 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -307,5 +307,52 @@ void main() { expect(fetchedMatch.winner, isNotNull); expect(fetchedMatch.winner!.id, testPlayer5.id); }); + + // Tests for removeMatchGroup + test( + 'removeMatchGroup removes group from match with existing group', + () async { + await database.matchDao.addMatch(match: testMatch1); + + final removed = await database.matchDao.deleteMatchGroup( + matchId: testMatch1.id, + ); + expect(removed, isTrue); + + final updatedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(updatedMatch.group, null); + // Andere Felder bleiben unverändert + expect(updatedMatch.game.id, testMatch1.game.id); + expect(updatedMatch.name, testMatch1.name); + expect(updatedMatch.notes, testMatch1.notes); + }, + ); + + test( + 'removeMatchGroup on match that already has no group still succeeds', + () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); + + final removed = await database.matchDao.deleteMatchGroup( + matchId: testMatchOnlyPlayers.id, + ); + // Update sollte trotzdem eine Zeile betreffen + expect(removed, isTrue); + + final updatedMatch = await database.matchDao.getMatchById( + matchId: testMatchOnlyPlayers.id, + ); + expect(updatedMatch.group, null); + }, + ); + + test('removeMatchGroup on non-existing match returns false', () async { + final removed = await database.matchDao.deleteMatchGroup( + matchId: 'non-existing-id', + ); + expect(removed, isFalse); + }); }); } From 840faab0248f2641ae56709e285649690602fe60 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 15:28:37 +0100 Subject: [PATCH 07/14] add statistics reload and fix wrong best player calculation --- .../main_menu/group_view/group_detail_view.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index ad88d66..f4d12bf 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -191,7 +191,13 @@ class _GroupDetailViewState extends State { context, adaptivePageRoute( builder: (context) { - return CreateGroupView(groupToEdit: _group); + return CreateGroupView( + groupToEdit: _group, + onMembersChanged: () { + isLoading = true; + _loadStatistics(); + }, + ); }, ), ); @@ -260,7 +266,9 @@ class _GroupDetailViewState extends State { // Count wins for each player for (var match in matches) { - if (match.winner != null) { + if (match.winner != null && + _group.members.any((m) => m.id == match.winner?.id)) { + print(match.winner); bestPlayerCounts.update( match.winner!, (value) => value + 1, From b0b039875a409d5b5a45112b1589bcb6d8e8c87d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 15:29:03 +0100 Subject: [PATCH 08/14] add callback & implement deleteObsoleteMatchGroupRelations func --- .../group_view/create_group_view.dart | 67 +++++++------------ 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 97a49e4..bb77057 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -12,11 +12,13 @@ import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; class CreateGroupView extends StatefulWidget { - const CreateGroupView({super.key, this.groupToEdit}); + const CreateGroupView({super.key, this.groupToEdit, this.onMembersChanged}); /// The group to edit, if any final Group? groupToEdit; + final VoidCallback? onMembersChanged; + @override State createState() => _CreateGroupViewState(); } @@ -70,49 +72,6 @@ class _CreateGroupViewState extends State { title: Text( widget.groupToEdit == null ? loc.create_new_group : loc.edit_group, ), - actions: widget.groupToEdit == null - ? [] - : [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - if (widget.groupToEdit != null) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(loc.delete_group), - content: Text(loc.this_cannot_be_undone), - actions: [ - TextButton( - onPressed: () => - Navigator.of(context).pop(false), - child: Text(loc.cancel), - ), - TextButton( - onPressed: () => - Navigator.of(context).pop(true), - child: Text(loc.delete), - ), - ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - bool success = await db.groupDao.deleteGroup( - groupId: widget.groupToEdit!.id, - ); - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - if (!mounted) return; - showSnackbar(message: loc.error_deleting_group); - } - } - }); - } - }, - ), - ], ), body: SafeArea( child: Column( @@ -222,6 +181,8 @@ class _CreateGroupViewState extends State { groupId: widget.groupToEdit!.id, newPlayers: selectedPlayers, ); + await deleteObsoleteMatchGroupRelations(); + widget.onMembersChanged?.call(); } final success = successfullNameChange && successfullMemberChange; @@ -229,6 +190,24 @@ class _CreateGroupViewState extends State { return (success, updatedGroup); } + Future deleteObsoleteMatchGroupRelations() async { + final matches = await db.matchDao.getAllMatches(); + final groupMatches = matches + .where((match) => match.group?.id == widget.groupToEdit!.id) + .toList(); + + final selectedPlayerIds = selectedPlayers.map((p) => p.id).toSet(); + final relationshipsToDelete = groupMatches.where((match) { + return !match.players.any( + (player) => selectedPlayerIds.contains(player.id), + ); + }).toList(); + + for (var match in relationshipsToDelete) { + await db.matchDao.deleteMatchGroup(matchId: match.id); + } + } + /// Displays a snackbar with the given message and optional action. /// /// [message] The message to display in the snackbar. From 3fe421676c0ed1aad66d418b47ba524d3da2ae7d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 15:29:18 +0100 Subject: [PATCH 09/14] update onDelete behavior for groupId in match_table to set null --- lib/data/db/database.g.dart | 4 ++-- lib/data/db/tables/match_table.dart | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index fe14e93..227c7c0 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -1123,7 +1123,7 @@ class $MatchTableTable extends MatchTable type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES group_table (id) ON DELETE CASCADE', + 'REFERENCES group_table (id) ON DELETE SET NULL', ), ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @@ -2780,7 +2780,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { 'group_table', limitUpdateKind: UpdateKind.delete, ), - result: [TableUpdate('match_table', kind: UpdateKind.delete)], + result: [TableUpdate('match_table', kind: UpdateKind.update)], ), WritePropagation( on: TableUpdateQuery.onTableName( diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart index 191e72c..545b133 100644 --- a/lib/data/db/tables/match_table.dart +++ b/lib/data/db/tables/match_table.dart @@ -7,8 +7,10 @@ class MatchTable extends Table { TextColumn get gameId => text().references(GameTable, #id, onDelete: KeyAction.cascade)(); // Nullable if there is no group associated with the match - TextColumn get groupId => - text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); + // onDelete: If a group gets deleted, groupId in the match gets set to null + TextColumn get groupId => text() + .references(GroupTable, #id, onDelete: KeyAction.setNull) + .nullable()(); TextColumn get name => text().nullable()(); TextColumn get notes => text().nullable()(); DateTimeColumn get createdAt => dateTime()(); @@ -16,4 +18,4 @@ class MatchTable extends Table { @override Set> get primaryKey => {id}; -} \ No newline at end of file +} From 4726d170a193153f2cbd6a2f6f9cd2b01dd96bfe Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 15:29:26 +0100 Subject: [PATCH 10/14] update dependencies in pubspec.yaml for build_runner and drift_dev --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index bdb1efe..c812953 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,9 +31,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.5.4 + build_runner: ^2.7.0 dart_pubspec_licenses: ^3.0.14 - drift_dev: ^2.27.0 + drift_dev: ^2.29.0 flutter_lints: ^6.0.0 flutter: From 23d00c64ab98e2a2ba98a9747addb29ac1588798 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 21:00:13 +0100 Subject: [PATCH 11/14] fix comments --- lib/data/dao/group_dao.dart | 7 +++++++ lib/data/dao/match_dao.dart | 8 ++++---- .../main_menu/group_view/create_group_view.dart | 15 ++++++++++----- .../main_menu/group_view/group_detail_view.dart | 7 ++----- test/db_tests/aggregates/match_test.dart | 9 +++------ 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 558a93c..13d1940 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -3,6 +3,7 @@ import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/dto/group.dart'; +import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; part 'group_dao.g.dart'; @@ -171,6 +172,12 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { }); } + /// Retrieves all matches associated with the given [groupId]. + Future> getGroupMatches({required String groupId}) async { + final matches = await db.matchDao.getAllMatches(); + return matches.where((match) => match.group?.id == groupId).toList(); + } + /// Deletes the group with the given [id] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteGroup({required String groupId}) async { diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index a822634..e2c951f 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -338,10 +338,10 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return rowsAffected > 0; } - /// Entfernt die Gruppen-Verknüpfung des Matches mit der gegebenen [matchId]. - /// Setzt die groupId auf null. - /// Gibt `true` zurück, wenn mehr als 0 Zeilen betroffen waren, ansonsten `false`. - Future deleteMatchGroup({required String matchId}) async { + /// Removes the group association of the match with the given [matchId]. + /// Sets the groupId to null. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removeMatchGroup({required String matchId}) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( const MatchTableCompanion(groupId: Value(null)), diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index bb77057..4b12c80 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -190,11 +190,16 @@ class _CreateGroupViewState extends State { return (success, updatedGroup); } + /// Removes the group association from matches that no longer belong to the edited group. + /// + /// After updating the group's members, matches that were previously linked to + /// this group but don't have any of the newly selected players are considered + /// obsolete. For each such match, the group association is removed by setting + /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final matches = await db.matchDao.getAllMatches(); - final groupMatches = matches - .where((match) => match.group?.id == widget.groupToEdit!.id) - .toList(); + final groupMatches = await db.groupDao.getGroupMatches( + groupId: widget.groupToEdit!.id, + ); final selectedPlayerIds = selectedPlayers.map((p) => p.id).toSet(); final relationshipsToDelete = groupMatches.where((match) { @@ -204,7 +209,7 @@ class _CreateGroupViewState extends State { }).toList(); for (var match in relationshipsToDelete) { - await db.matchDao.deleteMatchGroup(matchId: match.id); + await db.matchDao.removeMatchGroup(matchId: match.id); } } diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index f4d12bf..0ff8bf7 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -194,7 +194,6 @@ class _GroupDetailViewState extends State { return CreateGroupView( groupToEdit: _group, onMembersChanged: () { - isLoading = true; _loadStatistics(); }, ); @@ -248,10 +247,8 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { - final matches = await db.matchDao.getAllMatches(); - final groupMatches = matches - .where((match) => match.group?.id == _group.id) - .toList(); + isLoading = true; + final groupMatches = await db.groupDao.getGroupMatches(groupId: _group.id); setState(() { totalMatches = groupMatches.length; diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 757987a..88b66da 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -308,13 +308,12 @@ void main() { expect(fetchedMatch.winner!.id, testPlayer5.id); }); - // Tests for removeMatchGroup test( 'removeMatchGroup removes group from match with existing group', () async { await database.matchDao.addMatch(match: testMatch1); - final removed = await database.matchDao.deleteMatchGroup( + final removed = await database.matchDao.removeMatchGroup( matchId: testMatch1.id, ); expect(removed, isTrue); @@ -323,7 +322,6 @@ void main() { matchId: testMatch1.id, ); expect(updatedMatch.group, null); - // Andere Felder bleiben unverändert expect(updatedMatch.game.id, testMatch1.game.id); expect(updatedMatch.name, testMatch1.name); expect(updatedMatch.notes, testMatch1.notes); @@ -335,10 +333,9 @@ void main() { () async { await database.matchDao.addMatch(match: testMatchOnlyPlayers); - final removed = await database.matchDao.deleteMatchGroup( + final removed = await database.matchDao.removeMatchGroup( matchId: testMatchOnlyPlayers.id, ); - // Update sollte trotzdem eine Zeile betreffen expect(removed, isTrue); final updatedMatch = await database.matchDao.getMatchById( @@ -349,7 +346,7 @@ void main() { ); test('removeMatchGroup on non-existing match returns false', () async { - final removed = await database.matchDao.deleteMatchGroup( + final removed = await database.matchDao.removeMatchGroup( matchId: 'non-existing-id', ); expect(removed, isFalse); From a304d9adf78bb41a53943e67e5e5aff9402fcdf6 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 21:13:08 +0100 Subject: [PATCH 12/14] change getGroupMatches --- lib/data/dao/group_dao.dart | 29 ++++++++++++++++++++++++++--- lib/data/dao/group_dao.g.dart | 2 ++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 13d1940..8891f1f 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/dto/match.dart'; @@ -8,7 +9,7 @@ import 'package:tallee/data/dto/player.dart'; part 'group_dao.g.dart'; -@DriftAccessor(tables: [GroupTable, PlayerGroupTable]) +@DriftAccessor(tables: [GroupTable, PlayerGroupTable, MatchTable]) class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { GroupDao(super.db); @@ -173,9 +174,31 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { } /// Retrieves all matches associated with the given [groupId]. + /// Queries the database directly, filtering by [groupId]. Future> getGroupMatches({required String groupId}) async { - final matches = await db.matchDao.getAllMatches(); - return matches.where((match) => match.group?.id == groupId).toList(); + final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); + final rows = await query.get(); + + return Future.wait( + rows.map((row) async { + final game = await db.gameDao.getGameById(gameId: row.gameId); + final group = await db.groupDao.getGroupById(groupId: groupId); + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final winner = await db.matchDao.getWinner(matchId: row.id); + return Match( + id: row.id, + name: row.name ?? '', + game: game, + group: group, + players: players, + notes: row.notes ?? '', + createdAt: row.createdAt, + endedAt: row.endedAt, + winner: winner, + ); + }), + ); } /// Deletes the group with the given [id] from the database. diff --git a/lib/data/dao/group_dao.g.dart b/lib/data/dao/group_dao.g.dart index b9534b4..37a5586 100644 --- a/lib/data/dao/group_dao.g.dart +++ b/lib/data/dao/group_dao.g.dart @@ -8,4 +8,6 @@ mixin _$GroupDaoMixin on DatabaseAccessor { $PlayerTableTable get playerTable => attachedDatabase.playerTable; $PlayerGroupTableTable get playerGroupTable => attachedDatabase.playerGroupTable; + $GameTableTable get gameTable => attachedDatabase.gameTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; } From 4e98dcde418c3c1e6bb92524970bdc7db0bb8919 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Mon, 9 Mar 2026 21:13:20 +0100 Subject: [PATCH 13/14] remove nullable from match name --- lib/data/db/tables/match_table.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart index 545b133..25b0a73 100644 --- a/lib/data/db/tables/match_table.dart +++ b/lib/data/db/tables/match_table.dart @@ -11,7 +11,7 @@ class MatchTable extends Table { TextColumn get groupId => text() .references(GroupTable, #id, onDelete: KeyAction.setNull) .nullable()(); - TextColumn get name => text().nullable()(); + TextColumn get name => text()(); TextColumn get notes => text().nullable()(); DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get endedAt => dateTime().nullable()(); From 611033b5cdfe6bcdc868b6593a004ff7bf1981f5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 9 Mar 2026 21:27:05 +0100 Subject: [PATCH 14/14] Moved getGroupMatches + Tests --- lib/data/dao/group_dao.dart | 29 ------------------- lib/data/dao/match_dao.dart | 28 ++++++++++++++++++ .../group_view/create_group_view.dart | 2 +- .../group_view/group_detail_view.dart | 2 +- test/db_tests/aggregates/match_test.dart | 22 +++++++++++++- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 8891f1f..552b566 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -4,7 +4,6 @@ import 'package:tallee/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/match_table.dart'; import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/dto/group.dart'; -import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; part 'group_dao.g.dart'; @@ -173,34 +172,6 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { }); } - /// Retrieves all matches associated with the given [groupId]. - /// Queries the database directly, filtering by [groupId]. - Future> getGroupMatches({required String groupId}) async { - final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); - final rows = await query.get(); - - return Future.wait( - rows.map((row) async { - final game = await db.gameDao.getGameById(gameId: row.gameId); - final group = await db.groupDao.getGroupById(groupId: groupId); - final players = - await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; - final winner = await db.matchDao.getWinner(matchId: row.id); - return Match( - id: row.id, - name: row.name ?? '', - game: game, - group: group, - players: players, - notes: row.notes ?? '', - createdAt: row.createdAt, - endedAt: row.endedAt, - winner: winner, - ); - }), - ); - } - /// Deletes the group with the given [id] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteGroup({required String groupId}) async { diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index e2c951f..d3fd06f 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -268,6 +268,34 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + /// Retrieves all matches associated with the given [groupId]. + /// Queries the database directly, filtering by [groupId]. + Future> getGroupMatches({required String groupId}) async { + final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); + final rows = await query.get(); + + return Future.wait( + rows.map((row) async { + final game = await db.gameDao.getGameById(gameId: row.gameId); + final group = await db.groupDao.getGroupById(groupId: groupId); + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final winner = await db.matchDao.getWinner(matchId: row.id); + return Match( + id: row.id, + name: row.name ?? '', + game: game, + group: group, + players: players, + notes: row.notes ?? '', + createdAt: row.createdAt, + endedAt: row.endedAt, + winner: winner, + ); + }), + ); + } + /// 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 { diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 4b12c80..8829343 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -197,7 +197,7 @@ class _CreateGroupViewState extends State { /// obsolete. For each such match, the group association is removed by setting /// its [groupId] to null. Future deleteObsoleteMatchGroupRelations() async { - final groupMatches = await db.groupDao.getGroupMatches( + final groupMatches = await db.matchDao.getGroupMatches( groupId: widget.groupToEdit!.id, ); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 0ff8bf7..c9ffa25 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -248,7 +248,7 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { isLoading = true; - final groupMatches = await db.groupDao.getGroupMatches(groupId: _group.id); + final groupMatches = await db.matchDao.getGroupMatches(groupId: _group.id); setState(() { totalMatches = groupMatches.length; diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 88b66da..f5fbeb6 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -42,7 +42,7 @@ void main() { testPlayer4 = Player(name: 'Diana', description: ''); testPlayer5 = Player(name: 'Eve', description: ''); testGroup1 = Group( - name: 'Test Group 2', + name: 'Test Group 1', description: '', members: [testPlayer1, testPlayer2, testPlayer3], ); @@ -351,5 +351,25 @@ void main() { ); expect(removed, isFalse); }); + + test('Fetching all matches related to a group', () async { + var matches = await database.matchDao.getGroupMatches( + groupId: 'non-existing-id', + ); + + expect(matches, isEmpty); + + await database.matchDao.addMatch(match: testMatch1); + print(await database.matchDao.getAllMatches()); + + matches = await database.matchDao.getGroupMatches(groupId: testGroup1.id); + + expect(matches, isNotEmpty); + + final match = matches.first; + expect(match.id, testMatch1.id); + expect(match.group, isNotNull); + expect(match.group!.id, testGroup1.id); + }); }); }