Merge pull request 'Bearbeiten und Löschen von Gruppen' (#148) from feature/118-bearbeiten-und-löschen-von-gruppen into development
All checks were successful
All checks were successful
Reviewed-on: #148 Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
This commit was merged in pull request #148.
This commit is contained in:
@@ -12,3 +12,7 @@ linter:
|
||||
unnecessary_const: true
|
||||
lines_longer_than_80_chars: false
|
||||
constant_identifier_names: false
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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/player.dart';
|
||||
|
||||
part 'group_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [GroupTable, PlayerGroupTable])
|
||||
@DriftAccessor(tables: [GroupTable, PlayerGroupTable, MatchTable])
|
||||
class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
GroupDao(super.db);
|
||||
|
||||
@@ -205,8 +206,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Retrieves the number of groups in the database.
|
||||
Future<int> getGroupCount() async {
|
||||
final count =
|
||||
@@ -235,10 +234,13 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
/// Replaces all players in a group with the provided list of players.
|
||||
/// Removes all existing players from the group and adds the new players.
|
||||
/// Also adds any new players to the player table if they don't exist.
|
||||
Future<void> replaceGroupPlayers({
|
||||
/// Returns `true` if the group exists and players were replaced, `false` otherwise.
|
||||
Future<bool> replaceGroupPlayers({
|
||||
required String groupId,
|
||||
required List<Player> newPlayers,
|
||||
}) async {
|
||||
if (!await groupExists(groupId: groupId)) return false;
|
||||
|
||||
await db.transaction(() async {
|
||||
// Remove all existing players from the group
|
||||
final deleteQuery = delete(db.playerGroupTable)
|
||||
@@ -270,5 +272,6 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ mixin _$GroupDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
|
||||
$PlayerGroupTableTable get playerGroupTable =>
|
||||
attachedDatabase.playerGroupTable;
|
||||
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||
$MatchTableTable get matchTable => attachedDatabase.matchTable;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,34 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves all matches associated with the given [groupId].
|
||||
/// Queries the database directly, filtering by [groupId].
|
||||
Future<List<Match>> 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<bool> matchExists({required String matchId}) async {
|
||||
@@ -338,6 +366,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// 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<bool> 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)),
|
||||
);
|
||||
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<bool> updateMatchCreatedAt({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,9 +7,11 @@ 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()();
|
||||
TextColumn get name => text().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()();
|
||||
TextColumn get notes => text().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get endedAt => dateTime().nullable()();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/constants.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
@@ -11,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<CreateGroupView> createState() => _CreateGroupViewState();
|
||||
}
|
||||
@@ -69,49 +72,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
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<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(
|
||||
child: Column(
|
||||
@@ -122,6 +82,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
child: TextInputField(
|
||||
controller: _groupNameController,
|
||||
hintText: loc.group_name,
|
||||
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -144,32 +105,33 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
(_groupNameController.text.isEmpty ||
|
||||
(selectedPlayers.length < 2))
|
||||
? null
|
||||
: () async {
|
||||
late Group? updatedGroup;
|
||||
late bool success;
|
||||
if (widget.groupToEdit == null) {
|
||||
success = await db.groupDao.addGroup(
|
||||
group: Group(
|
||||
name: _groupNameController.text.trim(),
|
||||
members: selectedPlayers,
|
||||
: _saveGroup,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
updatedGroup = Group(
|
||||
id: widget.groupToEdit!.id,
|
||||
name: _groupNameController.text.trim(),
|
||||
description: '',
|
||||
members: selectedPlayers,
|
||||
);
|
||||
//TODO: Implement group editing in database
|
||||
/*
|
||||
success = await db.groupDao.updateGroup(
|
||||
group: updatedGroup,
|
||||
);
|
||||
*/
|
||||
success = true;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
|
||||
/// Saves the group by creating a new one or updating the existing one,
|
||||
/// depending on whether the widget is in edit mode.
|
||||
Future<void> _saveGroup() async {
|
||||
final loc = AppLocalizations.of(context);
|
||||
late bool success;
|
||||
Group? updatedGroup;
|
||||
|
||||
if (widget.groupToEdit == null) {
|
||||
success = await _createGroup();
|
||||
} else {
|
||||
final result = await _editGroup();
|
||||
success = result.$1;
|
||||
updatedGroup = result.$2;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
Navigator.pop(context, updatedGroup);
|
||||
} else {
|
||||
@@ -179,14 +141,76 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
: loc.error_editing_group,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
/// Handles creating a new group and returns whether the operation was successful.
|
||||
Future<bool> _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,
|
||||
);
|
||||
await deleteObsoleteMatchGroupRelations();
|
||||
widget.onMembersChanged?.call();
|
||||
}
|
||||
|
||||
final success = successfullNameChange && successfullMemberChange;
|
||||
|
||||
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<void> deleteObsoleteMatchGroupRelations() async {
|
||||
final groupMatches = await db.matchDao.getGroupMatches(
|
||||
groupId: widget.groupToEdit!.id,
|
||||
);
|
||||
|
||||
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.removeMatchGroup(matchId: match.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a snackbar with the given message and optional action.
|
||||
|
||||
@@ -191,7 +191,12 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
return CreateGroupView(groupToEdit: _group);
|
||||
return CreateGroupView(
|
||||
groupToEdit: _group,
|
||||
onMembersChanged: () {
|
||||
_loadStatistics();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -242,10 +247,8 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
|
||||
/// Loads statistics for this group
|
||||
Future<void> _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.matchDao.getGroupMatches(groupId: _group.id);
|
||||
|
||||
setState(() {
|
||||
totalMatches = groupMatches.length;
|
||||
@@ -260,7 +263,9 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -363,6 +363,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
final match = widget.matchToEdit!;
|
||||
_matchNameController.text = match.name;
|
||||
selectedPlayers = match.players;
|
||||
selectedGameIndex = 0;
|
||||
|
||||
if (match.group != null) {
|
||||
selectedGroup = match.group;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
@@ -307,5 +307,69 @@ void main() {
|
||||
expect(fetchedMatch.winner, isNotNull);
|
||||
expect(fetchedMatch.winner!.id, testPlayer5.id);
|
||||
});
|
||||
|
||||
test(
|
||||
'removeMatchGroup removes group from match with existing group',
|
||||
() async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final removed = await database.matchDao.removeMatchGroup(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(removed, isTrue);
|
||||
|
||||
final updatedMatch = await database.matchDao.getMatchById(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(updatedMatch.group, null);
|
||||
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.removeMatchGroup(
|
||||
matchId: testMatchOnlyPlayers.id,
|
||||
);
|
||||
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.removeMatchGroup(
|
||||
matchId: 'non-existing-id',
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user