fix: use template viewmodels for the UI state

This commit is contained in:
Freek van de Ven 2024-07-16 11:31:38 +02:00 committed by FlutterJoey
parent fd81964b75
commit 313a402409
11 changed files with 387 additions and 169 deletions

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_availability/src/service/availability_service.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_clear.dart";
import "package:flutter_availability/src/ui/widgets/availability_template_selection.dart"; import "package:flutter_availability/src/ui/widgets/availability_template_selection.dart";
import "package:flutter_availability/src/ui/widgets/availabillity_time_selection.dart"; import "package:flutter_availability/src/ui/widgets/availabillity_time_selection.dart";
@ -172,12 +173,16 @@ class _AvailabilitiesModificationScreenState
); );
var pauseSelection = PauseSelection( var pauseSelection = PauseSelection(
breaks: _availability.breaks, breaks: _availability.breaks
.map(BreakViewModel.fromAvailabilityBreakModel)
.toList(),
editingTemplate: false, editingTemplate: false,
// TODO(Joey): Extract these // TODO(Joey): Extract these
onBreaksChanged: (breaks) { onBreaksChanged: (breaks) {
setState(() { setState(() {
_availability = _availability.copyWith(breaks: breaks); _availability = _availability.copyWith(
breaks: breaks.map((b) => b.toBreak()).toList(),
);
}); });
}, },
); );

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; 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/color_selection.dart";
import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart";
import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart";
@ -28,25 +28,16 @@ class DayTemplateModificationScreen extends StatefulWidget {
class _DayTemplateModificationScreenState class _DayTemplateModificationScreenState
extends State<DayTemplateModificationScreen> { extends State<DayTemplateModificationScreen> {
late int? _selectedColor; late DayTemplateViewModel _viewModel;
late AvailabilityTemplateModel _template;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedColor = widget.template?.color; if (widget.template != null) {
_template = widget.template ?? _viewModel = DayTemplateViewModel.fromTemplate(widget.template!);
AvailabilityTemplateModel( } else {
userId: "1", _viewModel = const DayTemplateViewModel();
name: "", }
color: 0,
templateType: AvailabilityTemplateType.day,
templateData: DayTemplateData(
startTime: DateTime.now(),
endTime: DateTime.now(),
breaks: [],
),
);
} }
@override @override
@ -59,20 +50,21 @@ class _DayTemplateModificationScreenState
var spacing = options.spacing; var spacing = options.spacing;
Future<void> onDeletePressed() async { Future<void> onDeletePressed() async {
await service.deleteTemplate(_template); await service.deleteTemplate(widget.template!);
widget.onExit(); widget.onExit();
} }
Future<void> onSavePressed() async { Future<void> onSavePressed() async {
var template = _viewModel.toTemplate();
if (widget.template == null) { if (widget.template == null) {
await service.createTemplate(_template); await service.createTemplate(template);
} else { } else {
await service.updateTemplate(_template); await service.updateTemplate(template);
} }
widget.onExit(); widget.onExit();
} }
var canSave = _template.name.isNotEmpty && _selectedColor != null; var canSave = _viewModel.canSave;
var saveButton = options.primaryButtonBuilder( var saveButton = options.primaryButtonBuilder(
context, context,
@ -94,34 +86,29 @@ class _DayTemplateModificationScreenState
); );
var templateTitleSection = TemplateNameInput( var templateTitleSection = TemplateNameInput(
initialValue: _template.name, initialValue: _viewModel.name,
onNameChanged: (name) { onNameChanged: (name) {
setState(() { setState(() {
_template = _template.copyWith(name: name); _viewModel = _viewModel.copyWith(name: name);
}); });
}, },
); );
var colorSection = TemplateColorSelection( var colorSection = TemplateColorSelection(
selectedColor: _selectedColor, selectedColor: _viewModel.color,
// TODO(Joey): Extract this // TODO(Joey): Extract this
onColorSelected: (color) { onColorSelected: (color) {
setState(() { setState(() {
_selectedColor = color; _viewModel = _viewModel.copyWith(color: color);
_template = _template.copyWith(color: color);
}); });
}, },
); );
var availabilitySection = TemplateTimeAndBreakSection( var availabilitySection = TemplateTimeAndBreakSection(
dayData: ViewDayTemplateData.fromDayTemplateData( dayData: _viewModel.data,
_template.templateData as DayTemplateData,
),
onDayDataChanged: (data) { onDayDataChanged: (data) {
setState(() { setState(() {
_template = _template.copyWith( _viewModel = _viewModel.copyWith(data: data);
templateData: data.toDayTemplateData(),
);
}); });
}, },
); );

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; 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/color_selection.dart";
import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart";
import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart";
@ -30,23 +31,16 @@ class WeekTemplateModificationScreen extends StatefulWidget {
class _WeekTemplateModificationScreenState class _WeekTemplateModificationScreenState
extends State<WeekTemplateModificationScreen> { extends State<WeekTemplateModificationScreen> {
late int? _selectedColor;
late AvailabilityTemplateModel _template;
bool _editing = true; bool _editing = true;
WeekDay _selectedDay = WeekDay.monday; WeekDay _selectedDay = WeekDay.monday;
late WeekTemplateViewModel _viewModel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedColor = widget.template?.color; _viewModel = widget.template != null
_template = widget.template ?? ? WeekTemplateViewModel.fromTemplate(widget.template!)
AvailabilityTemplateModel( : const WeekTemplateViewModel();
userId: "1",
name: "",
color: 0,
templateType: AvailabilityTemplateType.week,
templateData: WeekTemplateData.forDays(),
);
} }
@override @override
@ -59,11 +53,31 @@ class _WeekTemplateModificationScreenState
var translations = options.translations; var translations = options.translations;
var spacing = options.spacing; var spacing = options.spacing;
var weekTemplateDate = _template.templateData as WeekTemplateData; var weekTemplateDate = _viewModel.data;
var selectedDayData = weekTemplateDate.data[_selectedDay]; var selectedDayData = weekTemplateDate[_selectedDay];
void onDayDataChanged(DayTemplateDataViewModel data) {
setState(() {
// create a new copy of an unmodifiable map that can be modified
var updatedDays =
Map<WeekDay, DayTemplateDataViewModel>.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<void> onDeletePressed() async { Future<void> onDeletePressed() async {
await service.deleteTemplate(_template); await service.deleteTemplate(widget.template!);
widget.onExit(); widget.onExit();
} }
@ -84,15 +98,20 @@ class _WeekTemplateModificationScreenState
} }
Future<void> onSavePressed() async { Future<void> onSavePressed() async {
if (!_viewModel.isValid) {
// TODO(freek): show error message
return;
}
var template = _viewModel.toTemplate();
if (widget.template == null) { if (widget.template == null) {
await service.createTemplate(_template); await service.createTemplate(template);
} else { } else {
await service.updateTemplate(_template); await service.updateTemplate(template);
} }
widget.onExit(); widget.onExit();
} }
var canSave = _template.name.isNotEmpty && _selectedColor != null; var canSave = _viewModel.canSave;
var nextButton = options.primaryButtonBuilder( var nextButton = options.primaryButtonBuilder(
context, context,
canSave ? onNextPressed : null, canSave ? onNextPressed : null,
@ -125,20 +144,19 @@ class _WeekTemplateModificationScreenState
); );
var templateTitleSection = TemplateNameInput( var templateTitleSection = TemplateNameInput(
initialValue: _template.name, initialValue: _viewModel.name,
onNameChanged: (name) { onNameChanged: (name) {
setState(() { setState(() {
_template = _template.copyWith(name: name); _viewModel = _viewModel.copyWith(name: name);
}); });
}, },
); );
var colorSection = TemplateColorSelection( var colorSection = TemplateColorSelection(
selectedColor: _selectedColor, selectedColor: _viewModel.color,
onColorSelected: (color) { onColorSelected: (color) {
setState(() { setState(() {
_selectedColor = color; _viewModel = _viewModel.copyWith(color: color);
_template = _template.copyWith(color: color);
}); });
}, },
); );
@ -148,37 +166,13 @@ class _WeekTemplateModificationScreenState
const SizedBox(height: 24), const SizedBox(height: 24),
Padding( Padding(
padding: EdgeInsets.only(left: spacing.sidePadding), padding: EdgeInsets.only(left: spacing.sidePadding),
child: TemplateWeekDaySelection( child: TemplateWeekDaySelection(onDaySelected: onDaySelected),
onDaySelected: (day) {
setState(() {
_selectedDay = WeekDay.values[day];
});
},
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_WeekTemplateSidePadding( _WeekTemplateSidePadding(
child: TemplateTimeAndBreakSection( child: TemplateTimeAndBreakSection(
dayData: selectedDayData != null dayData: selectedDayData ?? const DayTemplateDataViewModel(),
? ViewDayTemplateData.fromDayTemplateData( onDayDataChanged: onDayDataChanged,
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(),
},
),
);
});
},
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -203,20 +197,20 @@ class _WeekTemplateModificationScreenState
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(_template.color), color: Color(_viewModel.color ?? 0),
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
width: 20, width: 20,
height: 20, height: 20,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(_template.name, style: textTheme.bodyLarge), Text(_viewModel.name ?? "", style: textTheme.bodyLarge),
], ],
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
TemplateWeekOverview( TemplateWeekOverview(
template: _template, template: _viewModel,
), ),
], ],
), ),

