diff --git a/packages/flutter_availability/lib/src/routes.dart b/packages/flutter_availability/lib/src/routes.dart index f245cd3..bc7078e 100644 --- a/packages/flutter_availability/lib/src/routes.dart +++ b/packages/flutter_availability/lib/src/routes.dart @@ -1,5 +1,6 @@ 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/screens/template_availability_day_overview.dart"; import "package:flutter_availability/src/ui/screens/template_day_edit.dart"; import "package:flutter_availability/src/ui/screens/template_overview.dart"; @@ -8,8 +9,8 @@ import "package:flutter_availability_data_interface/flutter_availability_data_in /// MaterialPageRoute homePageRoute(VoidCallback onExit) => MaterialPageRoute( builder: (context) => AvailabilityOverview( - onEditDateRange: (range) async => - Navigator.of(context).push(availabilityViewRoute(range.start)), + onEditDateRange: (range, availabilities) async => Navigator.of(context) + .push(availabilityViewRoute(range, availabilities)), onViewTemplates: () async => Navigator.of(context).push(templateOverviewRoute()), onExit: () => onExit(), @@ -44,12 +45,14 @@ MaterialPageRoute templateEditDayRoute(AvailabilityTemplateModel? template) => /// MaterialPageRoute availabilityViewRoute( - DateTime date, + DateTimeRange dateRange, + List initialAvailabilities, ) => MaterialPageRoute( - builder: (context) => AvailabilityDayOverview( - date: date, - onAvailabilitySaved: () { + builder: (context) => AvailabilityModificationView( + dateRange: dateRange, + initialAvailabilities: initialAvailabilities, + onExit: () { Navigator.of(context).pop(); }, ), diff --git a/packages/flutter_availability/lib/src/service/availability_service.dart b/packages/flutter_availability/lib/src/service/availability_service.dart index 3ed041b..008b2a1 100644 --- a/packages/flutter_availability/lib/src/service/availability_service.dart +++ b/packages/flutter_availability/lib/src/service/availability_service.dart @@ -10,27 +10,59 @@ class AvailabilityService { required this.dataInterface, }); - /// + /// The user id for which the availabilities are managed final String userId; - /// + /// The data interface that is used to store and retrieve data final AvailabilityDataInterface dataInterface; /// Creates a set of availabilities for the given [range], where every /// availability is a copy of [availability] with only date information /// changed - Future createAvailability( - AvailabilityModel availability, - DateTimeRange range, - ) async { + Future createAvailability({ + required AvailabilityModel availability, + required DateTimeRange range, + required TimeOfDay startTime, + required TimeOfDay endTime, + }) async { + // apply the startTime and endTime to the availability model + var updatedAvailability = availability.copyWith( + startDate: DateTime( + range.start.year, + range.start.month, + range.start.day, + startTime.hour, + startTime.minute, + ), + endDate: DateTime( + range.start.year, + range.start.month, + range.start.day, + endTime.hour, + endTime.minute, + ), + ); + await dataInterface.createAvailabilitiesForUser( userId: userId, - availability: availability, + availability: updatedAvailability, start: range.start, end: range.end, ); } + /// removes all the given [availabilities] from the data store + Future clearAvailabilities( + List availabilities, + ) async { + for (var availability in availabilities) { + await dataInterface.deleteAvailabilityForUser( + userId, + availability.id!, + ); + } + } + /// Returns a stream where data from availabilities and templates are merged Stream> getOverviewDataForMonth( DateTime dayInMonth, @@ -173,3 +205,11 @@ extension RetrieveUniqueTemplates on List { }, ); } + +/// Extension to retrieve [AvailabilityModel] from a list +/// of [AvailabilityWithTemplate] +extension TransformAvailabilityWithTemplate on List { + /// Retrieve all availabilities from a list of [AvailabilityWithTemplate] + List getAvailabilities() => + map((entry) => entry.availabilityModel).toList(); +} diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart index ac0008a..253ed53 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/service/availability_service.dart"; import "package:flutter_availability/src/ui/widgets/calendar.dart"; import "package:flutter_availability/src/ui/widgets/template_legend.dart"; import "package:flutter_availability/src/util/scope.dart"; @@ -14,8 +15,11 @@ class AvailabilityOverview extends StatefulHookWidget { super.key, }); - /// Callback for when the user clicks on a day - final void Function(DateTimeRange range) onEditDateRange; + /// Callback for when the user gives an availability range + final void Function( + DateTimeRange range, + List selectedAvailabilities, + ) onEditDateRange; /// Callback for when the user wants to navigate to the overview of templates final VoidCallback onViewTemplates; @@ -74,12 +78,23 @@ class _AvailabilityOverviewState extends State { availabilities: availabilitySnapshot, ); - // if there is no range selected we want to disable the button var onButtonPress = _selectedRange == null ? null : () { + var availabilitesWithinSelectedRange = availabilitySnapshot.data + ?.where( + (a) => + a.availabilityModel.startDate + .isAfter(_selectedRange!.start) && + a.availabilityModel.endDate + .isBefore(_selectedRange!.end), + ) + .toList() ?? + []; + widget.onEditDateRange( - DateTimeRange(start: DateTime(1), end: DateTime(2)), + _selectedRange!, + availabilitesWithinSelectedRange, ); }; diff --git a/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart index be15326..fe311d5 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart @@ -1,448 +1,191 @@ import "package:flutter/material.dart"; +import "package:flutter_availability/src/service/availability_service.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"; +import "package:flutter_availability/src/ui/widgets/pause_selection.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; -import "package:intl/intl.dart"; /// -class AvailabilityDayOverview extends StatefulWidget { +class AvailabilityModificationView extends StatefulWidget { /// - const AvailabilityDayOverview({ - required this.date, - required this.onAvailabilitySaved, - this.initialAvailability, + const AvailabilityModificationView({ + required this.dateRange, + required this.onExit, + required this.initialAvailabilities, super.key, }); /// The date for which the availability is being managed - final DateTime date; + final DateTimeRange dateRange; - /// The initial availability for the day - final AvailabilityModel? initialAvailability; + /// The initial availabilities for the selected period + final List initialAvailabilities; - /// Callback for when the availability is saved - final Function() onAvailabilitySaved; + /// Callback for when the user wants to navigate back or the + /// availabilities have been saved + final VoidCallback onExit; @override - State createState() => - _AvailabilityDayOverviewState(); + State createState() => + _AvailabilityModificationViewState(); } -class _AvailabilityDayOverviewState extends State { - late TextEditingController _startDateController; - late TextEditingController _endDateController; +class _AvailabilityModificationViewState + extends State { late AvailabilityModel _availability; - bool _clearAvailableToday = false; + bool _clearAvailability = false; + TimeOfDay? _startTime; + TimeOfDay? _endTime; @override void initState() { super.initState(); - _availability = widget.initialAvailability ?? - AvailabilityModel( - userId: "", - startDate: widget.date, - endDate: widget.date, - breaks: [], - ); - _startDateController = TextEditingController( - text: DateFormat("HH:mm").format(_availability.startDate), - ); - _endDateController = TextEditingController( - text: DateFormat("HH:mm").format(_availability.endDate), - ); - } - - @override - void dispose() { - _startDateController.dispose(); - _endDateController.dispose(); - super.dispose(); - } - - Future _selectTime(TextEditingController controller) async { - var picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null) { - setState(() { - controller.text = picked.format(context); - }); - return picked; - } - return null; + _availability = + widget.initialAvailabilities.getAvailabilities().firstOrNull ?? + AvailabilityModel( + userId: "", + startDate: widget.dateRange.start, + endDate: widget.dateRange.end, + breaks: [], + ); } @override Widget build(BuildContext context) { var availabilityScope = AvailabilityScope.of(context); - var userId = availabilityScope.userId; var service = availabilityScope.service; + var options = availabilityScope.options; + var spacing = options.spacing; + var translations = options.translations; - Future updateAvailabilityStart() async { - var selectedTime = await _selectTime(_startDateController); - if (selectedTime == null) return; + // TODO(freek): the selected period might be longer than 1 month + //so we need to get all the availabilites through a stream - var updatedStartDate = _availability.startDate.copyWith( - hour: selectedTime.hour, - minute: selectedTime.minute, - ); - setState(() { - _availability = _availability.copyWith(startDate: updatedStartDate); - }); - } - - Future updateAvailabilityEnd() async { - var selectedTime = await _selectTime(_endDateController); - if (selectedTime == null) return; - - var updatedEndDate = _availability.endDate.copyWith( - hour: selectedTime.hour, - minute: selectedTime.minute, - ); - setState(() { - _availability = _availability.copyWith(endDate: updatedEndDate); - }); - } - - Future onClickAddPause() async { - var newBreak = await AvailabilityBreakSelectionDialog.show(context, null); - if (newBreak != null) { - setState(() { - _availability.breaks.add(newBreak); - }); + Future onSave() async { + if (_clearAvailability) { + await service.clearAvailabilities( + widget.initialAvailabilities.getAvailabilities(), + ); + widget.onExit(); + return; } - } - - Future onClickSave() async { - if (_clearAvailableToday) { - // remove the availability for the user - if (_availability.id != null) { - await service.dataInterface.deleteAvailabilityForUser( - userId, - _availability.id!, - ); - } - } else { - // add an availability for the user - await service.dataInterface.createAvailabilitiesForUser( - userId: userId, - availability: _availability, - start: widget.date, - end: widget.date, + if (widget.initialAvailabilities.isNotEmpty) { + await service.clearAvailabilities( + widget.initialAvailabilities.getAvailabilities(), ); } - if (context.mounted) { - widget.onAvailabilitySaved(); - } + + await service.createAvailability( + availability: _availability, + range: widget.dateRange, + startTime: _startTime!, + endTime: _endTime!, + ); + widget.onExit(); } - var theme = Theme.of(context); - return Scaffold( - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Text( - DateFormat.yMMMMd().format(widget.date), - style: theme.textTheme.bodyLarge, - ), - Row( - children: [ - Checkbox( - value: _clearAvailableToday, - onChanged: (value) { - setState(() { - _clearAvailableToday = value!; - }); - }, - ), - const Text("Clear availability for today"), + var canSave = + _clearAvailability || (_startTime != null && _endTime != null); + var saveButton = options.primaryButtonBuilder( + context, + canSave ? onSave : null, + Text(translations.saveButton), + ); + + var clearSection = AvailabilityClearSection( + range: widget.dateRange, + clearAvailable: _clearAvailability, + onChanged: (isChecked) { + setState(() { + _clearAvailability = isChecked; + }); + }, + ); + + var templateSelection = const AvailabilityTemplateSelection(); + + var timeSelection = AvailabilityTimeSelection( + dateRange: widget.dateRange, + startTime: _startTime != null + ? DateTime( + widget.dateRange.start.year, + widget.dateRange.start.month, + widget.dateRange.start.day, + _startTime!.hour, + _startTime!.minute, + ) + : null, + endTime: _endTime != null + ? DateTime( + widget.dateRange.start.year, + widget.dateRange.start.month, + widget.dateRange.start.day, + _endTime!.hour, + _endTime!.minute, + ) + : null, + key: ValueKey([_startTime, _endTime]), + onStartChanged: (start) => setState(() { + _startTime = TimeOfDay.fromDateTime(start); + }), + onEndChanged: (end) => setState(() { + _endTime = TimeOfDay.fromDateTime(end); + }), + ); + + var pauseSelection = PauseSelection( + breaks: _availability.breaks, + onBreaksChanged: (breaks) { + setState(() { + _availability = _availability.copyWith(breaks: breaks); + }); + }, + ); + + var body = CustomScrollView( + slivers: [ + SliverPadding( + padding: + EdgeInsets.symmetric(horizontal: options.spacing.sidePadding), + sliver: SliverList.list( + children: [ + const SizedBox(height: 40), + clearSection, + if (!_clearAvailability) ...[ + const SizedBox(height: 24), + templateSelection, + const SizedBox(height: 24), + timeSelection, + const SizedBox(height: 26), + pauseSelection, ], - ), - Opacity( - opacity: _clearAvailableToday ? 0.5 : 1, - child: IgnorePointer( - ignoring: _clearAvailableToday, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: updateAvailabilityStart, - child: AbsorbPointer( - child: TextField( - controller: _startDateController, - decoration: const InputDecoration( - labelText: "Begin tijd", - suffixIcon: Icon(Icons.access_time), - ), - ), - ), - ), - ), - const SizedBox(width: 16), - const Text("tot"), - const SizedBox(width: 16), - Expanded( - child: GestureDetector( - onTap: updateAvailabilityEnd, - child: AbsorbPointer( - child: TextField( - controller: _endDateController, - decoration: const InputDecoration( - labelText: "Eind tijd", - suffixIcon: Icon(Icons.access_time), - ), - ), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - const Text("Add pause (optional)"), - ListView( - shrinkWrap: true, - children: _availability.breaks.map( - (breakModel) { - var start = - DateFormat("HH:mm").format(breakModel.startTime); - var end = - DateFormat("HH:mm").format(breakModel.endTime); - return GestureDetector( - onTap: () async { - var updatedBreak = - await AvailabilityBreakSelectionDialog.show( - context, - breakModel, - ); - if (updatedBreak != null) { - setState(() { - _availability.breaks.remove(breakModel); - _availability.breaks.add(updatedBreak); - }); - } - }, - child: Container( - decoration: const BoxDecoration( - color: Colors.lightBlue, - ), - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Text( - "${breakModel.duration.inMinutes}" - " minutes | ", - ), - Text( - "$start - " - "$end", - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - setState(() { - _availability.breaks.remove(breakModel); - }); - }, - ), - ], - ), - ), - ); - }, - ).toList(), - ), - TextButton( - onPressed: onClickAddPause, - child: const Text("Add"), - ), - ], - ), - ), - ), - const Spacer(), - ElevatedButton( - onPressed: () async => onClickSave(), - child: const Text(""), - ), - ], - ), - ), - ); - } -} - -/// -class AvailabilityBreakSelectionDialog extends StatefulWidget { - /// - const AvailabilityBreakSelectionDialog({ - required this.initialBreak, - super.key, - }); - - /// The initial break to show in the dialog if any - final AvailabilityBreakModel? initialBreak; - - /// Opens the dialog to add a break - static Future show( - BuildContext context, - AvailabilityBreakModel? initialBreak, - ) async => - showDialog( - context: context, - builder: (context) => AvailabilityBreakSelectionDialog( - initialBreak: initialBreak, - ), - ); - - @override - State createState() => - _AvailabilityBreakSelectionDialogState(); -} - -class _AvailabilityBreakSelectionDialogState - extends State { - late TextEditingController _durationController; - late TextEditingController _startPauseController; - late TextEditingController _endPauseController; - late AvailabilityBreakModel _breakModel; - - @override - void initState() { - super.initState(); - _breakModel = widget.initialBreak ?? - AvailabilityBreakModel( - startTime: DateTime.now(), - endTime: DateTime.now(), - ); - _durationController = TextEditingController( - text: _breakModel.duration.inMinutes.toString(), - ); - _startPauseController = TextEditingController( - text: DateFormat("HH:mm").format(_breakModel.startTime), - ); - _endPauseController = TextEditingController( - text: DateFormat("HH:mm").format(_breakModel.endTime), - ); - } - - @override - void dispose() { - _durationController.dispose(); - _startPauseController.dispose(); - _endPauseController.dispose(); - super.dispose(); - } - - Future _selectTime( - BuildContext context, - TextEditingController controller, - ) async { - var picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null) { - setState(() { - controller.text = picked.format(context); - }); - return picked; - } - return null; - } - - @override - Widget build(BuildContext context) { - void onUpdateDuration() { - var duration = int.tryParse(_durationController.text); - if (duration != null) { - setState(() { - _breakModel = _breakModel.copyWith( - duration: Duration(minutes: duration), - ); - }); - } - } - - Future onUpdateStart() async { - var selectedTime = await _selectTime(context, _startPauseController); - if (selectedTime == null) return; - - var updatedStartTime = _breakModel.startTime.copyWith( - hour: selectedTime.hour, - minute: selectedTime.minute, - ); - setState(() { - _breakModel = _breakModel.copyWith(startTime: updatedStartTime); - }); - } - - Future onUpdateEnd() async { - var selectedTime = await _selectTime(context, _endPauseController); - if (selectedTime == null) return; - - var updatedEndTime = _breakModel.endTime.copyWith( - hour: selectedTime.hour, - minute: selectedTime.minute, - ); - setState(() { - _breakModel = _breakModel.copyWith(endTime: updatedEndTime); - }); - } - - return AlertDialog( - title: const Text("Pauze toevoegen"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // textfield for duration in minutes - TextField( - controller: _durationController, - decoration: const InputDecoration( - labelText: "Duration in minutes", - ), - onChanged: (_) => onUpdateDuration(), + ], ), - TextField( - controller: _startPauseController, - decoration: const InputDecoration( - labelText: "Start time", - suffixIcon: Icon(Icons.access_time), - ), - readOnly: true, - onTap: () async => onUpdateStart(), - ), - TextField( - controller: _endPauseController, - decoration: const InputDecoration( - labelText: "End time", - suffixIcon: Icon(Icons.access_time), - ), - readOnly: true, - onTap: () async => onUpdateEnd(), - ), - ], - ), - actions: [ - TextButton( - child: const Text("Cancel"), - onPressed: () { - Navigator.of(context).pop(); - }, ), - TextButton( - child: const Text("Save"), - onPressed: () { - Navigator.of(context).pop(_breakModel); - }, + SliverFillRemaining( + fillOverscroll: false, + hasScrollBody: false, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: spacing.sidePadding, + ).copyWith( + bottom: spacing.bottomButtonPadding, + ), + child: Align( + alignment: Alignment.bottomCenter, + child: saveButton, + ), + ), ), ], ); + + return options.baseScreenBuilder( + context, + widget.onExit, + body, + ); } } diff --git a/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart b/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart index 43b071c..61f3e52 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/availability_clear.dart @@ -47,15 +47,20 @@ class AvailabilityClearSection extends StatelessWidget { titleText, style: textTheme.titleMedium, ), + const SizedBox(height: 8), Row( children: [ Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + splashRadius: 0, value: clearAvailable, onChanged: (value) { if (value == null) return; onChanged(value); }, ), + const SizedBox(width: 8), Text( unavailableText, style: textTheme.bodyMedium, diff --git a/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart b/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart index 4b9a31e..18de772 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/availability_template_selection.dart @@ -1,10 +1,10 @@ import "package:flutter/material.dart"; /// Selection of the template to use for the availability -/// +/// /// This can show multiple templates when the user selects a date range. -/// When updating the templates for a date range where there are multiple -/// different templates used, the user first needs to remove the existing +/// When updating the templates for a date range where there are multiple +/// different templates used, the user first needs to remove the existing /// templates. class AvailabilityTemplateSelection extends StatelessWidget { /// Constructor 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 94b14f5..45a88b6 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/pause_selection.dart @@ -81,6 +81,7 @@ class PauseSelection extends StatelessWidget { return Column( children: [ Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( translations.pauseSectionTitle, diff --git a/packages/flutter_availability/pubspec.yaml b/packages/flutter_availability/pubspec.yaml index d799842..1ea056c 100644 --- a/packages/flutter_availability/pubspec.yaml +++ b/packages/flutter_availability/pubspec.yaml @@ -10,7 +10,6 @@ environment: dependencies: flutter: sdk: flutter - intl: any rxdart: ^0.27.0 flutter_hooks: ^0.20.5 flutter_availability_data_interface: