mirror of
https://github.com/Iconica-Development/flutter_availability.git
synced 2025-05-19 13:13:44 +02:00
feat: add marking for template deviation in the availability modification screen
This commit is contained in:
parent
5eda42c9dd
commit
eb704f44cd
5 changed files with 160 additions and 41 deletions
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
if (viewModel.isDeviatingFromTemplate) {
|
||||||
|
explanationText = isSingleDay
|
||||||
|
? translations.availabilityTemplateDeviationExplanation
|
||||||
|
: translations.availabilitiesTemplateDeviationExplanation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TimeSelection(
|
||||||
title: titleText,
|
title: titleText,
|
||||||
description: null,
|
description: null,
|
||||||
startTime: startTime,
|
startTime: viewModel.startTime,
|
||||||
endTime: endTime,
|
endTime: viewModel.endTime,
|
||||||
onStartChanged: onStartChanged,
|
onStartChanged: onStartChanged,
|
||||||
onEndChanged: onEndChanged,
|
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)),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue