From 39e086a3f1384f4bc2ca94dd7fc32d38f3a685c2 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Fri, 26 Jul 2024 10:11:47 +0200 Subject: [PATCH] feat: add marking for template deviation in the availability modification screen --- .../src/config/availability_translations.dart | 22 ++++++ .../ui/screens/availability_modification.dart | 32 +++------ .../view_models/availability_view_model.dart | 38 ++++++++++- .../widgets/availabillity_time_selection.dart | 68 ++++++++++++++----- .../lib/src/models/templates.dart | 41 +++++++++++ 5 files changed, 160 insertions(+), 41 deletions(-) diff --git a/packages/flutter_availability/lib/src/config/availability_translations.dart b/packages/flutter_availability/lib/src/config/availability_translations.dart index 6b64007..6814975 100644 --- a/packages/flutter_availability/lib/src/config/availability_translations.dart +++ b/packages/flutter_availability/lib/src/config/availability_translations.dart @@ -29,6 +29,9 @@ class AvailabilityTranslations { required this.availabilityUsedTemplates, required this.availabilityTimeTitle, required this.availabilitiesTimeTitle, + required this.availabilityTemplateDeviationExplanation, + required this.availabilitiesTemplateDeviationExplanation, + required this.availabilitiesConflictingTimeExplanation, required this.availabilityDialogConfirmTitle, required this.availabilityDialogConfirmDescription, required this.templateScreenTitle, @@ -95,6 +98,13 @@ class AvailabilityTranslations { this.availabilityUsedTemplates = "Used templates", this.availabilityTimeTitle = "Start and end time workday", this.availabilitiesTimeTitle = "Start and end time workdays", + this.availabilityTemplateDeviationExplanation = + "The start and end time are deviating from the template for this day", + this.availabilitiesTemplateDeviationExplanation = + "The start and end time are deviating from the template for these days", + this.availabilitiesConflictingTimeExplanation = + "There are conflicting times when applying this template " + "for this period", this.availabilityDialogConfirmTitle = "Are you sure you want to save the changes?", this.availabilityDialogConfirmDescription = @@ -204,6 +214,18 @@ class AvailabilityTranslations { /// The title on the time selection section for adding multiple availabilities final String availabilitiesTimeTitle; + /// The explainer text when the availability deviates from the used template + /// on the availability modification screen + final String availabilityTemplateDeviationExplanation; + + /// The explainer text when one of the availabilities deviates from the used + /// template on the availability modification screen + final String availabilitiesTemplateDeviationExplanation; + + /// The explainer text when the availabilities have conflicting times when + /// applying a template and the start and end time have not been filled in + final String availabilitiesConflictingTimeExplanation; + /// The title on the dialog for confirming the availability update final String availabilityDialogConfirmTitle; 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 9af134c..4b824b4 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart @@ -183,17 +183,13 @@ class _AvailabilitiesModificationScreenState } return _AvailabilitiesModificationScreenLayout( - dateRange: widget.dateRange, - clearAvailability: _availabilityViewModel.clearAvailability, + viewModel: _availabilityViewModel, onClearSection: onClearSection, selectedTemplates: _availabilityViewModel.templates, onTemplateSelected: onTemplateSelected, onTemplatesRemoved: onTemplatesRemoved, - startTime: _availabilityViewModel.startTime, - endTime: _availabilityViewModel.endTime, onStartChanged: onStartChanged, onEndChanged: onEndChanged, - breaks: _availabilityViewModel.breaks, onBreaksChanged: onBreaksChanged, sidePadding: spacing.sidePadding, bottomButtonPadding: spacing.bottomButtonPadding, @@ -205,17 +201,13 @@ class _AvailabilitiesModificationScreenState class _AvailabilitiesModificationScreenLayout extends HookWidget { const _AvailabilitiesModificationScreenLayout({ - required this.dateRange, - required this.clearAvailability, + required this.viewModel, required this.onClearSection, required this.selectedTemplates, required this.onTemplateSelected, required this.onTemplatesRemoved, - required this.startTime, - required this.endTime, required this.onStartChanged, required this.onEndChanged, - required this.breaks, required this.onBreaksChanged, required this.sidePadding, required this.bottomButtonPadding, @@ -223,8 +215,7 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget { required this.onExit, }); - final DateTimeRange dateRange; - final bool clearAvailability; + final AvailabilityViewModel viewModel; // ignore: avoid_positional_boolean_parameters final void Function(bool isChecked) onClearSection; @@ -232,12 +223,9 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget { final void Function() onTemplateSelected; final void Function() onTemplatesRemoved; - final TimeOfDay? startTime; - final TimeOfDay? endTime; final void Function(TimeOfDay start) onStartChanged; final void Function(TimeOfDay start) onEndChanged; - final List breaks; final void Function(List breaks) onBreaksChanged; final double sidePadding; @@ -258,11 +246,11 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget { BasePage( body: [ AvailabilityClearSection( - range: dateRange, - clearAvailable: clearAvailability, + range: viewModel.selectedRange, + clearAvailable: viewModel.clearAvailability, onChanged: onClearSection, ), - if (!clearAvailability) ...[ + if (!viewModel.clearAvailability) ...[ const SizedBox(height: 24), AvailabilityTemplateSelection( selectedTemplates: selectedTemplates, @@ -271,16 +259,14 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget { ), const SizedBox(height: 24), AvailabilityTimeSelection( - dateRange: dateRange, - startTime: startTime, - endTime: endTime, - key: ValueKey([startTime, endTime]), + viewModel: viewModel, + key: ValueKey(viewModel), onStartChanged: onStartChanged, onEndChanged: onEndChanged, ), const SizedBox(height: 24), PauseSelection( - breaks: breaks, + breaks: viewModel.breaks, editingTemplate: false, onBreaksChanged: onBreaksChanged, ), 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 f9b2394..eb4c0d6 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 @@ -21,7 +21,8 @@ class AvailabilityViewModel { this.conflictingPauses = false, this.conflictingTime = false, this.templateSelected = false, - }); + List initialModels = const [], + }) : _initialModels = initialModels; /// This constructor creates a [AvailabilityViewModel] from a list of /// [AvailabilityWithTemplate] models @@ -59,6 +60,7 @@ class AvailabilityViewModel { } return AvailabilityViewModel( + initialModels: models, templates: models.getUniqueTemplates(), breaks: breaks, ids: models.map((e) => e.availabilityModel.id!).toList(), @@ -71,6 +73,10 @@ class AvailabilityViewModel { ); } + /// The initial models that were selected and can be checked for deviations + /// from the templates + final List _initialModels; + /// 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 @@ -124,6 +130,17 @@ class AvailabilityViewModel { templateSelected || (startTime != null && endTime != null); + /// Whether a template deviation should be shown to the user + bool get isDeviatingFromTemplate => + startTime != null && + endTime != null && + templates.isNotEmpty && + _isAnyTemplateWithDifferentTimeFromAvailability(); + + /// Checks whether any availability has a different time from their template + bool _isAnyTemplateWithDifferentTimeFromAvailability() => + _initialModels.any(_availabilityTemplateDeviatesFromTime); + /// AvailabilityViewModel applyTemplate(AvailabilityTemplateModel template) { TimeOfDay? startTime; @@ -209,6 +226,24 @@ class AvailabilityViewModel { ); } + /// Checks the current selected start and end time against the templates for + /// the initial models to see if the time deviates from the template + bool _availabilityTemplateDeviatesFromTime(AvailabilityWithTemplate model) { + var template = model.template; + var availability = model.availabilityModel; + if (template == null) { + return false; + } + var startDate = DateTime(0, 0, 0, startTime!.hour, startTime!.minute); + var endDate = DateTime(0, 0, 0, endTime!.hour, endTime!.minute); + + return template.availabilityDeviatesFromTemplate( + availability, + startDate, + endDate, + ); + } + /// Copies the current properties into a new instance /// of [AvailabilityViewModel], AvailabilityViewModel copyWith({ @@ -236,6 +271,7 @@ class AvailabilityViewModel { conflictingTime: conflictingTime ?? this.conflictingTime, templateSelected: templateSelected ?? this.templateSelected, selectedRange: selectedRange ?? this.selectedRange, + initialModels: _initialModels, ); } diff --git a/packages/flutter_availability/lib/src/ui/widgets/availabillity_time_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/availabillity_time_selection.dart index 1932408..851bac1 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/availabillity_time_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/availabillity_time_selection.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/view_models/availability_view_model.dart"; import "package:flutter_availability/src/ui/widgets/generic_time_selection.dart"; import "package:flutter_availability/src/util/scope.dart"; @@ -6,19 +7,14 @@ import "package:flutter_availability/src/util/scope.dart"; class AvailabilityTimeSelection extends StatelessWidget { /// const AvailabilityTimeSelection({ - required this.startTime, - required this.endTime, + required this.viewModel, required this.onStartChanged, required this.onEndChanged, - required this.dateRange, super.key, }); /// - final TimeOfDay? startTime; - - /// - final TimeOfDay? endTime; + final AvailabilityViewModel viewModel; /// final void Function(TimeOfDay) onStartChanged; @@ -26,27 +22,65 @@ class AvailabilityTimeSelection extends StatelessWidget { /// final void Function(TimeOfDay) onEndChanged; - /// The date range for which the availabilities are being managed - final DateTimeRange dateRange; - @override Widget build(BuildContext context) { var availabilityScope = AvailabilityScope.of(context); var options = availabilityScope.options; var translations = options.translations; + var dateRange = viewModel.selectedRange; + var isSingleDay = dateRange.start.isAtSameMomentAs(dateRange.end); var titleText = isSingleDay ? translations.availabilityTimeTitle : translations.availabilitiesTimeTitle; - return TimeSelection( - title: titleText, - description: null, - startTime: startTime, - endTime: endTime, - onStartChanged: onStartChanged, - onEndChanged: onEndChanged, + String? explanationText; + if (viewModel.isDeviatingFromTemplate) { + explanationText = isSingleDay + ? translations.availabilityTemplateDeviationExplanation + : translations.availabilitiesTemplateDeviationExplanation; + } + + return Column( + children: [ + TimeSelection( + title: titleText, + description: null, + startTime: viewModel.startTime, + endTime: viewModel.endTime, + onStartChanged: onStartChanged, + onEndChanged: onEndChanged, + ), + if (explanationText != null) ...[ + const SizedBox(height: 8), + _AvailabilityExplanation( + explanation: explanationText, + ), + ], + ], + ); + } +} + +class _AvailabilityExplanation extends StatelessWidget { + const _AvailabilityExplanation({ + required this.explanation, + }); + + final String explanation; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var textTheme = theme.textTheme; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outline), + const SizedBox(width: 8), + Expanded(child: Text(explanation, style: textTheme.bodyMedium)), + ], ); } } diff --git a/packages/flutter_availability_data_interface/lib/src/models/templates.dart b/packages/flutter_availability_data_interface/lib/src/models/templates.dart index d8e0662..67dc5e6 100644 --- a/packages/flutter_availability_data_interface/lib/src/models/templates.dart +++ b/packages/flutter_availability_data_interface/lib/src/models/templates.dart @@ -1,5 +1,6 @@ import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:flutter_availability_data_interface/src/models/availability.dart"; +import "package:flutter_availability_data_interface/src/utils.dart"; /// Exception thrown when the end is before the start class TemplateEndBeforeStartException implements Exception {} @@ -119,6 +120,15 @@ class AvailabilityTemplateModel { void validate() { templateData.validate(); } + + /// check if an availability's day corresponds to the template with the given + /// [availability] and [start] and [end] dates + bool availabilityDeviatesFromTemplate( + AvailabilityModel availability, + DateTime start, + DateTime end, + ) => + templateData.availabilityDeviates(availability, start, end); } /// Used as the key for defining week-based templates @@ -186,6 +196,14 @@ abstract interface class TemplateData { /// Verify the validity of the data in this template void validate(); + + /// Check if an availability's day corresponds to the template with the given + /// [availability] and [start] and [end] dates + bool availabilityDeviates( + AvailabilityModel availability, + DateTime start, + DateTime end, + ); } /// A week based template data structure @@ -287,6 +305,21 @@ class WeekTemplateData implements TemplateData { dayData.value.validate(); } } + + @override + bool availabilityDeviates( + AvailabilityModel availability, + DateTime start, + DateTime end, + ) { + var dayOfWeek = WeekDay.values[availability.startDate.weekday]; + var data = _data[dayOfWeek]; + if (data == null) { + return false; + } + // compare the start and end with the template + return !start.timeMatches(data.startTime) || !end.timeMatches(data.endTime); + } } /// A day based template data structure @@ -416,6 +449,14 @@ class DayTemplateData implements TemplateData { } } } + + @override + bool availabilityDeviates( + AvailabilityModel availability, + DateTime start, + DateTime end, + ) => + !start.timeMatches(startTime) || !end.timeMatches(endTime); } List _getDatesBetween(DateTime startDate, DateTime endDate) {