feat: add marking for template deviation in the availability modification screen

This commit is contained in:
Freek van de Ven 2024-07-26 10:11:47 +02:00 committed by FlutterJoey
parent 5eda42c9dd
commit eb704f44cd
5 changed files with 160 additions and 41 deletions

View file

@ -29,6 +29,9 @@ class AvailabilityTranslations {
required this.availabilityUsedTemplates, required this.availabilityUsedTemplates,
required this.availabilityTimeTitle, required this.availabilityTimeTitle,
required this.availabilitiesTimeTitle, required this.availabilitiesTimeTitle,
required this.availabilityTemplateDeviationExplanation,
required this.availabilitiesTemplateDeviationExplanation,
required this.availabilitiesConflictingTimeExplanation,
required this.availabilityDialogConfirmTitle, required this.availabilityDialogConfirmTitle,
required this.availabilityDialogConfirmDescription, required this.availabilityDialogConfirmDescription,
required this.templateScreenTitle, required this.templateScreenTitle,
@ -95,6 +98,13 @@ class AvailabilityTranslations {
this.availabilityUsedTemplates = "Used templates", this.availabilityUsedTemplates = "Used templates",
this.availabilityTimeTitle = "Start and end time workday", this.availabilityTimeTitle = "Start and end time workday",
this.availabilitiesTimeTitle = "Start and end time workdays", 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 = this.availabilityDialogConfirmTitle =
"Are you sure you want to save the changes?", "Are you sure you want to save the changes?",
this.availabilityDialogConfirmDescription = this.availabilityDialogConfirmDescription =
@ -204,6 +214,18 @@ class AvailabilityTranslations {
/// The title on the time selection section for adding multiple availabilities /// The title on the time selection section for adding multiple availabilities
final String availabilitiesTimeTitle; 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 /// The title on the dialog for confirming the availability update
final String availabilityDialogConfirmTitle; final String availabilityDialogConfirmTitle;

View file

@ -183,17 +183,13 @@ class _AvailabilitiesModificationScreenState
} }
return _AvailabilitiesModificationScreenLayout( return _AvailabilitiesModificationScreenLayout(
dateRange: widget.dateRange, viewModel: _availabilityViewModel,
clearAvailability: _availabilityViewModel.clearAvailability,
onClearSection: onClearSection, onClearSection: onClearSection,
selectedTemplates: _availabilityViewModel.templates, selectedTemplates: _availabilityViewModel.templates,
onTemplateSelected: onTemplateSelected, onTemplateSelected: onTemplateSelected,
onTemplatesRemoved: onTemplatesRemoved, onTemplatesRemoved: onTemplatesRemoved,
startTime: _availabilityViewModel.startTime,
endTime: _availabilityViewModel.endTime,
onStartChanged: onStartChanged, onStartChanged: onStartChanged,
onEndChanged: onEndChanged, onEndChanged: onEndChanged,
breaks: _availabilityViewModel.breaks,
onBreaksChanged: onBreaksChanged, onBreaksChanged: onBreaksChanged,
sidePadding: spacing.sidePadding, sidePadding: spacing.sidePadding,
bottomButtonPadding: spacing.bottomButtonPadding, bottomButtonPadding: spacing.bottomButtonPadding,
@ -205,17 +201,13 @@ class _AvailabilitiesModificationScreenState
class _AvailabilitiesModificationScreenLayout extends HookWidget { class _AvailabilitiesModificationScreenLayout extends HookWidget {
const _AvailabilitiesModificationScreenLayout({ const _AvailabilitiesModificationScreenLayout({
required this.dateRange, required this.viewModel,
required this.clearAvailability,
required this.onClearSection, required this.onClearSection,
required this.selectedTemplates, required this.selectedTemplates,
required this.onTemplateSelected, required this.onTemplateSelected,
required this.onTemplatesRemoved, required this.onTemplatesRemoved,
required this.startTime,
required this.endTime,
required this.onStartChanged, required this.onStartChanged,
required this.onEndChanged, required this.onEndChanged,
required this.breaks,
required this.onBreaksChanged, required this.onBreaksChanged,
required this.sidePadding, required this.sidePadding,
required this.bottomButtonPadding, required this.bottomButtonPadding,
@ -223,8 +215,7 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget {
required this.onExit, required this.onExit,
}); });
final DateTimeRange dateRange; final AvailabilityViewModel viewModel;
final bool clearAvailability;
// ignore: avoid_positional_boolean_parameters // ignore: avoid_positional_boolean_parameters
final void Function(bool isChecked) onClearSection; final void Function(bool isChecked) onClearSection;
@ -232,12 +223,9 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget {
final void Function() onTemplateSelected; final void Function() onTemplateSelected;
final void Function() onTemplatesRemoved; final void Function() onTemplatesRemoved;
final TimeOfDay? startTime;
final TimeOfDay? endTime;
final void Function(TimeOfDay start) onStartChanged; final void Function(TimeOfDay start) onStartChanged;
final void Function(TimeOfDay start) onEndChanged; final void Function(TimeOfDay start) onEndChanged;
final List<BreakViewModel> breaks;
final void Function(List<BreakViewModel> breaks) onBreaksChanged; final void Function(List<BreakViewModel> breaks) onBreaksChanged;
final double sidePadding; final double sidePadding;
@ -258,11 +246,11 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget {
BasePage( BasePage(
body: [ body: [
AvailabilityClearSection( AvailabilityClearSection(
range: dateRange, range: viewModel.selectedRange,
clearAvailable: clearAvailability, clearAvailable: viewModel.clearAvailability,
onChanged: onClearSection, onChanged: onClearSection,
), ),
if (!clearAvailability) ...[ if (!viewModel.clearAvailability) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
AvailabilityTemplateSelection( AvailabilityTemplateSelection(
selectedTemplates: selectedTemplates, selectedTemplates: selectedTemplates,
@ -271,16 +259,14 @@ class _AvailabilitiesModificationScreenLayout extends HookWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
AvailabilityTimeSelection( AvailabilityTimeSelection(
dateRange: dateRange, viewModel: viewModel,
startTime: startTime, key: ValueKey(viewModel),
endTime: endTime,
key: ValueKey([startTime, endTime]),
onStartChanged: onStartChanged, onStartChanged: onStartChanged,
onEndChanged: onEndChanged, onEndChanged: onEndChanged,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
PauseSelection( PauseSelection(
breaks: breaks, breaks: viewModel.breaks,
editingTemplate: false, editingTemplate: false,
onBreaksChanged: onBreaksChanged, onBreaksChanged: onBreaksChanged,
), ),

View file

@ -21,7 +21,8 @@ class AvailabilityViewModel {
this.conflictingPauses = false, this.conflictingPauses = false,
this.conflictingTime = false, this.conflictingTime = false,
this.templateSelected = false, this.templateSelected = false,
}); List<AvailabilityWithTemplate> initialModels = const [],
}) : _initialModels = initialModels;
/// This constructor creates a [AvailabilityViewModel] from a list of /// This constructor creates a [AvailabilityViewModel] from a list of
/// [AvailabilityWithTemplate] models /// [AvailabilityWithTemplate] models
@ -59,6 +60,7 @@ class AvailabilityViewModel {
} }
return AvailabilityViewModel( return AvailabilityViewModel(
initialModels: models,
templates: models.getUniqueTemplates(), templates: models.getUniqueTemplates(),
breaks: breaks, breaks: breaks,
ids: models.map((e) => e.availabilityModel.id!).toList(), 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<AvailabilityWithTemplate> _initialModels;
/// The templates are selected for the availability range /// The templates are selected for the availability range
/// There can be multiple templates used in a selected range but only one /// There can be multiple templates used in a selected range but only one
/// template can be applied at a time /// template can be applied at a time
@ -124,6 +130,17 @@ class AvailabilityViewModel {
templateSelected || templateSelected ||
(startTime != null && endTime != null); (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) { AvailabilityViewModel applyTemplate(AvailabilityTemplateModel template) {
TimeOfDay? startTime; 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 /// Copies the current properties into a new instance
/// of [AvailabilityViewModel], /// of [AvailabilityViewModel],
AvailabilityViewModel copyWith({ AvailabilityViewModel copyWith({
@ -236,6 +271,7 @@ class AvailabilityViewModel {
conflictingTime: conflictingTime ?? this.conflictingTime, conflictingTime: conflictingTime ?? this.conflictingTime,
templateSelected: templateSelected ?? this.templateSelected, templateSelected: templateSelected ?? this.templateSelected,
selectedRange: selectedRange ?? this.selectedRange, selectedRange: selectedRange ?? this.selectedRange,
initialModels: _initialModels,
); );
} }

View file

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; 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/ui/widgets/generic_time_selection.dart";
import "package:flutter_availability/src/util/scope.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 { class AvailabilityTimeSelection extends StatelessWidget {
/// ///
const AvailabilityTimeSelection({ const AvailabilityTimeSelection({
required this.startTime, required this.viewModel,
required this.endTime,
required this.onStartChanged, required this.onStartChanged,
required this.onEndChanged, required this.onEndChanged,
required this.dateRange,
super.key, super.key,
}); });
/// ///
final TimeOfDay? startTime; final AvailabilityViewModel viewModel;
///
final TimeOfDay? endTime;
/// ///
final void Function(TimeOfDay) onStartChanged; final void Function(TimeOfDay) onStartChanged;
@ -26,27 +22,65 @@ class AvailabilityTimeSelection extends StatelessWidget {
/// ///
final void Function(TimeOfDay) onEndChanged; final void Function(TimeOfDay) onEndChanged;
/// The date range for which the availabilities are being managed
final DateTimeRange dateRange;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var availabilityScope = AvailabilityScope.of(context); var availabilityScope = AvailabilityScope.of(context);
var options = availabilityScope.options; var options = availabilityScope.options;
var translations = options.translations; var translations = options.translations;
var dateRange = viewModel.selectedRange;
var isSingleDay = dateRange.start.isAtSameMomentAs(dateRange.end); var isSingleDay = dateRange.start.isAtSameMomentAs(dateRange.end);
var titleText = isSingleDay var titleText = isSingleDay
? translations.availabilityTimeTitle ? translations.availabilityTimeTitle
: translations.availabilitiesTimeTitle; : translations.availabilitiesTimeTitle;
return TimeSelection( String? explanationText;
title: titleText, if (viewModel.isDeviatingFromTemplate) {
description: null, explanationText = isSingleDay
startTime: startTime, ? translations.availabilityTemplateDeviationExplanation
endTime: endTime, : translations.availabilitiesTemplateDeviationExplanation;
onStartChanged: onStartChanged, }
onEndChanged: onEndChanged,
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)),
],
); );
} }
} }

View file

@ -1,5 +1,6 @@
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; 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/models/availability.dart";
import "package:flutter_availability_data_interface/src/utils.dart";
/// Exception thrown when the end is before the start /// Exception thrown when the end is before the start
class TemplateEndBeforeStartException implements Exception {} class TemplateEndBeforeStartException implements Exception {}
@ -119,6 +120,15 @@ class AvailabilityTemplateModel {
void validate() { void validate() {
templateData.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 /// 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 /// Verify the validity of the data in this template
void validate(); 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 /// A week based template data structure
@ -287,6 +305,21 @@ class WeekTemplateData implements TemplateData {
dayData.value.validate(); 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 /// 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<DateTime> _getDatesBetween(DateTime startDate, DateTime endDate) { List<DateTime> _getDatesBetween(DateTime startDate, DateTime endDate) {