5 Commits

Author SHA1 Message Date
4726d170a1 update dependencies in pubspec.yaml for build_runner and drift_dev
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 39s
Pull Request Pipeline / lint (pull_request) Successful in 44s
2026-03-09 15:29:26 +01:00
3fe421676c update onDelete behavior for groupId in match_table to set null 2026-03-09 15:29:18 +01:00
b0b039875a add callback & implement deleteObsoleteMatchGroupRelations func 2026-03-09 15:29:03 +01:00
840faab024 add statistics reload and fix wrong best player calculation 2026-03-09 15:28:37 +01:00
6c50eaefc7 Add deleteMatchGroup method & tests 2026-03-09 15:27:15 +01:00
7 changed files with 100 additions and 53 deletions

View File

@@ -338,6 +338,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0; 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<bool> 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]. /// Updates the createdAt timestamp of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchCreatedAt({ Future<bool> updateMatchCreatedAt({

View File

@@ -1123,7 +1123,7 @@ class $MatchTableTable extends MatchTable
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( 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'); static const VerificationMeta _nameMeta = const VerificationMeta('name');
@@ -2780,7 +2780,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
'group_table', 'group_table',
limitUpdateKind: UpdateKind.delete, limitUpdateKind: UpdateKind.delete,
), ),
result: [TableUpdate('match_table', kind: UpdateKind.delete)], result: [TableUpdate('match_table', kind: UpdateKind.update)],
), ),
WritePropagation( WritePropagation(
on: TableUpdateQuery.onTableName( on: TableUpdateQuery.onTableName(

View File

@@ -7,8 +7,10 @@ class MatchTable extends Table {
TextColumn get gameId => TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)(); text().references(GameTable, #id, onDelete: KeyAction.cascade)();
// Nullable if there is no group associated with the match // Nullable if there is no group associated with the match
TextColumn get groupId => // onDelete: If a group gets deleted, groupId in the match gets set to null
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); TextColumn get groupId => text()
.references(GroupTable, #id, onDelete: KeyAction.setNull)
.nullable()();
TextColumn get name => text().nullable()(); TextColumn get name => text().nullable()();
TextColumn get notes => text().nullable()(); TextColumn get notes => text().nullable()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();

View File

@@ -12,11 +12,13 @@ import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class CreateGroupView extends StatefulWidget { class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key, this.groupToEdit}); const CreateGroupView({super.key, this.groupToEdit, this.onMembersChanged});
/// The group to edit, if any /// The group to edit, if any
final Group? groupToEdit; final Group? groupToEdit;
final VoidCallback? onMembersChanged;
@override @override
State<CreateGroupView> createState() => _CreateGroupViewState(); State<CreateGroupView> createState() => _CreateGroupViewState();
} }
@@ -70,49 +72,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
title: Text( title: Text(
widget.groupToEdit == null ? loc.create_new_group : loc.edit_group, 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<bool>(
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( body: SafeArea(
child: Column( child: Column(
@@ -222,6 +181,8 @@ class _CreateGroupViewState extends State<CreateGroupView> {
groupId: widget.groupToEdit!.id, groupId: widget.groupToEdit!.id,
newPlayers: selectedPlayers, newPlayers: selectedPlayers,
); );
await deleteObsoleteMatchGroupRelations();
widget.onMembersChanged?.call();
} }
final success = successfullNameChange && successfullMemberChange; final success = successfullNameChange && successfullMemberChange;
@@ -229,6 +190,24 @@ class _CreateGroupViewState extends State<CreateGroupView> {
return (success, updatedGroup); return (success, updatedGroup);
} }
Future<void> 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. /// Displays a snackbar with the given message and optional action.
/// ///
/// [message] The message to display in the snackbar. /// [message] The message to display in the snackbar.

View File

@@ -191,7 +191,13 @@ class _GroupDetailViewState extends State<GroupDetailView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return CreateGroupView(groupToEdit: _group); return CreateGroupView(
groupToEdit: _group,
onMembersChanged: () {
isLoading = true;
_loadStatistics();
},
);
}, },
), ),
); );
@@ -260,7 +266,9 @@ class _GroupDetailViewState extends State<GroupDetailView> {
// Count wins for each player // Count wins for each player
for (var match in matches) { 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( bestPlayerCounts.update(
match.winner!, match.winner!,
(value) => value + 1, (value) => value + 1,

View File

@@ -31,9 +31,9 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.5.4 build_runner: ^2.7.0
dart_pubspec_licenses: ^3.0.14 dart_pubspec_licenses: ^3.0.14
drift_dev: ^2.27.0 drift_dev: ^2.29.0
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter: flutter:

View File

@@ -307,5 +307,52 @@ void main() {
expect(fetchedMatch.winner, isNotNull); expect(fetchedMatch.winner, isNotNull);
expect(fetchedMatch.winner!.id, testPlayer5.id); 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);
});
}); });
} }