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.
This commit is contained in:
Freek van de Ven 2024-07-22 10:58:11 +02:00 committed by FlutterJoey
parent e1dd2a3520
commit 53fb9a2e2c
6 changed files with 219 additions and 47 deletions

View file

@ -49,7 +49,10 @@ class AvailabilitiesModificationScreen extends StatefulWidget {
class _AvailabilitiesModificationScreenState
extends State<AvailabilitiesModificationScreen> {
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();
});
}

View file

@ -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<AvailabilityWithTemplate> 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 = <BreakViewModel>[];
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<AvailabilityTemplateModel> 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<String>? ids;
///
/// The user id for which the availabilities are managed
final String? userId;
///
/// The configured breaks while editing availabilities
final List<BreakViewModel> 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 = <BreakViewModel>[];
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<AvailabilityTemplateModel>? templates,
TimeOfDay? startTime,
TimeOfDay? endTime,
String? id,
List<String>? ids,
String? userId,
List<BreakViewModel>? 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<AvailabilityModel> 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<AvailabilityModel> availabilityModels) {
var firstModel = availabilityModels.firstOrNull;
if (firstModel == null) {
return false;
}
var breaks = firstModel.breaks;
return availabilityModels.every((model) => model.breaksEqual(breaks));
}

View file

@ -177,7 +177,7 @@ List<CalendarDay> _mapAvailabilitiesToCalendarDays(
availability.template!,
);
return CalendarDay(
date: availability.availabilityModel.startDate,
date: DateUtils.dateOnly(availability.availabilityModel.startDate),
color: availability.template != null
? Color(availability.template!.color)
: null,

View file

@ -250,8 +250,16 @@ List<CalendarDay> _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<CalendarDay> _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 &&

View file

@ -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<AvailabilityBreakModel> 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;
}

View file

@ -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;
}