diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/bout_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/bout_edit.dart index 7b01c56e..6b57525a 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/bout_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/bout_edit.dart @@ -75,7 +75,7 @@ abstract class BoutEditState extends ConsumerState implem deleteParticipantState: (participation) => _blueParticipation = null, ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.fitness_center), selectedItem: _weightClass, label: AppLocalizations.of(context)!.weightClass, @@ -84,12 +84,9 @@ abstract class BoutEditState extends ConsumerState implem _weightClass = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { final boutWeightClasses = await availableWeightClasses; - return (filter == null - ? boutWeightClasses - : boutWeightClasses.where((element) => element.name.contains(filter))) - .toList(); + return boutWeightClasses.toList(); }, ), ), @@ -234,7 +231,7 @@ class ParticipantSelectTile extends ConsumerWidget { flex: 80, child: Container( padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8), - child: getDropdown( + child: SearchableDropdown( selectedItem: participation, label: label, context: context, @@ -251,7 +248,7 @@ class ParticipantSelectTile extends ConsumerWidget { } }, itemAsString: (u) => u.membership.person.fullName, - onFind: (String? filter) => _filterParticipants(ref, filter, lineup), + asyncItems: (String filter) => _filterParticipants(ref, filter, lineup), ), ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/club_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/club_edit.dart index 2cd4fd03..3c2a9542 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/club_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/club_edit.dart @@ -65,7 +65,7 @@ class ClubEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.corporate_fare), selectedItem: _organization, label: localizations.organization, @@ -75,13 +75,10 @@ class ClubEditState extends ConsumerState { }), allowEmpty: false, itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableOrganizations ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableOrganizations! - : _availableOrganizations!.where((element) => element.name.contains(filter))) - .toList(); + return _availableOrganizations!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/lineup_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/lineup_edit.dart index 46e4ee4e..354e6dd2 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/lineup_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/lineup_edit.dart @@ -116,7 +116,7 @@ class LineupEditState extends ConsumerState { items: [ ListTile(title: HeadingText(widget.lineup.team.name)), ListTile( - title: getDropdown( + title: SearchableDropdown( selectedItem: _leader, label: localizations.leader, context: context, @@ -124,25 +124,27 @@ class LineupEditState extends ConsumerState { _leader = value; }), itemAsString: (u) => u.person.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _memberships ??= await _getMemberships(ref, club: widget.lineup.team.club); return _filterMemberships(ref, filter, widget.lineup, _memberships!); }, + isFilterOnline: true, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( selectedItem: _coach, label: localizations.coach, context: context, onSaved: (Membership? value) => setState(() { _coach = value; }), - itemAsString: (u) => u.person.fullName, - onFind: (String? filter) async { + itemAsString: (u) => u.info, + asyncItems: (String filter) async { _memberships ??= await _getMemberships(ref, club: widget.lineup.team.club); return _filterMemberships(ref, filter, widget.lineup, _memberships!); }, + isFilterOnline: true, ), ), ..._participations.entries.map((mapEntry) { @@ -233,7 +235,7 @@ class _ParticipationEditTileState extends ConsumerState { flex: 80, child: Container( padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8), - child: getDropdown( + child: SearchableDropdown( selectedItem: widget.participation?.membership, label: '${localizations.weightClass} ${widget.weightClass.name} ${widget.weightClass.style.abbreviation(context)}', @@ -242,11 +244,12 @@ class _ParticipationEditTileState extends ConsumerState { _curMembership = newMembership; }, onSaved: (Membership? newMembership) => onSave(), - itemAsString: (u) => u.person.fullName, - onFind: (String? filter) async { + itemAsString: (u) => u.info, + asyncItems: (String filter) async { _memberships ??= await _getMemberships(ref, club: widget.lineup.team.club); return _filterMemberships(ref, filter, widget.lineup, _memberships!); }, + isFilterOnline: true, ), ), ), @@ -277,12 +280,24 @@ class _ParticipationEditTileState extends ConsumerState { Future> _filterMemberships( WidgetRef ref, - String? filter, + String filter, Lineup lineup, Iterable memberships, ) async { - return (filter == null ? memberships : memberships.where((element) => element.person.fullName.contains(filter))) - .toList(); + filter = filter.trim().toLowerCase(); + if (filter.isEmpty) { + return memberships.toList(); + } + final number = int.tryParse(filter); + if (number == null) { + return memberships.where((item) => item.person.fullName.toLowerCase().contains(filter)).toList(); + } + + // If filter string is a number, search for membership no or at API provider, if present. + filter = number.toString(); + final filteredMemberships = + memberships.where((item) => (item.orgSyncId?.contains(filter) ?? false) || (item.no?.contains(filter) ?? false)); + return filteredMemberships.toList(); } Future> _getMemberships(WidgetRef ref, {required Club club}) async { diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/organization_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/organization_edit.dart index 1dfcc2b2..dc9f83a0 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/organization_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/organization_edit.dart @@ -81,7 +81,7 @@ class _OrganizationEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.corporate_fare), selectedItem: _parent, label: localizations.organization, @@ -91,13 +91,10 @@ class _OrganizationEditState extends ConsumerState { }), allowEmpty: true, itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableOrganizations ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableOrganizations! - : _availableOrganizations!.where((element) => element.name.contains(filter))) - .toList(); + return _availableOrganizations!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/person_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/person_edit.dart index 318c4c50..0ec296fe 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/person_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/person_edit.dart @@ -123,7 +123,7 @@ abstract class PersonEditState extends ConsumerState im ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( // TODO: replace icon with home_pin when available, also in overview // https://github.com/flutter/flutter/issues/102560 icon: const Icon(Icons.location_on), @@ -134,11 +134,11 @@ abstract class PersonEditState extends ConsumerState im _nationality = value; }), itemAsString: (u) => '${u.nationality} (${u.isoShortName})', - onFind: (String? filter) async { - return (filter == null - ? Countries.values - : Countries.values.where((element) => element.nationality.contains(filter))) - .toList(); + asyncItems: (String filter) async { + return Countries.values; + }, + onFilter: (Country item, String filter) { + return item.nationality.toLowerCase().contains(filter); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/team_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/team_edit.dart index 8f0db58a..5b2d5846 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/team_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/team_edit.dart @@ -65,7 +65,7 @@ class TeamEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.foundation), selectedItem: _club, label: localizations.club, @@ -74,12 +74,9 @@ class TeamEditState extends ConsumerState { _club = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableClubs ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableClubs! - : _availableClubs!.where((element) => element.name.contains(filter))) - .toList(); + return _availableClubs!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/division_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/division_edit.dart index 62d898a6..d4228179 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/division_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/division_edit.dart @@ -127,7 +127,7 @@ class DevisionEditState extends BoutConfigEditState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.corporate_fare), selectedItem: _organization, label: localizations.organization, @@ -137,18 +137,15 @@ class DevisionEditState extends BoutConfigEditState { }), allowEmpty: false, itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableOrganizations ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableOrganizations! - : _availableOrganizations!.where((element) => element.name.contains(filter))) - .toList(); + return _availableOrganizations!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.inventory), selectedItem: _parentDivision, label: localizations.division, @@ -157,12 +154,9 @@ class DevisionEditState extends BoutConfigEditState { _parentDivision = value; }), itemAsString: (u) => u.fullname, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableDivisions ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableDivisions! - : _availableDivisions!.where((element) => element.fullname.contains(filter))) - .toList(); + return _availableDivisions!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_edit.dart index b46265d2..87258e84 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_edit.dart @@ -105,7 +105,7 @@ class LeagueEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.inventory), selectedItem: _division, label: localizations.division, @@ -115,12 +115,9 @@ class LeagueEditState extends ConsumerState { }), itemAsString: (u) => u.fullname, allowEmpty: false, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableDivisions ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableDivisions! - : _availableDivisions!.where((element) => element.fullname.contains(filter))) - .toList(); + return _availableDivisions!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_team_participation_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_team_participation_edit.dart index 185b03f4..8bc750f9 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_team_participation_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/league_team_participation_edit.dart @@ -44,7 +44,7 @@ class TeamEditState extends ConsumerState { final items = [ ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.group), selectedItem: _team, label: localizations.team, @@ -53,17 +53,14 @@ class TeamEditState extends ConsumerState { _team = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { availableTeams ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? availableTeams! - : availableTeams!.where((element) => element.name.contains(filter))) - .toList(); + return availableTeams!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.emoji_events), selectedItem: _league, label: localizations.league, @@ -72,12 +69,9 @@ class TeamEditState extends ConsumerState { _league = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableLeagues ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableLeagues! - : _availableLeagues!.where((element) => element.name.contains(filter))) - .toList(); + return _availableLeagues!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/team_match_edit.dart b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/team_match_edit.dart index 00430bce..d29c59fe 100644 --- a/wrestling_scoreboard_client/lib/view/screens/edit/team_match/team_match_edit.dart +++ b/wrestling_scoreboard_client/lib/view/screens/edit/team_match/team_match_edit.dart @@ -123,7 +123,7 @@ class TeamMatchEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.group), selectedItem: _homeTeam, label: '${localizations.team} ${localizations.red}', @@ -132,18 +132,15 @@ class TeamMatchEditState extends ConsumerState { _homeTeam = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { - // TODO: filter by teams of same league, but may add an option to search all teams + asyncItems: (String filter) async { + // TODO: filter by teams of same league, but may add an option to search all teams, needs isFilterOnline option _availableTeams ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableTeams! - : _availableTeams!.where((element) => element.name.contains(filter))) - .toList(); + return _availableTeams!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.group), selectedItem: _guestTeam, label: '${localizations.team} ${localizations.blue}', @@ -152,13 +149,10 @@ class TeamMatchEditState extends ConsumerState { _guestTeam = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { // TODO: filter by teams of same league, but may add an option to search all teams _availableTeams ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableTeams! - : _availableTeams!.where((element) => element.name.contains(filter))) - .toList(); + return _availableTeams!.toList(); }, ), ), @@ -174,7 +168,7 @@ class TeamMatchEditState extends ConsumerState { ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.emoji_events), selectedItem: _league, label: localizations.league, @@ -183,12 +177,9 @@ class TeamMatchEditState extends ConsumerState { _league = value; }), itemAsString: (u) => u.name, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availableLeagues ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availableLeagues! - : _availableLeagues!.where((element) => element.name.contains(filter))) - .toList(); + return _availableLeagues!.toList(); }, ), ), @@ -205,7 +196,7 @@ class TeamMatchEditState extends ConsumerState { ), HeadingItem(title: localizations.persons), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.sports), selectedItem: _referee, label: localizations.referee, @@ -214,17 +205,14 @@ class TeamMatchEditState extends ConsumerState { _referee = value; }), itemAsString: (u) => u.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availablePersons ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availablePersons! - : _availablePersons!.where((element) => element.fullName.contains(filter))) - .toList(); + return _availablePersons!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.manage_accounts), selectedItem: _matChairman, label: localizations.matChairman, @@ -233,17 +221,14 @@ class TeamMatchEditState extends ConsumerState { _matChairman = value; }), itemAsString: (u) => u.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availablePersons ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availablePersons! - : _availablePersons!.where((element) => element.fullName.contains(filter))) - .toList(); + return _availablePersons!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.manage_accounts), selectedItem: _judge, label: localizations.judge, @@ -252,17 +237,14 @@ class TeamMatchEditState extends ConsumerState { _judge = value; }), itemAsString: (u) => u.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availablePersons ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availablePersons! - : _availablePersons!.where((element) => element.fullName.contains(filter))) - .toList(); + return _availablePersons!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.pending_actions), selectedItem: _timeKeeper, label: localizations.timeKeeper, @@ -271,17 +253,14 @@ class TeamMatchEditState extends ConsumerState { _timeKeeper = value; }), itemAsString: (u) => u.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availablePersons ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availablePersons! - : _availablePersons!.where((element) => element.fullName.contains(filter))) - .toList(); + return _availablePersons!.toList(); }, ), ), ListTile( - title: getDropdown( + title: SearchableDropdown( icon: const Icon(Icons.history_edu), selectedItem: _transcriptWriter, label: localizations.transcriptionWriter, @@ -290,12 +269,9 @@ class TeamMatchEditState extends ConsumerState { _transcriptWriter = value; }), itemAsString: (u) => u.fullName, - onFind: (String? filter) async { + asyncItems: (String filter) async { _availablePersons ??= await (await ref.read(dataManagerNotifierProvider)).readMany(); - return (filter == null - ? _availablePersons! - : _availablePersons!.where((element) => element.fullName.contains(filter))) - .toList(); + return _availablePersons!.toList(); }, ), ), diff --git a/wrestling_scoreboard_client/lib/view/widgets/dropdown.dart b/wrestling_scoreboard_client/lib/view/widgets/dropdown.dart index 3fae3ab0..ef483816 100644 --- a/wrestling_scoreboard_client/lib/view/widgets/dropdown.dart +++ b/wrestling_scoreboard_client/lib/view/widgets/dropdown.dart @@ -2,33 +2,55 @@ import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -Widget getDropdown({ - required T? selectedItem, - required String label, - void Function(T? value)? onChanged, - void Function(T? value)? onSaved, - required Future> Function(String? filter) onFind, - required String Function(T u) itemAsString, - bool allowEmpty = true, - required BuildContext context, - Widget? icon, -}) { - return DropdownSearch( - dropdownDecoratorProps: DropDownDecoratorProps( - dropdownSearchDecoration: InputDecoration(labelText: label, icon: icon), - ), - popupProps: const PopupProps.menu( - searchFieldProps: TextFieldProps(decoration: InputDecoration(prefixIcon: Icon(Icons.search))), - showSearchBox: true, - ), - clearButtonProps: const ClearButtonProps(isVisible: true), - asyncItems: (String? filter) => onFind(filter), - itemAsString: (T? u) => u != null ? itemAsString(u) : 'empty value', - selectedItem: selectedItem, - onChanged: onChanged, - validator: (val) => (val == null && !allowEmpty) ? 'This field is mandatory' : null, - onSaved: onSaved, - ); +class SearchableDropdown extends StatelessWidget { + final T? selectedItem; + final String label; + final void Function(T? value)? onChanged; + final void Function(T? value)? onSaved; + final Future> Function(String filter) asyncItems; + final bool Function(T item, String filter)? onFilter; + final String Function(T u) itemAsString; + final bool allowEmpty; + final BuildContext context; + final Widget? icon; + final bool isFilterOnline; + + const SearchableDropdown({ + required this.selectedItem, + required this.label, + this.onChanged, + this.onSaved, + required this.asyncItems, + this.onFilter, + required this.itemAsString, + this.allowEmpty = true, + required this.context, + this.icon, + this.isFilterOnline = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return DropdownSearch( + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration(labelText: label, icon: icon), + ), + popupProps: PopupProps.menu( + searchFieldProps: const TextFieldProps(decoration: InputDecoration(prefixIcon: Icon(Icons.search))), + showSearchBox: true, + isFilterOnline: isFilterOnline, + ), + clearButtonProps: const ClearButtonProps(isVisible: true), + asyncItems: asyncItems, + filterFn: onFilter, + itemAsString: (T? u) => u != null ? itemAsString(u) : 'empty value', + selectedItem: selectedItem, + onChanged: onChanged, + validator: (val) => (val == null && !allowEmpty) ? 'This field is mandatory' : null, + onSaved: onSaved, + ); + } } class SimpleDropdown extends StatelessWidget { diff --git a/wrestling_scoreboard_common/lib/src/data/membership.dart b/wrestling_scoreboard_common/lib/src/data/membership.dart index 2427af54..9098a019 100644 --- a/wrestling_scoreboard_common/lib/src/data/membership.dart +++ b/wrestling_scoreboard_common/lib/src/data/membership.dart @@ -48,7 +48,7 @@ class Membership with _$Membership implements DataObject { } String get info { - return '${no ?? '-'},\t ${person.fullName},\t${person.age}'; + return '${no == null ? '' : 'No. $no, '}\t ${person.fullName},\t${person.age}'; } @override