From 96fb14e7de58b741cf48e8ddcaf784bbe25105d2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 22 Apr 2021 15:41:35 +0200 Subject: [PATCH] Display Forez presence --- lib/helpers/forez_presence_helper.dart | 76 ++++++++ lib/lists/base_set.dart | 30 ++++ lib/lists/forez_presences_set.dart | 51 ++++++ lib/models/forez_presence.dart | 29 +++ .../screens/authorized_group_page_screen.dart | 2 +- .../forez_presence_section.dart | 73 +++++++- .../forez_presence_calendar_widget.dart | 165 ++++++++++++++++++ pubspec.lock | 14 ++ pubspec.yaml | 5 +- 9 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 lib/helpers/forez_presence_helper.dart create mode 100644 lib/lists/base_set.dart create mode 100644 lib/lists/forez_presences_set.dart create mode 100644 lib/models/forez_presence.dart create mode 100644 lib/ui/widgets/forez_presence_calendar_widget.dart diff --git a/lib/helpers/forez_presence_helper.dart b/lib/helpers/forez_presence_helper.dart new file mode 100644 index 0000000..137d14b --- /dev/null +++ b/lib/helpers/forez_presence_helper.dart @@ -0,0 +1,76 @@ +import 'package:comunic/helpers/websocket_helper.dart'; +import 'package:comunic/lists/forez_presences_set.dart'; +import 'package:comunic/models/forez_presence.dart'; + +/// Presence helper +/// +/// @author Pierre Hubert + +int _cachedGroup; +PresenceSet _cache; + +class ForezPresenceHelper { + /// Refresh presence cache + /// + /// Throws in case of failure + static Future refreshCache(int groupID) async { + final response = await ws("forez_presence/list", {"group": groupID}); + + final list = response + .cast() + .map((element) { + final cut = element.split(",").map((e) => int.parse(e)).toList(); + assert(cut.length == 4); + + return Presence( + userID: cut[0], year: cut[1], month: cut[2], day: cut[3]); + }) + .toList() + .cast(); + + _cachedGroup = groupID; + _cache = PresenceSet()..addAll(list); + } + + /// Initialize cache if required + static Future _checkCache(int groupID) async { + if (_cache == null || _cachedGroup != groupID) await refreshCache(groupID); + } + + /// Get the presences of a given user + /// + /// Throws in case of failure + static Future getForUser(int groupID, int userID) async { + await _checkCache(groupID); + + return _cache.getForUser(userID); + } + + /// Get all the available presences + /// + /// Throws in case of failure + static Future getAll(int groupID) async { + await _checkCache(groupID); + return _cache; + } + + /// Add a new day of presence + /// + /// Throws in case of failure + static Future addDay(DateTime dt) async => + await ws("forez_presence/add_day", { + "year": dt.year, + "month": dt.month, + "day": dt.day, + }); + + /// Remove a new day of presence + /// + /// Throws in case of failure + static Future delDay(DateTime dt) async => + await ws("forez_presence/del_day", { + "year": dt.year, + "month": dt.month, + "day": dt.day, + }); +} diff --git a/lib/lists/base_set.dart b/lib/lists/base_set.dart new file mode 100644 index 0000000..d67724f --- /dev/null +++ b/lib/lists/base_set.dart @@ -0,0 +1,30 @@ +import 'dart:collection'; + +/// Base set +/// +/// @author pierre Hubert + +class BaseSet extends SetBase { + final _set = new Set(); + + @override + bool add(T value) => _set.add(value); + + @override + bool contains(Object element) => _set.contains(element); + + @override + Iterator get iterator => _set.iterator; + + @override + int get length => _set.length; + + @override + T lookup(Object element) => _set.lookup(element); + + @override + bool remove(Object value) => _set.remove(value); + + @override + Set toSet() => _set.toSet(); +} diff --git a/lib/lists/forez_presences_set.dart b/lib/lists/forez_presences_set.dart new file mode 100644 index 0000000..f733e58 --- /dev/null +++ b/lib/lists/forez_presences_set.dart @@ -0,0 +1,51 @@ +import 'package:comunic/lists/base_set.dart'; +import 'package:comunic/models/forez_presence.dart'; + +/// Forez presence set +/// +/// @author Pierre Hubert + +class PresenceSet extends BaseSet { + /// Get the presence of a specific user + PresenceSet getForUser(int userID) => + PresenceSet()..addAll(where((element) => element.userID == userID)); + + bool containsDate(DateTime dt) => any( + (element) => + element.year == dt.year && + element.month == dt.month && + element.day == dt.day, + ); + + void removeDate(DateTime dt) => removeWhere( + (element) => + element.year == dt.year && + element.month == dt.month && + element.day == dt.day, + ); + + void toggleDate(DateTime dt, int userID) { + if (containsDate(dt)) + removeDate(dt); + else + add(Presence.fromDateTime(dt, userID)); + } + + int countAtDate(DateTime dt) => where( + (element) => + element.year == dt.year && + element.month == dt.month && + element.day == dt.day, + ).length; + + /// Get the list of users present at a specified date + List getUsersAtDate(DateTime dt) => where( + (element) => + element.year == dt.year && + element.month == dt.month && + element.day == dt.day, + ).map((e) => e.userID).toList(); + + /// Get the ID of all the users referenced in this set + Set get usersID => map((element) => element.userID).toSet(); +} diff --git a/lib/models/forez_presence.dart b/lib/models/forez_presence.dart new file mode 100644 index 0000000..236bb9a --- /dev/null +++ b/lib/models/forez_presence.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +/// Single presence information +/// +/// @author Pierre Hubert + +class Presence { + final int userID; + final int year; + final int month; + final int day; + + const Presence({ + @required this.userID, + @required this.year, + @required this.month, + @required this.day, + }) : assert(userID != null), + assert(year != null), + assert(month != null), + assert(day != null); + + Presence.fromDateTime(DateTime dt, this.userID) + : assert(dt != null), + year = dt.year, + month = dt.month, + day = dt.day, + assert(userID != null); +} diff --git a/lib/ui/screens/authorized_group_page_screen.dart b/lib/ui/screens/authorized_group_page_screen.dart index 858dd78..a993585 100644 --- a/lib/ui/screens/authorized_group_page_screen.dart +++ b/lib/ui/screens/authorized_group_page_screen.dart @@ -60,7 +60,7 @@ class _AuthorizedGroupPageScreenState // Forez presence tab _GroupPageTab( - widget: (c) => ForezPresenceSection(), + widget: (c) => ForezPresenceSection(groupID: _group.id), label: tr("Presence"), visible: _group.isForezGroup, ), diff --git a/lib/ui/screens/group_sections/forez_presence_section.dart b/lib/ui/screens/group_sections/forez_presence_section.dart index bc2876f..a47f37a 100644 --- a/lib/ui/screens/group_sections/forez_presence_section.dart +++ b/lib/ui/screens/group_sections/forez_presence_section.dart @@ -1,3 +1,10 @@ +import 'package:comunic/helpers/forez_presence_helper.dart'; +import 'package:comunic/helpers/users_helper.dart'; +import 'package:comunic/lists/forez_presences_set.dart'; +import 'package:comunic/lists/users_list.dart'; +import 'package:comunic/ui/widgets/account_image_widget.dart'; +import 'package:comunic/ui/widgets/async_screen_widget.dart'; +import 'package:comunic/ui/widgets/forez_presence_calendar_widget.dart'; import 'package:flutter/material.dart'; /// Forez presence section @@ -5,13 +12,77 @@ import 'package:flutter/material.dart'; /// @author Pierre Hubert class ForezPresenceSection extends StatefulWidget { + final int groupID; + + const ForezPresenceSection({ + Key key, + @required this.groupID, + }) : assert(groupID != null), + super(key: key); + @override _ForezPresenceSectionState createState() => _ForezPresenceSectionState(); } class _ForezPresenceSectionState extends State { + PresenceSet _presences; + UsersList _users; + DateTime _currentDay = DateTime.now(); + + List get _currentListOfUsers => _presences.getUsersAtDate(_currentDay); + + Future _refresh() async { + await ForezPresenceHelper.refreshCache(widget.groupID); + + _presences = await ForezPresenceHelper.getAll(widget.groupID); + _users = await UsersHelper().getList(_presences.usersID); + } + @override Widget build(BuildContext context) { - return Container(); + return Stack( + children: [ + AsyncScreenWidget( + onReload: _refresh, + onBuild: _buildList, + errorMessage: "Erreur lors du chargement des présences !", + ), + Positioned( + right: 10, + bottom: 10, + child: FloatingActionButton( + backgroundColor: Colors.green, + onPressed: () {}, + child: Icon(Icons.edit), + ), + ) + ], + ); + } + + Widget _buildList() { + final currentList = _currentListOfUsers; + return ListView.builder( + itemCount: currentList.length + 1, + itemBuilder: (c, i) => + i == 0 ? _buildCalendar() : _buildUser(currentList[i - 1]), + ); + } + + Widget _buildCalendar() => PresenceCalendarWidget( + presenceSet: _presences, + mode: CalendarDisplayMode.MULTIPLE_USERS, + selectedDay: _currentDay, + onDayClicked: _selectDay, + ); + + void _selectDay(DateTime dt) => setState(() => _currentDay = dt); + + Widget _buildUser(int userID) { + final user = _users.getUser(userID); + return ListTile( + leading: AccountImageWidget(user: user), + title: Text(user.fullName), + ); } } diff --git a/lib/ui/widgets/forez_presence_calendar_widget.dart b/lib/ui/widgets/forez_presence_calendar_widget.dart new file mode 100644 index 0000000..b501a04 --- /dev/null +++ b/lib/ui/widgets/forez_presence_calendar_widget.dart @@ -0,0 +1,165 @@ +import 'package:comunic/lists/forez_presences_set.dart'; +import 'package:comunic/utils/date_utils.dart' as date_utils; +/// Forez presence calendar widget +/// +/// This widget is used only by Forez groups +/// +/// @author Pierre Hubert +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; + +enum CalendarDisplayMode { SINGLE_USER, MULTIPLE_USERS } + +extension DateOnlyCompare on DateTime { + bool isSameDate(DateTime other) => date_utils.isSameDate(this, other); +} + +class PresenceCalendarWidget extends StatefulWidget { + final PresenceSet presenceSet; + final void Function(DateTime) onDayClicked; + final CalendarDisplayMode mode; + final DateTime selectedDay; + + const PresenceCalendarWidget({ + Key key, + @required this.presenceSet, + this.onDayClicked, + this.mode = CalendarDisplayMode.SINGLE_USER, + this.selectedDay, + }) : assert(presenceSet != null), + assert(mode != null), + super(key: key); + + @override + _PresenceCalendarWidgetState createState() => _PresenceCalendarWidgetState(); +} + +class _PresenceCalendarWidgetState extends State { + CalendarController _calendarController; + + @override + void initState() { + super.initState(); + _calendarController = CalendarController(); + } + + @override + void dispose() { + _calendarController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TableCalendar( + calendarController: _calendarController, + locale: "fr_FR", + weekendDays: [], + onHeaderTapped: _pickDate, + builders: CalendarBuilders(dayBuilder: _dayBuilder), + onDaySelected: _selectedDay, + availableCalendarFormats: const {CalendarFormat.month: "Mois"}, + ); + } + + void _pickDate(DateTime date) async { + final pickedDate = await showDatePicker( + context: context, + initialDate: date, + firstDate: DateTime.now().subtract(Duration(days: 20)), + lastDate: DateTime.now().add(Duration(days: 365 * 5)), + ); + + if (pickedDate != null) { + _calendarController.setSelectedDay(pickedDate, animate: true); + setState(() {}); + } + } + + Widget _dayBuilder( + BuildContext context, DateTime date, List events) { + if (widget.presenceSet.containsDate(date)) { + // Show the number of users who are present + if (widget.mode == CalendarDisplayMode.MULTIPLE_USERS) + return Stack( + children: [ + CellWidget( + text: date.day.toString(), + color: Colors.green, + textColor: Colors.white, + circle: false, + selected: date.isSameDate(widget.selectedDay), + ), + Positioned( + child: Material( + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Text(widget.presenceSet.countAtDate(date).toString()), + ), + textStyle: TextStyle(color: Colors.white), + color: Colors.red, + ), + bottom: 4, + right: 4, + ), + ], + ); + + // Only show green circle + else + return CellWidget( + text: date.day.toString(), + color: Colors.green, + textColor: Colors.white, + ); + } + + return CellWidget( + text: date.day.toString(), + selected: date.isSameDate(widget.selectedDay), + ); + } + + void _selectedDay( + DateTime day, List events, List holidays) { + if (widget.onDayClicked != null) widget.onDayClicked(day); + } +} + +class CellWidget extends StatelessWidget { + final String text; + final Color color; + final Color textColor; + final bool circle; + final bool selected; + + const CellWidget({ + Key key, + @required this.text, + this.color, + this.textColor, + this.circle = true, + this.selected, + }) : assert(text != null), + super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 250), + decoration: _buildCellDecoration(), + margin: const EdgeInsets.all(6.0), + alignment: Alignment.center, + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle(color: selected ?? false ? Colors.white : textColor), + ), + ); + } + + Decoration _buildCellDecoration() => BoxDecoration( + shape: circle ? BoxShape.circle : BoxShape.rectangle, + color: selected ?? false ? Colors.deepPurple : color, + ); +} diff --git a/pubspec.lock b/pubspec.lock index 9015b24..08b1969 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -555,6 +555,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.2+7" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" sky_engine: dependency: transitive description: flutter @@ -609,6 +616,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.3" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bd597fb..0799383 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ description: Comunic client version: 1.1.5+9 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: flutter: @@ -130,6 +130,9 @@ dependencies: firebase_core: ^1.0.1 firebase_messaging: ^9.0.0 + # Forez presence + table_calendar: ^2.3.3 + dev_dependencies: flutter_test: sdk: flutter