From 53fb9a2e2cc119119da555e3469b59fd10b22271 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Mon, 22 Jul 2024 10:58:11 +0200 Subject: [PATCH] feat: add method for applying a template in the availability viewmodel This code determines if a selected range of availabilities has the same start and end times and looks at the breaks. --- .../ui/screens/availability_modification.dart | 11 +- .../view_models/availability_view_model.dart | 189 +++++++++++++++--- .../lib/src/ui/widgets/calendar.dart | 2 +- .../lib/src/ui/widgets/calendar_grid.dart | 14 +- .../lib/src/models/availability.dart | 41 ++-- .../lib/src/utils.dart | 9 + 6 files changed, 219 insertions(+), 47 deletions(-) create mode 100644 packages/flutter_availability_data_interface/lib/src/utils.dart diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart index b46b295..5916657 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart @@ -49,7 +49,10 @@ class AvailabilitiesModificationScreen extends StatefulWidget { class _AvailabilitiesModificationScreenState extends State { late AvailabilityViewModel _availabilityViewModel = - AvailabilityViewModel.fromModel(widget.initialAvailabilities); + AvailabilityViewModel.fromModel( + widget.initialAvailabilities, + widget.dateRange, + ); @override Widget build(BuildContext context) { @@ -117,16 +120,14 @@ class _AvailabilitiesModificationScreenState if (template != null) { setState(() { _availabilityViewModel = - _availabilityViewModel.copyWith(templates: [template]); + _availabilityViewModel.applyTemplate(template); }); } } void onTemplatesRemoved() { setState(() { - _availabilityViewModel = _availabilityViewModel.copyWith( - templates: [], - ); + _availabilityViewModel = _availabilityViewModel.removeTemplates(); }); } diff --git a/packages/flutter_availability/lib/src/ui/view_models/availability_view_model.dart b/packages/flutter_availability/lib/src/ui/view_models/availability_view_model.dart index 8915de7..f827a65 100644 --- a/packages/flutter_availability/lib/src/ui/view_models/availability_view_model.dart +++ b/packages/flutter_availability/lib/src/ui/view_models/availability_view_model.dart @@ -1,62 +1,115 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/service/availability_service.dart"; import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; +import "package:flutter_availability/src/util/utils.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; -/// +/// The view model for the availability modification screen +/// This view model is used to manage the state of the availabilities while +/// editing them or creating new ones class AvailabilityViewModel { /// const AvailabilityViewModel({ + required this.selectedRange, this.templates = const [], this.breaks = const [], - this.id, + this.ids = const [], this.userId, this.startTime, this.endTime, this.clearAvailability = false, + this.conflictingPauses = false, + this.conflictingTime = false, + this.templateSelected = false, }); - /// + /// This constructor creates a [AvailabilityViewModel] from a list of + /// [AvailabilityWithTemplate] models + /// It will check if the models have the same start and end time, breaks and + /// if the entire selected range is covered by the models factory AvailabilityViewModel.fromModel( List models, + DateTimeRange range, ) { - var model = models.firstOrNull?.availabilityModel; + var coveredByAvailabilities = models.length == (range.duration.inDays + 1); + var userId = models.firstOrNull?.availabilityModel.userId; + // if there is no availability there is no conflicting time or pauses + var conflictingPauses = models.isNotEmpty; + var conflictingTime = models.isNotEmpty; + TimeOfDay? startTime; + TimeOfDay? endTime; + var breaks = []; + + if (coveredByAvailabilities) { + var availabilities = models.getAvailabilities(); + if (_availabilityTimesAreEqual(availabilities)) { + conflictingTime = false; + startTime = TimeOfDay.fromDateTime(availabilities.first.startDate); + endTime = TimeOfDay.fromDateTime(availabilities.first.endDate); + } + if (_availabilityBreaksAreEqual(availabilities)) { + conflictingPauses = false; + breaks = availabilities.first.breaks + .map(BreakViewModel.fromAvailabilityBreakModel) + .toList(); + } + } - var startTime = - model != null ? TimeOfDay.fromDateTime(model.startDate) : null; - var endTime = model != null ? TimeOfDay.fromDateTime(model.endDate) : null; return AvailabilityViewModel( templates: models.getUniqueTemplates(), - breaks: model?.breaks - .map(BreakViewModel.fromAvailabilityBreakModel) - .toList() ?? - [], - id: model?.id, - userId: model?.userId, + breaks: breaks, + ids: models.map((e) => e.availabilityModel.id!).toList(), + userId: userId, startTime: startTime, endTime: endTime, + conflictingPauses: conflictingPauses, + conflictingTime: conflictingTime, + selectedRange: range, ); } - /// + /// The templates are selected for the availability range + /// There can be multiple templates used in a selected range but only one + /// template can be applied at a time final List templates; - /// + /// Whether the selected availability range should be cleared final bool clearAvailability; - /// + /// Whether the initial selected range has different pauses. + /// If true an indication will be shown to the user that there are different + /// pauses and the pause section will be empty. If the user then selects a new + /// pause all the availabilities will be updated with the new pause. + final bool conflictingPauses; + + /// Whether the initial selected range has different times + /// If true an indication will be shown to the user that there are different + /// times and the time section will be empty. If the user then selects a new + /// time all the availabilities will be updated with the new time. + final bool conflictingTime; + + /// Whether a new template has been selected for the availability + /// If true a template will be applied, if false the availability will be + /// changed but the template will not be applied again + final bool templateSelected; + + /// The selected range for the availabilities + /// This is used for applying templates + final DateTimeRange selectedRange; + + /// The start time in the time selection while editing availabilities final TimeOfDay? startTime; - /// + /// The end time in the time selection while editing availabilities final TimeOfDay? endTime; - /// - final String? id; + /// The ids of the availabilities + final List? ids; - /// + /// The user id for which the availabilities are managed final String? userId; - /// + /// The configured breaks while editing availabilities final List breaks; /// Whether the availability is valid @@ -66,12 +119,63 @@ class AvailabilityViewModel { bool get canSave => clearAvailability || (startTime != null && endTime != null); + /// + AvailabilityViewModel applyTemplate(AvailabilityTemplateModel template) { + TimeOfDay? startTime; + TimeOfDay? endTime; + var conflictingPauses = true; + var conflictingTime = true; + var breaks = []; + var appliedAvailabilities = + template.apply(selectedRange.start, selectedRange.end); + + if (_availabilityTimesAreEqual(appliedAvailabilities)) { + conflictingTime = false; + startTime = TimeOfDay.fromDateTime(appliedAvailabilities.first.startDate); + endTime = TimeOfDay.fromDateTime(appliedAvailabilities.first.endDate); + } + if (_availabilityBreaksAreEqual(appliedAvailabilities)) { + conflictingPauses = false; + breaks = appliedAvailabilities.first.breaks + .map(BreakViewModel.fromAvailabilityBreakModel) + .toList(); + } + + return copyWith( + templates: [template], + breaks: breaks, + conflictingPauses: conflictingPauses, + conflictingTime: conflictingTime, + startTime: startTime, + endTime: endTime, + templateSelected: true, + ); + } + + /// + AvailabilityViewModel removeTemplates() => copyWith( + templates: [], + templateSelected: false, + ); + /// create a AvailabilityModel from the current AvailabilityViewModel AvailabilityModel toModel() { - var startDate = DateTime.now(); - var endDate = DateTime.now(); + var startDate = DateTime( + selectedRange.start.year, + selectedRange.start.month, + selectedRange.start.day, + startTime!.hour, + startTime!.minute, + ); + var endDate = DateTime( + selectedRange.start.year, + selectedRange.start.month, + selectedRange.start.day, + endTime!.hour, + endTime!.minute, + ); return AvailabilityModel( - id: id, + id: ids?.firstOrNull, userId: userId!, startDate: startDate, endDate: endDate, @@ -85,18 +189,51 @@ class AvailabilityViewModel { List? templates, TimeOfDay? startTime, TimeOfDay? endTime, - String? id, + List? ids, String? userId, List? breaks, bool? clearAvailability, + bool? conflictingPauses, + bool? conflictingTime, + bool? templateSelected, + DateTimeRange? selectedRange, }) => AvailabilityViewModel( templates: templates ?? this.templates, startTime: startTime ?? this.startTime, endTime: endTime ?? this.endTime, - id: id ?? this.id, + ids: ids ?? this.ids, userId: userId ?? this.userId, breaks: breaks ?? this.breaks, clearAvailability: clearAvailability ?? this.clearAvailability, + conflictingPauses: conflictingPauses ?? this.conflictingPauses, + conflictingTime: conflictingTime ?? this.conflictingTime, + templateSelected: templateSelected ?? this.templateSelected, + selectedRange: selectedRange ?? this.selectedRange, ); } + +/// Checks if the availability times are equal +bool _availabilityTimesAreEqual(List availabilityModels) { + var firstModel = availabilityModels.firstOrNull; + if (firstModel == null) { + return false; + } + var startDate = firstModel.startDate; + var endDate = firstModel.endDate; + return availabilityModels.every( + (model) => + isAtSameTime(startDate, model.startDate) && + isAtSameTime(endDate, model.endDate), + ); +} + +/// Checks if the availability breaks are equal +bool _availabilityBreaksAreEqual(List availabilityModels) { + var firstModel = availabilityModels.firstOrNull; + if (firstModel == null) { + return false; + } + var breaks = firstModel.breaks; + return availabilityModels.every((model) => model.breaksEqual(breaks)); +} diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart index 0d24166..d49c396 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar.dart @@ -177,7 +177,7 @@ List _mapAvailabilitiesToCalendarDays( availability.template!, ); return CalendarDay( - date: availability.availabilityModel.startDate, + date: DateUtils.dateOnly(availability.availabilityModel.startDate), color: availability.template != null ? Color(availability.template!.color) : null, diff --git a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart index 25d7a9e..59d410c 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/calendar_grid.dart @@ -250,8 +250,16 @@ List _generateCalendarDays( }) { for (var i = 0; i < count; i++) { var day = isNextMonth - ? DateTime(startDay.year, startDay.month, startDay.day + i + 1) - : DateTime(startDay.year, startDay.month, startDay.day - count + i); + ? DateTime( + startDay.year, + startDay.month, + startDay.day + i + 1, + ) + : DateTime( + startDay.year, + startDay.month, + startDay.day - count + i, + ); var isSelected = selectedRange != null && !day.isBefore(selectedRange.start) && !day.isAfter(selectedRange.end); @@ -275,7 +283,7 @@ List _generateCalendarDays( // Add days of the current month for (var i = 1; i <= daysInMonth; i++) { - var day = DateTime(month.year, month.month, i); + var day = DateTime(month.year, month.month, i, 0, 0); var specialDay = days.firstWhere( (d) => d.date.day == i && diff --git a/packages/flutter_availability_data_interface/lib/src/models/availability.dart b/packages/flutter_availability_data_interface/lib/src/models/availability.dart index 2899409..83e2dd7 100644 --- a/packages/flutter_availability_data_interface/lib/src/models/availability.dart +++ b/packages/flutter_availability_data_interface/lib/src/models/availability.dart @@ -1,4 +1,6 @@ // ignore_for_file: Generated using data class generator +import "package:flutter_availability_data_interface/src/utils.dart"; + /// A model defining the data structure for an availability class AvailabilityModel { /// Creates a new availability @@ -56,24 +58,29 @@ class AvailabilityModel { /// returns true if the date of the availability overlaps with the given range /// This disregards the time of the date bool isInRange(DateTime start, DateTime end) { - var startDate = DateTime(start.year, start.month, start.day); - var endDate = DateTime(end.year, end.month, end.day); - var availabilityStartDate = DateTime( - this.startDate.year, - this.startDate.month, - this.startDate.day, - ); - var availabilityEndDate = DateTime( - this.endDate.year, - this.endDate.month, - this.endDate.day, - ); + var startDate = start.date; + var endDate = end.date; + var availabilityStartDate = this.startDate.date; + var availabilityEndDate = this.endDate.date; return (startDate.isBefore(availabilityEndDate) || startDate.isAtSameMomentAs(availabilityEndDate)) && (endDate.isAfter(availabilityStartDate) || endDate.isAtSameMomentAs(availabilityStartDate)); } + + /// Compares this AvailabilityModel breaks to another AvailabilityModel breaks + bool breaksEqual(List otherBreaks) { + if (breaks.length != otherBreaks.length) { + return false; + } + for (var i = 0; i < breaks.length; i++) { + if (!breaks[i].equals(otherBreaks[i])) { + return false; + } + } + return true; + } } /// A model defining the structure of a break within an [AvailabilityModel] @@ -160,4 +167,14 @@ class AvailabilityBreakModel { "endTime": endTime.millisecondsSinceEpoch, "duration": submittedDuration?.inMinutes, }; + + /// Compares this AvailabilityBreakModel to another AvailabilityBreakModel + /// This only compares the start time, end time and submitted duration, + /// it ignores the date of the DateTime objects + bool equals(AvailabilityBreakModel other) => + startTime.hour == other.startTime.hour && + startTime.minute == other.startTime.minute && + endTime.hour == other.endTime.hour && + endTime.minute == other.endTime.minute && + submittedDuration == other.submittedDuration; } diff --git a/packages/flutter_availability_data_interface/lib/src/utils.dart b/packages/flutter_availability_data_interface/lib/src/utils.dart new file mode 100644 index 0000000..48f08fe --- /dev/null +++ b/packages/flutter_availability_data_interface/lib/src/utils.dart @@ -0,0 +1,9 @@ +/// Utility datetime functions for the availability data interface +extension DateUtils on DateTime { + /// Gets the date without the time + DateTime get date => DateTime(year, month, day); + + /// Returns true if the time of the date matches the time of the other date + bool timeMatches(DateTime other) => + other.hour == hour && other.minute == minute; +}