Skip to content

Commit

Permalink
Add option to filter and sort registrations in admin view (#373)
Browse files Browse the repository at this point in the history
  • Loading branch information
JAicewizard authored Dec 13, 2023
1 parent 8624915 commit f436678
Show file tree
Hide file tree
Showing 10 changed files with 647 additions and 125 deletions.
16 changes: 15 additions & 1 deletion lib/models/payment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ enum PaymentType {
@JsonValue('tpay_payment')
tpayPayment,
@JsonValue('wire_payment')
wirePayment,
wirePayment;

@override
String toString() {
switch (this) {
case PaymentType.cashPayment:
return 'Cash payment';
case PaymentType.cardPayment:
return 'Card payment';
case PaymentType.tpayPayment:
return 'Thalia Pay';
case PaymentType.wirePayment:
return 'Wire payment';
}
}
}

@JsonSerializable(fieldRename: FieldRename.snake)
Expand Down
298 changes: 233 additions & 65 deletions lib/ui/screens/event_admin_screen.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
Expand All @@ -20,6 +21,54 @@ class EventAdminScreen extends StatefulWidget {
}

class _EventAdminScreenState extends State<EventAdminScreen> {
static Filter<AdminEventRegistration> _defaultFilter() => MultipleFilter(
[
MapFilter<PaymentType?, AdminEventRegistration>(
map: {
for (PaymentType value in PaymentType.values) value: true,
null: true,
},
title: 'Payment type',
asString: (item) => item?.toString() ?? 'Not paid',
toKey: (item) => item.payment?.type),
MapFilter<bool, AdminEventRegistration>(
map: {
true: true,
false: true,
},
title: 'Precense',
asString: (item) => item ? 'Is present' : 'Is not present',
toKey: (item) => item.present),
],
);

Filter<AdminEventRegistration> _filter = _defaultFilter();

_SortOrder _sortOrder = _SortOrder.none;

void _resetfilter() {
setState(() {
_filter = _defaultFilter();
});
}

void _showPaymentFilter() async {
final Filter<AdminEventRegistration>? results = await showDialog(
context: context,
builder: (BuildContext context) {
return MultiSelectPopup(
filter: _filter.clone(),
title: 'Filter registrations',
);
},
);
if (results != null) {
setState(() {
_filter = results;
});
}
}

void _showQRCode(EventAdminCubit cubit) async {
showModalBottomSheet(
isScrollControlled: true,
Expand Down Expand Up @@ -80,6 +129,36 @@ class _EventAdminScreenState extends State<EventAdminScreen> {
);
}

void _openSearch(BuildContext context) async {
final adminCubit = BlocProvider.of<EventAdminCubit>(context);
// TODO: check if we need this second cubit!.
final searchCubit = EventAdminCubit(
RepositoryProvider.of<ApiRepository>(context),
eventPk: widget.pk,
);

await showSearch(
context: context,
delegate: EventAdminSearchDelegate(searchCubit),
);

searchCubit.close();

// After the search dialog closes, refresh the results,
// since the search screen may have changed stuff through
// its own EventAdminCubit, that do not show up in the cubit
// for the EventAdminScreen until a refresh.
adminCubit.loadRegistrations();
}

Widget _resetfilterMessage() {
return ErrorCenter([
const Text('No results that match the filter',
textAlign: TextAlign.center),
TextButton(onPressed: _resetfilter, child: const Text('Reset filter'))
]);
}

@override
Widget build(BuildContext context) {
return BlocProvider(
Expand All @@ -96,38 +175,30 @@ class _EventAdminScreenState extends State<EventAdminScreen> {
appBar: ThaliaAppBar(
title: const Text('REGISTRATIONS'),
collapsingActions: [
IconAppbarAction(
'SEARCH',
Icons.search,
() async {
final adminCubit =
BlocProvider.of<EventAdminCubit>(context);
final searchCubit = EventAdminCubit(
RepositoryProvider.of<ApiRepository>(context),
eventPk: widget.pk,
);

await showSearch(
context: context,
delegate: EventAdminSearchDelegate(searchCubit),
);

searchCubit.close();

// After the search dialog closes, refresh the results,
// since the search screen may have changed stuff through
// its own EventAdminCubit, that do not show up in the cubit
// for the EventAdminScreen until a refresh.
adminCubit.loadRegistrations();
},
),
IconAppbarAction(
'QR Code',
Icons.qr_code,
() =>
_showQRCode(BlocProvider.of<EventAdminCubit>(context)),
tooltip: 'Show presence QR code',
),
IconAppbarAction(
'SEARCH',
Icons.search,
() => _openSearch(context),
),
SortButton<_SortOrder>(
_SortOrder.values.map((e) => e.asSortItem()).toList(),
(p0) => setState(() {
_sortOrder = p0 ?? _SortOrder.none;
}),
),
IconAppbarAction(
'Filter',
Icons.filter_alt_rounded,
_showPaymentFilter,
tooltip: 'Filter registrations',
),
],
bottom: TabBar(
indicatorColor: Theme.of(context).colorScheme.primary,
Expand All @@ -151,52 +222,83 @@ class _EventAdminScreenState extends State<EventAdminScreen> {
} else if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
} else {
return TabBarView(children: [
if (state.queuedMessage != null)
ErrorCenter(state.queuedMessage!)
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_QueuedRegistrationTile(
registration: state.queuedRegistrations[index],
List<AdminEventRegistration> filteredRegistrations = state
.registrations
.where(_filter.passes)
.sorted(_sortOrder.compare)
.toList();
List<AdminEventRegistration> filteredCancels = state
.cancelledRegistrations
.where(_filter.passes)
.sorted(_sortOrder.compare)
.toList();
List<AdminEventRegistration> filteredQueue = state
.queuedRegistrations
.where(_filter.passes)
.sorted(_sortOrder.compare)
.toList();
return TabBarView(
children: [
if (state.queuedMessage != null)
ErrorCenter([
Text(state.queuedMessage!,
textAlign: TextAlign.center)
])
else if (filteredQueue.isEmpty)
_resetfilterMessage()
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_QueuedRegistrationTile(
registration: filteredQueue[index],
),
separatorBuilder: (_, __) => const Divider(),
itemCount: filteredQueue.length,
),
separatorBuilder: (_, __) => const Divider(),
itemCount: state.queuedRegistrations.length,
),
),
if (state.message != null)
ErrorCenter(state.message!)
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_RegistrationTile(
registration: state.registrations[index],
requiresPayment: state.event!.paymentIsRequired,
if (state.message != null)
ErrorCenter([
Text(state.message!, textAlign: TextAlign.center)
])
else if (filteredRegistrations.isEmpty)
_resetfilterMessage()
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_RegistrationTile(
registration: filteredRegistrations[index],
requiresPayment:
state.event!.paymentIsRequired,
),
separatorBuilder: (_, __) => const Divider(),
itemCount: filteredRegistrations.length,
),
separatorBuilder: (_, __) => const Divider(),
itemCount: state.registrations.length,
),
),
if (state.cancelledMessage != null)
ErrorCenter(state.cancelledMessage!)
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_CancelledRegistrationTile(
registration:
state.cancelledRegistrations[index],
if (state.cancelledMessage != null)
ErrorCenter([
Text(state.cancelledMessage!,
textAlign: TextAlign.center)
])
else if (filteredCancels.isEmpty)
_resetfilterMessage()
else
Scrollbar(
child: ListView.separated(
key: const PageStorageKey('event-admin'),
itemBuilder: (context, index) =>
_CancelledRegistrationTile(
registration: filteredCancels[index],
),
separatorBuilder: (_, __) => const Divider(),
itemCount: filteredCancels.length,
),
separatorBuilder: (_, __) => const Divider(),
itemCount: state.cancelledRegistrations.length,
),
),
]);
],
);
}
},
),
Expand Down Expand Up @@ -510,3 +612,69 @@ class EventAdminSearchDelegate extends SearchDelegate {
);
}
}