View file

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

View file

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

View file

@ -1,21 +1,23 @@
import "package:flutter/material.dart"; 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"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
/// The data for creating or editing a day template /// The data for creating or editing a day template
class ViewDayTemplateData { class DayTemplateDataViewModel {
/// Constructor /// Constructor
const ViewDayTemplateData({ const DayTemplateDataViewModel({
this.startTime, this.startTime,
this.endTime, this.endTime,
this.breaks = const [], this.breaks = const [],
}); });
/// Create a new instance from a [DayTemplateData] /// Create a new instance from a [DayTemplateData]
factory ViewDayTemplateData.fromDayTemplateData(DayTemplateData data) => factory DayTemplateDataViewModel.fromDayTemplateData(DayTemplateData data) =>
ViewDayTemplateData( DayTemplateDataViewModel(
startTime: TimeOfDay.fromDateTime(data.startTime), startTime: TimeOfDay.fromDateTime(data.startTime),
endTime: TimeOfDay.fromDateTime(data.endTime), endTime: TimeOfDay.fromDateTime(data.endTime),
breaks: data.breaks, breaks:
data.breaks.map(BreakViewModel.fromAvailabilityBreakModel).toList(),
); );
/// The start time to apply on a new availability /// The start time to apply on a new availability
@ -25,18 +27,28 @@ class ViewDayTemplateData {
final TimeOfDay? endTime; final TimeOfDay? endTime;
/// A list of breaks to apply to every new availability /// A list of breaks to apply to every new availability
final List<AvailabilityBreakModel> breaks; final List<BreakViewModel> breaks;
/// Whether the data is valid for saving /// Whether the data is valid
bool get isValid => startTime != null && endTime != null; /// 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 /// Create a copy with new values
ViewDayTemplateData copyWith({ DayTemplateDataViewModel copyWith({
TimeOfDay? startTime, TimeOfDay? startTime,
TimeOfDay? endTime, TimeOfDay? endTime,
List<AvailabilityBreakModel>? breaks, List<BreakViewModel>? breaks,
}) => }) =>
ViewDayTemplateData( DayTemplateDataViewModel(
startTime: startTime ?? this.startTime, startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime, endTime: endTime ?? this.endTime,
breaks: breaks ?? this.breaks, breaks: breaks ?? this.breaks,
@ -47,6 +59,6 @@ class ViewDayTemplateData {
startTime: startTime:
DateTime(0, 0, 0, startTime?.hour ?? 0, startTime?.minute ?? 0), DateTime(0, 0, 0, startTime?.hour ?? 0, startTime?.minute ?? 0),
endTime: DateTime(0, 0, 0, endTime?.hour ?? 0, endTime?.minute ?? 0), endTime: DateTime(0, 0, 0, endTime?.hour ?? 0, endTime?.minute ?? 0),
breaks: breaks, breaks: breaks.map((b) => b.toBreak()).toList(),
); );
} }

View file

@ -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<WeekDay, DayTemplateDataViewModel> 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<WeekDay, DayTemplateDataViewModel>? data,
}) =>
WeekTemplateViewModel(
id: id ?? this.id,
name: name ?? this.name,
color: color ?? this.color,
data: data ?? this.data,
);
}

