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.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;

View file

@ -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<BreakViewModel> breaks;
final void Function(List<BreakViewModel> 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,
),

View file

@ -21,7 +21,8 @@ class AvailabilityViewModel {
this.conflictingPauses = false,
this.conflictingTime = false,
this.templateSelected = false,
});
List<AvailabilityWithTemplate> 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<AvailabilityWithTemplate> _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,
);
}

View file

@ -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)),
],
);
}
}

View file

@ -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<DateTime> _getDatesBetween(DateTime startDate, DateTime endDate) {