enum _SortOrder {
none(text: 'None', icon: Icons.cancel, compare: equal),
payedUp(text: 'Paid', icon: Icons.keyboard_arrow_up, compare: cmpPaid),
payedDown(text: 'Paid', icon: Icons.keyboard_arrow_down, compare: cmpPaid_2),
presentUp(
text: 'Present', icon: Icons.keyboard_arrow_up, compare: cmpPresent),
presentDown(
text: 'Present', icon: Icons.keyboard_arrow_down, compare: cmpPresent_2),
nameUp(text: 'Name', icon: Icons.keyboard_arrow_up, compare: cmpName),
nameDown(text: 'Name', icon: Icons.keyboard_arrow_down, compare: cmpName_2);

final String text;
final IconData? icon;
final int Function(AdminEventRegistration, AdminEventRegistration) compare;

const _SortOrder({required this.text, this.icon, required this.compare});

SortItem<_SortOrder> asSortItem() {
return SortItem(this, text, icon);
}

static int equal(AdminEventRegistration e1, AdminEventRegistration e2) {
return 0;
}

static int cmpPaid(AdminEventRegistration e1, AdminEventRegistration e2) {
if (e1.isPaid) {
return -1;
}
if (e2.isPaid) {
return 1;
}
return 0;
}

static int cmpPaid_2(AdminEventRegistration e1, AdminEventRegistration e2) =>
-cmpPaid(e1, e2);

static int cmpPresent(AdminEventRegistration e1, AdminEventRegistration e2) {
if (e1.present) {
return -1;
}
if (e2.present) {
return 1;
}
return 0;
}

static int cmpPresent_2(
AdminEventRegistration e1, AdminEventRegistration e2) =>
-cmpPresent(e1, e2);

static int cmpName(AdminEventRegistration e1, AdminEventRegistration e2) {
if (e1.name == null) {
return -1;
}
if (e2.name == null) {
return 1;
}
return e1.name!.compareTo(e2.name!);
}

static int cmpName_2(AdminEventRegistration e1, AdminEventRegistration e2) =>
-cmpName(e1, e2);
}
Loading

0 comments on commit f436678

Please sign in to comment.