View file

@ -1,10 +1,10 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_availability/flutter_availability.dart"; import "package:flutter_availability/flutter_availability.dart";
import "package:flutter_availability/src/service/availability_service.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/generic_time_selection.dart";
import "package:flutter_availability/src/ui/widgets/input_fields.dart"; import "package:flutter_availability/src/ui/widgets/input_fields.dart";
import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/scope.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
/// ///
class PauseSelection extends StatelessWidget { class PauseSelection extends StatelessWidget {
@ -17,10 +17,10 @@ class PauseSelection extends StatelessWidget {
}); });
/// The breaks that are currently set /// The breaks that are currently set
final List<AvailabilityBreakModel> breaks; final List<BreakViewModel> breaks;
/// Callback for when the breaks are changed /// Callback for when the breaks are changed
final void Function(List<AvailabilityBreakModel>) onBreaksChanged; final void Function(List<BreakViewModel>) onBreaksChanged;
/// Whether the pause selection is used for editing a template /// Whether the pause selection is used for editing a template
final bool editingTemplate; final bool editingTemplate;
@ -33,11 +33,12 @@ class PauseSelection extends StatelessWidget {
var options = availabilityScope.options; var options = availabilityScope.options;
var translations = options.translations; var translations = options.translations;
Future<AvailabilityBreakModel?> openBreakDialog( Future<BreakViewModel?> openBreakDialog(
AvailabilityBreakModel? initialBreak, BreakViewModel? initialBreak,
) async => ) async =>
AvailabilityBreakSelectionDialog.show( AvailabilityBreakSelectionDialog.show(
context, context,
initialBreak: initialBreak,
userId: availabilityScope.userId, userId: availabilityScope.userId,
options: options, options: options,
service: availabilityScope.service, service: availabilityScope.service,
@ -52,7 +53,7 @@ class PauseSelection extends StatelessWidget {
onBreaksChanged(updatedBreaks); onBreaksChanged(updatedBreaks);
} }
Future<void> onEditBreak(AvailabilityBreakModel availabilityBreak) async { Future<void> onEditBreak(BreakViewModel availabilityBreak) async {
var updatedBreak = await openBreakDialog(availabilityBreak); var updatedBreak = await openBreakDialog(availabilityBreak);
if (updatedBreak == null) return; if (updatedBreak == null) return;
@ -60,13 +61,12 @@ class PauseSelection extends StatelessWidget {
onBreaksChanged(updatedBreaks); onBreaksChanged(updatedBreaks);
} }
void onDeleteBreak(AvailabilityBreakModel availabilityBreak) { void onDeleteBreak(BreakViewModel availabilityBreak) {
var updatedBreaks = breaks.where((b) => b != availabilityBreak).toList(); var updatedBreaks = breaks.where((b) => b != availabilityBreak).toList();
onBreaksChanged(updatedBreaks); onBreaksChanged(updatedBreaks);
} }
var sortedBreaks = breaks.toList() var sortedBreaks = breaks.toList()..sort((a, b) => a.compareTo(b));
..sort((a, b) => a.startTime.compareTo(b.startTime));
var addButton = options.bigTextButtonWrapperBuilder( var addButton = options.bigTextButtonWrapperBuilder(
context, context,
@ -120,7 +120,7 @@ class BreakDisplay extends StatelessWidget {
}); });
/// The break to display /// The break to display
final AvailabilityBreakModel breakModel; final BreakViewModel breakModel;
/// Callback for when the minus button is clicked /// Callback for when the minus button is clicked
final VoidCallback onRemove; final VoidCallback onRemove;
@ -138,11 +138,11 @@ class BreakDisplay extends StatelessWidget {
var starTime = translations.timeFormatter( var starTime = translations.timeFormatter(
context, context,
TimeOfDay.fromDateTime(breakModel.startTime), breakModel.startTime!,
); );
var endTime = translations.timeFormatter( var endTime = translations.timeFormatter(
context, context,
TimeOfDay.fromDateTime(breakModel.endTime), breakModel.endTime!,
); );
// TODO(Joey): Watch out with gesture detectors // TODO(Joey): Watch out with gesture detectors
@ -158,7 +158,7 @@ class BreakDisplay extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text( Text(
"${breakModel.duration.inMinutes} " "${breakModel.duration!.inMinutes} "
"${translations.timeMinutes} | " "${translations.timeMinutes} | "
"$starTime - " "$starTime - "
"$endTime", "$endTime",
@ -183,22 +183,22 @@ class AvailabilityBreakSelectionDialog extends StatefulWidget {
}); });
/// The initial break to show in the dialog if any /// 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 /// Whether the dialog is used to edit a template
/// This will change the description of the dialog /// This will change the description of the dialog
final bool editingTemplate; final bool editingTemplate;
/// Opens the dialog to add a break /// Opens the dialog to add a break
static Future<AvailabilityBreakModel?> show( static Future<BreakViewModel?> show(
BuildContext context, { BuildContext context, {
required AvailabilityOptions options, required AvailabilityOptions options,
required String userId, required String userId,
required AvailabilityService service, required AvailabilityService service,
required bool editingTemplate, required bool editingTemplate,
AvailabilityBreakModel? initialBreak, BreakViewModel? initialBreak,
}) async => }) async =>
showModalBottomSheet<AvailabilityBreakModel>( showModalBottomSheet<BreakViewModel>(
context: context, context: context,
useSafeArea: false, useSafeArea: false,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -225,20 +225,12 @@ class AvailabilityBreakSelectionDialog extends StatefulWidget {
class _AvailabilityBreakSelectionDialogState class _AvailabilityBreakSelectionDialogState
extends State<AvailabilityBreakSelectionDialog> { extends State<AvailabilityBreakSelectionDialog> {
late TimeOfDay? _startTime; late BreakViewModel _breakViewModel;
late TimeOfDay? _endTime;
late Duration? _duration;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_startTime = widget.initialBreak != null _breakViewModel = widget.initialBreak ?? const BreakViewModel();
? TimeOfDay.fromDateTime(widget.initialBreak!.startTime)
: null;
_endTime = widget.initialBreak != null
? TimeOfDay.fromDateTime(widget.initialBreak!.endTime)
: null;
_duration = widget.initialBreak?.duration;
} }
@override @override
@ -252,44 +244,31 @@ class _AvailabilityBreakSelectionDialogState
void onUpdateDuration(Duration duration) { void onUpdateDuration(Duration duration) {
setState(() { setState(() {
_duration = duration; _breakViewModel = _breakViewModel.copyWith(duration: duration);
}); });
} }
void onUpdateStart(TimeOfDay start) { void onUpdateStart(TimeOfDay start) {
setState(() { setState(() {
_startTime = start; _breakViewModel = _breakViewModel.copyWith(startTime: start);
}); });
} }
void onUpdateEnd(TimeOfDay end) { void onUpdateEnd(TimeOfDay end) {
setState(() { setState(() {
_endTime = end; _breakViewModel = _breakViewModel.copyWith(endTime: end);
}); });
} }
var canSave = _startTime != null && _endTime != null; var canSave = _breakViewModel.canSave;
var onSaveButtonPress = canSave var onSaveButtonPress = canSave
? () { ? () {
var breakModel = AvailabilityBreakModel( if (_breakViewModel.isValid) {
startTime: DateTime( Navigator.of(context).pop(_breakViewModel);
DateTime.now().year, }
DateTime.now().month, debugPrint("Break is not valid");
DateTime.now().day, // TODO(freek): show error message
_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);
} }
: null; : null;
@ -333,7 +312,7 @@ class _AvailabilityBreakSelectionDialogState
Expanded( Expanded(
flex: 2, flex: 2,
child: DurationInputField( child: DurationInputField(
initialValue: _duration, initialValue: _breakViewModel.duration,
onDurationChanged: onUpdateDuration, onDurationChanged: onUpdateDuration,
), ),
), ),
@ -342,13 +321,15 @@ class _AvailabilityBreakSelectionDialogState
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
TimeSelection( TimeSelection(
key: ValueKey(
[_breakViewModel.startTime, _breakViewModel.endTime],
),
// rebuild the widget when the start or end time changes // rebuild the widget when the start or end time changes
key: ValueKey([_startTime, _endTime]),
title: translations.pauseDialogPeriodTitle, title: translations.pauseDialogPeriodTitle,
description: translations.pauseDialogPeriodDescription, description: translations.pauseDialogPeriodDescription,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
startTime: _startTime, startTime: _breakViewModel.startTime,
endTime: _endTime, endTime: _breakViewModel.endTime,
onStartChanged: onUpdateStart, onStartChanged: onUpdateStart,
onEndChanged: onUpdateEnd, onEndChanged: onUpdateEnd,
), ),

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; 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/pause_selection.dart";
import "package:flutter_availability/src/ui/widgets/template_time_selection.dart"; import "package:flutter_availability/src/ui/widgets/template_time_selection.dart";
@ -8,15 +8,15 @@ class TemplateTimeAndBreakSection extends StatelessWidget {
/// ///
const TemplateTimeAndBreakSection({ const TemplateTimeAndBreakSection({
required this.onDayDataChanged, required this.onDayDataChanged,
this.dayData = const ViewDayTemplateData(), this.dayData = const DayTemplateDataViewModel(),
super.key, super.key,
}); });
/// The day data to display and edit /// The day data to display and edit
final ViewDayTemplateData dayData; final DayTemplateDataViewModel dayData;
/// Callback for when the day data changes /// Callback for when the day data changes
final void Function(ViewDayTemplateData data) onDayDataChanged; final void Function(DayTemplateDataViewModel data) onDayDataChanged;
@override @override
Widget build(BuildContext context) => Column( Widget build(BuildContext context) => Column(

View file

@ -1,5 +1,7 @@
import "package:flutter/material.dart"; 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/ui/widgets/calendar_grid.dart";
import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/scope.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
@ -13,7 +15,7 @@ class TemplateWeekOverview extends StatelessWidget {
}); });
/// The template to show /// The template to show
final AvailabilityTemplateModel template; final WeekTemplateViewModel template;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,7 +28,7 @@ class TemplateWeekOverview extends StatelessWidget {
var dayNames = getDaysOfTheWeekAsStrings(translations, context); var dayNames = getDaysOfTheWeekAsStrings(translations, context);
var templateData = template.templateData as WeekTemplateData; var templateData = template.data;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -50,11 +52,8 @@ class TemplateWeekOverview extends StatelessWidget {
for (var day in WeekDay.values) ...[ for (var day in WeekDay.values) ...[
_TemplateDayDetailRow( _TemplateDayDetailRow(
dayName: dayNames[day.index], dayName: dayNames[day.index],
dayData: templateData.data.containsKey(day) dayData:
? ViewDayTemplateData.fromDayTemplateData( templateData.containsKey(day) ? templateData[day] : null,
templateData.data[day]!,
)
: null,
isOdd: day.index.isOdd, isOdd: day.index.isOdd,
), ),
], ],
@ -81,7 +80,7 @@ class _TemplateDayDetailRow extends StatelessWidget {
final bool isOdd; final bool isOdd;
/// The data of the day /// The data of the day
final ViewDayTemplateData? dayData; final DayTemplateDataViewModel? dayData;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -102,7 +101,7 @@ class _TemplateDayDetailRow extends StatelessWidget {
dayPeriod = translations.unavailable; dayPeriod = translations.unavailable;
} }
var breaks = dayData?.breaks ?? <AvailabilityBreakModel>[]; var breaks = dayData?.breaks ?? <BreakViewModel>[];
BoxDecoration? boxDecoration; BoxDecoration? boxDecoration;
if (isOdd) { if (isOdd) {
@ -134,7 +133,7 @@ class _TemplateDayDetailRow extends StatelessWidget {
// for each break add a line // for each break add a line
for (var dayBreak in breaks) ...[ for (var dayBreak in breaks) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
_TemplateDayDetailPauseRow(dayBreak: dayBreak), _TemplateDayDetailPauseRow(dayBreakViewModel: dayBreak),
], ],
], ],
), ),
@ -144,10 +143,10 @@ class _TemplateDayDetailRow extends StatelessWidget {
class _TemplateDayDetailPauseRow extends StatelessWidget { class _TemplateDayDetailPauseRow extends StatelessWidget {
const _TemplateDayDetailPauseRow({ const _TemplateDayDetailPauseRow({
required this.dayBreak, required this.dayBreakViewModel,
}); });
final AvailabilityBreakModel dayBreak; final BreakViewModel dayBreakViewModel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -157,6 +156,7 @@ class _TemplateDayDetailPauseRow extends StatelessWidget {
var options = availabilityScope.options; var options = availabilityScope.options;
var translations = options.translations; var translations = options.translations;
var dayBreak = dayBreakViewModel.toBreak();
var startTime = TimeOfDay.fromDateTime(dayBreak.startTime); var startTime = TimeOfDay.fromDateTime(dayBreak.startTime);
var endTime = TimeOfDay.fromDateTime(dayBreak.endTime); var endTime = TimeOfDay.fromDateTime(dayBreak.endTime);
var startTimeString = translations.timeFormatter(context, startTime); var startTimeString = translations.timeFormatter(context, startTime);