diff --git a/packages/flutter_availability/lib/src/config/availability_options.dart b/packages/flutter_availability/lib/src/config/availability_options.dart index 6e4d995..d70d2a3 100644 --- a/packages/flutter_availability/lib/src/config/availability_options.dart +++ b/packages/flutter_availability/lib/src/config/availability_options.dart @@ -161,7 +161,7 @@ class AvailabilityColors { /// If not provided the text color will be the theme's text color final Color? textDarkColor; - /// The color of the background in the template week overview that creates a + /// The color of the background in the template week overview that creates a /// layered effect by interchanging a color and a transparent color /// If not provided the color will be the theme's [ColorScheme.surface] final Color? templateWeekOverviewBackgroundColor; 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 87b02e9..787614f 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart @@ -1,5 +1,6 @@ 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/ui/widgets/availability_clear.dart"; import "package:flutter_availability/src/ui/widgets/availability_template_selection.dart"; import "package:flutter_availability/src/ui/widgets/availabillity_time_selection.dart"; @@ -172,12 +173,16 @@ class _AvailabilitiesModificationScreenState ); var pauseSelection = PauseSelection( - breaks: _availability.breaks, + breaks: _availability.breaks + .map(BreakViewModel.fromAvailabilityBreakModel) + .toList(), editingTemplate: false, // TODO(Joey): Extract these onBreaksChanged: (breaks) { setState(() { - _availability = _availability.copyWith(breaks: breaks); + _availability = _availability.copyWith( + breaks: breaks.map((b) => b.toBreak()).toList(), + ); }); }, ); diff --git a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart index 4573c9d..ac4bf62 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_availability/src/ui/models/view_template_daydata.dart"; +import "package:flutter_availability/src/ui/view_models/day_template_view_model.dart"; import "package:flutter_availability/src/ui/widgets/color_selection.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; @@ -28,25 +28,16 @@ class DayTemplateModificationScreen extends StatefulWidget { class _DayTemplateModificationScreenState extends State { - late int? _selectedColor; - late AvailabilityTemplateModel _template; + late DayTemplateViewModel _viewModel; @override void initState() { super.initState(); - _selectedColor = widget.template?.color; - _template = widget.template ?? - AvailabilityTemplateModel( - userId: "1", - name: "", - color: 0, - templateType: AvailabilityTemplateType.day, - templateData: DayTemplateData( - startTime: DateTime.now(), - endTime: DateTime.now(), - breaks: [], - ), - ); + if (widget.template != null) { + _viewModel = DayTemplateViewModel.fromTemplate(widget.template!); + } else { + _viewModel = const DayTemplateViewModel(); + } } @override @@ -59,20 +50,21 @@ class _DayTemplateModificationScreenState var spacing = options.spacing; Future onDeletePressed() async { - await service.deleteTemplate(_template); + await service.deleteTemplate(widget.template!); widget.onExit(); } Future onSavePressed() async { + var template = _viewModel.toTemplate(); if (widget.template == null) { - await service.createTemplate(_template); + await service.createTemplate(template); } else { - await service.updateTemplate(_template); + await service.updateTemplate(template); } widget.onExit(); } - var canSave = _template.name.isNotEmpty && _selectedColor != null; + var canSave = _viewModel.canSave; var saveButton = options.primaryButtonBuilder( context, @@ -94,34 +86,29 @@ class _DayTemplateModificationScreenState ); var templateTitleSection = TemplateNameInput( - initialValue: _template.name, + initialValue: _viewModel.name, onNameChanged: (name) { setState(() { - _template = _template.copyWith(name: name); + _viewModel = _viewModel.copyWith(name: name); }); }, ); var colorSection = TemplateColorSelection( - selectedColor: _selectedColor, + selectedColor: _viewModel.color, // TODO(Joey): Extract this onColorSelected: (color) { setState(() { - _selectedColor = color; - _template = _template.copyWith(color: color); + _viewModel = _viewModel.copyWith(color: color); }); }, ); var availabilitySection = TemplateTimeAndBreakSection( - dayData: ViewDayTemplateData.fromDayTemplateData( - _template.templateData as DayTemplateData, - ), + dayData: _viewModel.data, onDayDataChanged: (data) { setState(() { - _template = _template.copyWith( - templateData: data.toDayTemplateData(), - ); + _viewModel = _viewModel.copyWith(data: data); }); }, ); diff --git a/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart b/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart index 1e3583d..6eae0e0 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_week_modification.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_availability/src/ui/models/view_template_daydata.dart"; +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; +import "package:flutter_availability/src/ui/view_models/week_template_view_models.dart"; import "package:flutter_availability/src/ui/widgets/color_selection.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; @@ -30,23 +31,16 @@ class WeekTemplateModificationScreen extends StatefulWidget { class _WeekTemplateModificationScreenState extends State { - late int? _selectedColor; - late AvailabilityTemplateModel _template; bool _editing = true; WeekDay _selectedDay = WeekDay.monday; + late WeekTemplateViewModel _viewModel; @override void initState() { super.initState(); - _selectedColor = widget.template?.color; - _template = widget.template ?? - AvailabilityTemplateModel( - userId: "1", - name: "", - color: 0, - templateType: AvailabilityTemplateType.week, - templateData: WeekTemplateData.forDays(), - ); + _viewModel = widget.template != null + ? WeekTemplateViewModel.fromTemplate(widget.template!) + : const WeekTemplateViewModel(); } @override @@ -59,11 +53,31 @@ class _WeekTemplateModificationScreenState var translations = options.translations; var spacing = options.spacing; - var weekTemplateDate = _template.templateData as WeekTemplateData; - var selectedDayData = weekTemplateDate.data[_selectedDay]; + var weekTemplateDate = _viewModel.data; + var selectedDayData = weekTemplateDate[_selectedDay]; + + void onDayDataChanged(DayTemplateDataViewModel data) { + setState(() { + // create a new copy of an unmodifiable map that can be modified + var updatedDays = + Map.from(weekTemplateDate); + if (data.isEmpty) { + updatedDays.remove(_selectedDay); + } else { + updatedDays[_selectedDay] = data; + } + _viewModel = _viewModel.copyWith(data: updatedDays); + }); + } + + void onDaySelected(int day) { + setState(() { + _selectedDay = WeekDay.values[day]; + }); + } Future onDeletePressed() async { - await service.deleteTemplate(_template); + await service.deleteTemplate(widget.template!); widget.onExit(); } @@ -84,15 +98,20 @@ class _WeekTemplateModificationScreenState } Future onSavePressed() async { + if (!_viewModel.isValid) { + // TODO(freek): show error message + return; + } + var template = _viewModel.toTemplate(); if (widget.template == null) { - await service.createTemplate(_template); + await service.createTemplate(template); } else { - await service.updateTemplate(_template); + await service.updateTemplate(template); } widget.onExit(); } - var canSave = _template.name.isNotEmpty && _selectedColor != null; + var canSave = _viewModel.canSave; var nextButton = options.primaryButtonBuilder( context, canSave ? onNextPressed : null, @@ -125,20 +144,19 @@ class _WeekTemplateModificationScreenState ); var templateTitleSection = TemplateNameInput( - initialValue: _template.name, + initialValue: _viewModel.name, onNameChanged: (name) { setState(() { - _template = _template.copyWith(name: name); + _viewModel = _viewModel.copyWith(name: name); }); }, ); var colorSection = TemplateColorSelection( - selectedColor: _selectedColor, + selectedColor: _viewModel.color, onColorSelected: (color) { setState(() { - _selectedColor = color; - _template = _template.copyWith(color: color); + _viewModel = _viewModel.copyWith(color: color); }); }, ); @@ -148,37 +166,13 @@ class _WeekTemplateModificationScreenState const SizedBox(height: 24), Padding( padding: EdgeInsets.only(left: spacing.sidePadding), - child: TemplateWeekDaySelection( - onDaySelected: (day) { - setState(() { - _selectedDay = WeekDay.values[day]; - }); - }, - ), + child: TemplateWeekDaySelection(onDaySelected: onDaySelected), ), const SizedBox(height: 24), _WeekTemplateSidePadding( child: TemplateTimeAndBreakSection( - dayData: selectedDayData != null - ? ViewDayTemplateData.fromDayTemplateData( - selectedDayData, - ) - : const ViewDayTemplateData(), - onDayDataChanged: (data) { - setState(() { - _template = _template.copyWith( - templateData: - // create a copy of the week template data - WeekTemplateData( - data: { - for (var entry in weekTemplateDate.data.entries) - entry.key: entry.value, - _selectedDay: data.toDayTemplateData(), - }, - ), - ); - }); - }, + dayData: selectedDayData ?? const DayTemplateDataViewModel(), + onDayDataChanged: onDayDataChanged, ), ), const SizedBox(height: 24), @@ -203,20 +197,20 @@ class _WeekTemplateModificationScreenState children: [ Container( decoration: BoxDecoration( - color: Color(_template.color), + color: Color(_viewModel.color ?? 0), borderRadius: BorderRadius.circular(5), ), width: 20, height: 20, ), const SizedBox(width: 12), - Text(_template.name, style: textTheme.bodyLarge), + Text(_viewModel.name ?? "", style: textTheme.bodyLarge), ], ), ), const SizedBox(height: 30), TemplateWeekOverview( - template: _template, + template: _viewModel, ), ], ), diff --git a/packages/flutter_availability/lib/src/ui/view_models/break_view_model.dart b/packages/flutter_availability/lib/src/ui/view_models/break_view_model.dart new file mode 100644 index 0000000..23c921a --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/view_models/break_view_model.dart @@ -0,0 +1,87 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; + +/// View model to represent the data during the modification of a break +class BreakViewModel { + /// Constructor + const BreakViewModel({ + this.startTime, + this.endTime, + this.duration, + }); + + /// Create a [BreakViewModel] from a [AvailabilityBreakModel] + factory BreakViewModel.fromAvailabilityBreakModel( + AvailabilityBreakModel data, + ) => + BreakViewModel( + startTime: TimeOfDay.fromDateTime(data.startTime), + endTime: TimeOfDay.fromDateTime(data.endTime), + duration: data.duration, + ); + + /// The start time for this break + final TimeOfDay? startTime; + + /// The end time for this break + final TimeOfDay? endTime; + + /// The full duration of the actual break. + /// + /// This is allowed to diverge from the difference between [startTime] and + /// [endTime] to indicate that the break is somewhere between [startTime] and + /// [endTime] + final Duration? duration; + + /// Returns true if the break is valid + /// The start is before the end and the duration is equal or lower than the + /// difference between the start and end + bool get isValid { + if (startTime == null || endTime == null) { + return false; + } + var startDateTime = DateTime(0, 1, 1, startTime!.hour, startTime!.minute); + var endDateTime = DateTime(0, 1, 1, endTime!.hour, endTime!.minute); + + if (startDateTime.isAfter(endDateTime)) { + return false; + } + if (duration == null) { + return true; + } + var actualDuration = endDateTime.difference(startDateTime); + return duration! <= actualDuration; + } + + /// Whether the save/next button should be enabled + bool get canSave => startTime != null && endTime != null; + + /// Convert to [AvailabilityBreakModel] for saving + AvailabilityBreakModel toBreak() => AvailabilityBreakModel( + startTime: DateTime(0, 0, 0, startTime!.hour, startTime!.minute), + endTime: DateTime(0, 0, 0, endTime!.hour, endTime!.minute), + duration: duration, + ); + + /// Create a copy with new values + BreakViewModel copyWith({ + TimeOfDay? startTime, + TimeOfDay? endTime, + Duration? duration, + }) => + BreakViewModel( + startTime: startTime ?? this.startTime, + endTime: endTime ?? this.endTime, + duration: duration ?? this.duration, + ); + + /// compareto method to order two breaks based on their start time + /// multiples hours are converted to minutes and added to the minutes + int compareTo(BreakViewModel other) { + var difference = startTime!.hour * 60 + + startTime!.minute - + other.startTime!.hour * 60 - + other.startTime!.minute; + return difference; + } +} diff --git a/packages/flutter_availability/lib/src/ui/view_models/day_template_view_model.dart b/packages/flutter_availability/lib/src/ui/view_models/day_template_view_model.dart new file mode 100644 index 0000000..f3592ba --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/view_models/day_template_view_model.dart @@ -0,0 +1,69 @@ +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; + +/// View model to represent the data during the modification of a day template +class DayTemplateViewModel { + /// Constructor + const DayTemplateViewModel({ + this.data = const DayTemplateDataViewModel(), + this.id, + this.name, + this.color, + }); + + /// Create a [WeekTemplateViewModel] from a [AvailabilityTemplateModel] + factory DayTemplateViewModel.fromTemplate( + AvailabilityTemplateModel template, + ) { + var data = template.templateData as DayTemplateData; + return DayTemplateViewModel( + id: template.id, + name: template.name, + color: template.color, + data: DayTemplateDataViewModel.fromDayTemplateData(data), + ); + } + + /// The identifier for this template + final String? id; + + /// The name by which the template can be visually identified + final String? name; + + /// The color by which the template can be visually identified + final int? color; + + /// The data for the template day + final DayTemplateDataViewModel data; + + /// Whether the data is valid for saving + bool get isValid => data.isValid; + + /// Whether the save/next button should be enabled + bool get canSave => + color != null && (name?.isNotEmpty ?? false) && data.canSave; + + /// Convert to [AvailabilityTemplateModel] for saving + AvailabilityTemplateModel toTemplate() => AvailabilityTemplateModel( + id: id, + userId: "", + name: name!, + color: color!, + templateType: AvailabilityTemplateType.day, + templateData: data.toDayTemplateData(), + ); + + /// Create a copy with new values + DayTemplateViewModel copyWith({ + String? id, + String? name, + int? color, + DayTemplateDataViewModel? data, + }) => + DayTemplateViewModel( + id: id ?? this.id, + name: name ?? this.name, + color: color ?? this.color, + data: data ?? this.data, + ); +} diff --git a/packages/flutter_availability/lib/src/ui/models/view_template_daydata.dart b/packages/flutter_availability/lib/src/ui/view_models/template_daydata_view_model.dart similarity index 52% rename from packages/flutter_availability/lib/src/ui/models/view_template_daydata.dart rename to packages/flutter_availability/lib/src/ui/view_models/template_daydata_view_model.dart index f59a142..495f539 100644 --- a/packages/flutter_availability/lib/src/ui/models/view_template_daydata.dart +++ b/packages/flutter_availability/lib/src/ui/view_models/template_daydata_view_model.dart @@ -1,21 +1,23 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; /// The data for creating or editing a day template -class ViewDayTemplateData { +class DayTemplateDataViewModel { /// Constructor - const ViewDayTemplateData({ + const DayTemplateDataViewModel({ this.startTime, this.endTime, this.breaks = const [], }); /// Create a new instance from a [DayTemplateData] - factory ViewDayTemplateData.fromDayTemplateData(DayTemplateData data) => - ViewDayTemplateData( + factory DayTemplateDataViewModel.fromDayTemplateData(DayTemplateData data) => + DayTemplateDataViewModel( startTime: TimeOfDay.fromDateTime(data.startTime), endTime: TimeOfDay.fromDateTime(data.endTime), - breaks: data.breaks, + breaks: + data.breaks.map(BreakViewModel.fromAvailabilityBreakModel).toList(), ); /// The start time to apply on a new availability @@ -25,18 +27,28 @@ class ViewDayTemplateData { final TimeOfDay? endTime; /// A list of breaks to apply to every new availability - final List breaks; + final List breaks; - /// Whether the data is valid for saving - bool get isValid => startTime != null && endTime != null; + /// Whether the data is valid + /// The start is before the end + /// There are no breaks that are invalid + /// The breaks are not outside the start and end time + /// The breaks are not overlapping + bool get isValid => canSave && breaks.every((b) => b.isValid); + + /// Whether the save/next button should be enabled + bool get canSave => startTime != null && endTime != null; + + /// Whether the day is empty and can be removed from the template when saving + bool get isEmpty => startTime == null && endTime == null && breaks.isEmpty; /// Create a copy with new values - ViewDayTemplateData copyWith({ + DayTemplateDataViewModel copyWith({ TimeOfDay? startTime, TimeOfDay? endTime, - List? breaks, + List? breaks, }) => - ViewDayTemplateData( + DayTemplateDataViewModel( startTime: startTime ?? this.startTime, endTime: endTime ?? this.endTime, breaks: breaks ?? this.breaks, @@ -47,6 +59,6 @@ class ViewDayTemplateData { startTime: DateTime(0, 0, 0, startTime?.hour ?? 0, startTime?.minute ?? 0), endTime: DateTime(0, 0, 0, endTime?.hour ?? 0, endTime?.minute ?? 0), - breaks: breaks, + breaks: breaks.map((b) => b.toBreak()).toList(), ); } diff --git a/packages/flutter_availability/lib/src/ui/view_models/week_template_view_models.dart b/packages/flutter_availability/lib/src/ui/view_models/week_template_view_models.dart new file mode 100644 index 0000000..a13e210 --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/view_models/week_template_view_models.dart @@ -0,0 +1,83 @@ +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; + +/// View model to represent the data during the modification of a week template +class WeekTemplateViewModel { + /// Constructor + const WeekTemplateViewModel({ + this.data = const {}, + this.id, + this.name, + this.color, + }); + + /// Create a [WeekTemplateViewModel] from a [AvailabilityTemplateModel] + factory WeekTemplateViewModel.fromTemplate( + AvailabilityTemplateModel template, + ) { + var data = template.templateData as WeekTemplateData; + return WeekTemplateViewModel( + id: template.id, + name: template.name, + color: template.color, + data: { + for (var day in WeekDay.values) + day: data.data.containsKey(day) + ? DayTemplateDataViewModel.fromDayTemplateData( + data.data[day]!, + ) + : const DayTemplateDataViewModel(), + }, + ); + } + + /// The identifier for this template + final String? id; + + /// The name by which the template can be visually identified + final String? name; + + /// The color by which the template can be visually identified + final int? color; + + /// The data for each day of the week + final Map data; + + /// Whether the data is valid for saving + /// All days must be valid and there must be at least one day with data + bool get isValid => + data.values.every((e) => e.isValid) && data.values.isNotEmpty; + + /// Whether the save/next button should be enabled + bool get canSave => + color != null && (name?.isNotEmpty ?? false) && data.values.isNotEmpty; + + /// Convert to [AvailabilityTemplateModel] for saving + AvailabilityTemplateModel toTemplate() => AvailabilityTemplateModel( + id: id, + userId: "", + name: name!, + color: color!, + templateType: AvailabilityTemplateType.week, + templateData: WeekTemplateData( + data: { + for (var entry in data.entries) + entry.key: entry.value.toDayTemplateData(), + }, + ), + ); + + /// Create a copy with new values + WeekTemplateViewModel copyWith({ + String? id, + String? name, + int? color, + Map? data, + }) => + WeekTemplateViewModel( + id: id ?? this.id, + name: name ?? this.name, + color: color ?? this.color, + data: data ?? this.data, + ); +} diff --git a/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart index 312449f..8e7d4fd 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart @@ -1,10 +1,10 @@ import "package:flutter/material.dart"; import "package:flutter_availability/flutter_availability.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/ui/widgets/generic_time_selection.dart"; import "package:flutter_availability/src/ui/widgets/input_fields.dart"; import "package:flutter_availability/src/util/scope.dart"; -import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; /// class PauseSelection extends StatelessWidget { @@ -17,10 +17,10 @@ class PauseSelection extends StatelessWidget { }); /// The breaks that are currently set - final List breaks; + final List breaks; /// Callback for when the breaks are changed - final void Function(List) onBreaksChanged; + final void Function(List) onBreaksChanged; /// Whether the pause selection is used for editing a template final bool editingTemplate; @@ -33,11 +33,12 @@ class PauseSelection extends StatelessWidget { var options = availabilityScope.options; var translations = options.translations; - Future openBreakDialog( - AvailabilityBreakModel? initialBreak, + Future openBreakDialog( + BreakViewModel? initialBreak, ) async => AvailabilityBreakSelectionDialog.show( context, + initialBreak: initialBreak, userId: availabilityScope.userId, options: options, service: availabilityScope.service, @@ -52,7 +53,7 @@ class PauseSelection extends StatelessWidget { onBreaksChanged(updatedBreaks); } - Future onEditBreak(AvailabilityBreakModel availabilityBreak) async { + Future onEditBreak(BreakViewModel availabilityBreak) async { var updatedBreak = await openBreakDialog(availabilityBreak); if (updatedBreak == null) return; @@ -60,13 +61,12 @@ class PauseSelection extends StatelessWidget { onBreaksChanged(updatedBreaks); } - void onDeleteBreak(AvailabilityBreakModel availabilityBreak) { + void onDeleteBreak(BreakViewModel availabilityBreak) { var updatedBreaks = breaks.where((b) => b != availabilityBreak).toList(); onBreaksChanged(updatedBreaks); } - var sortedBreaks = breaks.toList() - ..sort((a, b) => a.startTime.compareTo(b.startTime)); + var sortedBreaks = breaks.toList()..sort((a, b) => a.compareTo(b)); var addButton = options.bigTextButtonWrapperBuilder( context, @@ -120,7 +120,7 @@ class BreakDisplay extends StatelessWidget { }); /// The break to display - final AvailabilityBreakModel breakModel; + final BreakViewModel breakModel; /// Callback for when the minus button is clicked final VoidCallback onRemove; @@ -138,11 +138,11 @@ class BreakDisplay extends StatelessWidget { var starTime = translations.timeFormatter( context, - TimeOfDay.fromDateTime(breakModel.startTime), + breakModel.startTime!, ); var endTime = translations.timeFormatter( context, - TimeOfDay.fromDateTime(breakModel.endTime), + breakModel.endTime!, ); // TODO(Joey): Watch out with gesture detectors @@ -158,7 +158,7 @@ class BreakDisplay extends StatelessWidget { child: Row( children: [ Text( - "${breakModel.duration.inMinutes} " + "${breakModel.duration!.inMinutes} " "${translations.timeMinutes} | " "$starTime - " "$endTime", @@ -183,22 +183,22 @@ class AvailabilityBreakSelectionDialog extends StatefulWidget { }); /// The initial break to show in the dialog if any - final AvailabilityBreakModel? initialBreak; + final BreakViewModel? initialBreak; /// Whether the dialog is used to edit a template /// This will change the description of the dialog final bool editingTemplate; /// Opens the dialog to add a break - static Future show( + static Future show( BuildContext context, { required AvailabilityOptions options, required String userId, required AvailabilityService service, required bool editingTemplate, - AvailabilityBreakModel? initialBreak, + BreakViewModel? initialBreak, }) async => - showModalBottomSheet( + showModalBottomSheet( context: context, useSafeArea: false, shape: const RoundedRectangleBorder( @@ -225,20 +225,12 @@ class AvailabilityBreakSelectionDialog extends StatefulWidget { class _AvailabilityBreakSelectionDialogState extends State { - late TimeOfDay? _startTime; - late TimeOfDay? _endTime; - late Duration? _duration; + late BreakViewModel _breakViewModel; @override void initState() { super.initState(); - _startTime = widget.initialBreak != null - ? TimeOfDay.fromDateTime(widget.initialBreak!.startTime) - : null; - _endTime = widget.initialBreak != null - ? TimeOfDay.fromDateTime(widget.initialBreak!.endTime) - : null; - _duration = widget.initialBreak?.duration; + _breakViewModel = widget.initialBreak ?? const BreakViewModel(); } @override @@ -252,44 +244,31 @@ class _AvailabilityBreakSelectionDialogState void onUpdateDuration(Duration duration) { setState(() { - _duration = duration; + _breakViewModel = _breakViewModel.copyWith(duration: duration); }); } void onUpdateStart(TimeOfDay start) { setState(() { - _startTime = start; + _breakViewModel = _breakViewModel.copyWith(startTime: start); }); } void onUpdateEnd(TimeOfDay end) { setState(() { - _endTime = end; + _breakViewModel = _breakViewModel.copyWith(endTime: end); }); } - var canSave = _startTime != null && _endTime != null; + var canSave = _breakViewModel.canSave; var onSaveButtonPress = canSave ? () { - var breakModel = AvailabilityBreakModel( - startTime: DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - _startTime!.hour, - _startTime!.minute, - ), - endTime: DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - _endTime!.hour, - _endTime!.minute, - ), - duration: _duration, - ); - Navigator.of(context).pop(breakModel); + if (_breakViewModel.isValid) { + Navigator.of(context).pop(_breakViewModel); + } + debugPrint("Break is not valid"); + // TODO(freek): show error message } : null; @@ -333,7 +312,7 @@ class _AvailabilityBreakSelectionDialogState Expanded( flex: 2, child: DurationInputField( - initialValue: _duration, + initialValue: _breakViewModel.duration, onDurationChanged: onUpdateDuration, ), ), @@ -342,13 +321,15 @@ class _AvailabilityBreakSelectionDialogState ), const SizedBox(height: 24), TimeSelection( + key: ValueKey( + [_breakViewModel.startTime, _breakViewModel.endTime], + ), // rebuild the widget when the start or end time changes - key: ValueKey([_startTime, _endTime]), title: translations.pauseDialogPeriodTitle, description: translations.pauseDialogPeriodDescription, crossAxisAlignment: CrossAxisAlignment.center, - startTime: _startTime, - endTime: _endTime, + startTime: _breakViewModel.startTime, + endTime: _breakViewModel.endTime, onStartChanged: onUpdateStart, onEndChanged: onUpdateEnd, ), diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_time_break.dart b/packages/flutter_availability/lib/src/ui/widgets/template_time_break.dart index e845170..e857789 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_time_break.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_time_break.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_availability/src/ui/models/view_template_daydata.dart"; +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; import "package:flutter_availability/src/ui/widgets/pause_selection.dart"; import "package:flutter_availability/src/ui/widgets/template_time_selection.dart"; @@ -8,15 +8,15 @@ class TemplateTimeAndBreakSection extends StatelessWidget { /// const TemplateTimeAndBreakSection({ required this.onDayDataChanged, - this.dayData = const ViewDayTemplateData(), + this.dayData = const DayTemplateDataViewModel(), super.key, }); /// The day data to display and edit - final ViewDayTemplateData dayData; + final DayTemplateDataViewModel dayData; /// Callback for when the day data changes - final void Function(ViewDayTemplateData data) onDayDataChanged; + final void Function(DayTemplateDataViewModel data) onDayDataChanged; @override Widget build(BuildContext context) => Column( diff --git a/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart b/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart index f9a72c4..37d9cf8 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/template_week_overview.dart @@ -1,5 +1,7 @@ import "package:flutter/material.dart"; -import "package:flutter_availability/src/ui/models/view_template_daydata.dart"; +import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; +import "package:flutter_availability/src/ui/view_models/week_template_view_models.dart"; import "package:flutter_availability/src/ui/widgets/calendar_grid.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -13,7 +15,7 @@ class TemplateWeekOverview extends StatelessWidget { }); /// The template to show - final AvailabilityTemplateModel template; + final WeekTemplateViewModel template; @override Widget build(BuildContext context) { @@ -26,7 +28,7 @@ class TemplateWeekOverview extends StatelessWidget { var dayNames = getDaysOfTheWeekAsStrings(translations, context); - var templateData = template.templateData as WeekTemplateData; + var templateData = template.data; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -50,11 +52,8 @@ class TemplateWeekOverview extends StatelessWidget { for (var day in WeekDay.values) ...[ _TemplateDayDetailRow( dayName: dayNames[day.index], - dayData: templateData.data.containsKey(day) - ? ViewDayTemplateData.fromDayTemplateData( - templateData.data[day]!, - ) - : null, + dayData: + templateData.containsKey(day) ? templateData[day] : null, isOdd: day.index.isOdd, ), ], @@ -81,7 +80,7 @@ class _TemplateDayDetailRow extends StatelessWidget { final bool isOdd; /// The data of the day - final ViewDayTemplateData? dayData; + final DayTemplateDataViewModel? dayData; @override Widget build(BuildContext context) { @@ -102,7 +101,7 @@ class _TemplateDayDetailRow extends StatelessWidget { dayPeriod = translations.unavailable; } - var breaks = dayData?.breaks ?? []; + var breaks = dayData?.breaks ?? []; BoxDecoration? boxDecoration; if (isOdd) { @@ -134,7 +133,7 @@ class _TemplateDayDetailRow extends StatelessWidget { // for each break add a line for (var dayBreak in breaks) ...[ const SizedBox(height: 4), - _TemplateDayDetailPauseRow(dayBreak: dayBreak), + _TemplateDayDetailPauseRow(dayBreakViewModel: dayBreak), ], ], ), @@ -144,10 +143,10 @@ class _TemplateDayDetailRow extends StatelessWidget { class _TemplateDayDetailPauseRow extends StatelessWidget { const _TemplateDayDetailPauseRow({ - required this.dayBreak, + required this.dayBreakViewModel, }); - final AvailabilityBreakModel dayBreak; + final BreakViewModel dayBreakViewModel; @override Widget build(BuildContext context) { @@ -157,6 +156,7 @@ class _TemplateDayDetailPauseRow extends StatelessWidget { var options = availabilityScope.options; var translations = options.translations; + var dayBreak = dayBreakViewModel.toBreak(); var startTime = TimeOfDay.fromDateTime(dayBreak.startTime); var endTime = TimeOfDay.fromDateTime(dayBreak.endTime); var startTimeString = translations.timeFormatter(context, startTime);