diff --git a/example/lib/constants.dart b/example/lib/constants.dart index 36ec8416..21da78d8 100644 --- a/example/lib/constants.dart +++ b/example/lib/constants.dart @@ -5,6 +5,8 @@ import 'app_colors.dart'; class AppConstants { AppConstants._(); + static final List weekTitles = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + static OutlineInputBorder inputBorder = OutlineInputBorder( borderRadius: BorderRadius.circular(7), borderSide: BorderSide( diff --git a/example/lib/main.dart b/example/lib/main.dart index 146e23af..119fe64d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -46,6 +46,13 @@ List _events = [ date: _now.add(Duration(days: 1)), startTime: DateTime(_now.year, _now.month, _now.day, 18), endTime: DateTime(_now.year, _now.month, _now.day, 19), + recurrenceSettings: RecurrenceSettings( + startDate: _now, + endDate: _now.add(Duration(days: 5)), + frequency: RepeatFrequency.daily, + interval: 5, + recurrenceEndOn: RecurrenceEnd.after, + ), title: "Wedding anniversary", description: "Attend uncle's wedding anniversary.", ), diff --git a/example/lib/pages/event_details_page.dart b/example/lib/pages/event_details_page.dart index 2785688d..dada34dc 100644 --- a/example/lib/pages/event_details_page.dart +++ b/example/lib/pages/event_details_page.dart @@ -1,4 +1,5 @@ import 'package:calendar_view/calendar_view.dart'; +import 'package:example/pages/web/delete_event_dialog.dart'; import 'package:flutter/material.dart'; import '../extension.dart'; @@ -6,10 +7,18 @@ import 'create_event_page.dart'; class DetailsPage extends StatelessWidget { final CalendarEventData event; + final DateTime date; + + const DetailsPage({ + super.key, + required this.event, + required this.date, + }); - const DetailsPage({super.key, required this.event}); @override Widget build(BuildContext context) { + debugPrint('date details page: ${date}'); + return Scaffold( appBar: AppBar( backgroundColor: event.color, @@ -90,10 +99,8 @@ class DetailsPage extends StatelessWidget { children: [ Expanded( child: ElevatedButton( - onPressed: () { - CalendarControllerProvider.of(context) - .controller - .remove(event); + onPressed: () async { + await _handleDeleteEvent(context); Navigator.of(context).pop(); }, child: Text('Delete Event'), @@ -111,7 +118,7 @@ class DetailsPage extends StatelessWidget { ), ); - if (result) { + if (result != null) { Navigator.of(context).pop(); } }, @@ -124,4 +131,33 @@ class DetailsPage extends StatelessWidget { ), ); } + + /// Handles the deletion of an event, showing a dialog for repeating events. + /// + /// This method checks if the event is a repeating event. If it is, it shows + /// a dialog to the user to choose the deletion type (e.g., delete this + /// event, delete following events, delete all events). + /// If the event is not repeating, it defaults to deleting all occurrences + /// of the event. + Future _handleDeleteEvent(BuildContext context) async { + DeleteEvent? result; + final isRepeatingEvent = event.recurrenceSettings != null && + (event.recurrenceSettings?.frequency != RepeatFrequency.doNotRepeat); + + if (isRepeatingEvent) { + result = await showDialog( + context: context, + builder: (_) => DeleteEventDialog(), + ); + } else { + result = DeleteEvent.all; + } + if (result != null) { + CalendarControllerProvider.of(context).controller.deleteRecurrenceEvent( + date: date, + event: event, + deleteEventType: result, + ); + } + } } diff --git a/example/lib/pages/web/delete_event_dialog.dart b/example/lib/pages/web/delete_event_dialog.dart new file mode 100644 index 00000000..64bfceb6 --- /dev/null +++ b/example/lib/pages/web/delete_event_dialog.dart @@ -0,0 +1,63 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; + +class DeleteEventDialog extends StatefulWidget { + @override + _RadioDialogState createState() => _RadioDialogState(); +} + +class _RadioDialogState extends State { + DeleteEvent _selectedOption = DeleteEvent.current; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Delete recurring event '), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + title: Text('This event'), + value: DeleteEvent.current, + groupValue: _selectedOption, + onChanged: (deleteType) { + if (deleteType != null) { + setState(() => _selectedOption = deleteType); + } + }, + ), + RadioListTile( + title: Text('This and following events'), + value: DeleteEvent.following, + groupValue: _selectedOption, + onChanged: (deleteType) { + if (deleteType != null) { + setState(() => _selectedOption = deleteType); + } + }, + ), + RadioListTile( + title: Text('All events'), + value: DeleteEvent.all, + groupValue: _selectedOption, + onChanged: (deleteType) { + if (deleteType != null) { + setState(() => _selectedOption = deleteType); + } + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(_selectedOption), + child: Text('Done'), + ), + ], + ); + } +} diff --git a/example/lib/widgets/add_event_form.dart b/example/lib/widgets/add_event_form.dart index 40b18718..ed8fd1d3 100644 --- a/example/lib/widgets/add_event_form.dart +++ b/example/lib/widgets/add_event_form.dart @@ -25,15 +25,20 @@ class AddOrEditEventForm extends StatefulWidget { class _AddOrEditEventFormState extends State { late DateTime _startDate = DateTime.now().withoutTime; late DateTime _endDate = DateTime.now().withoutTime; + late DateTime _recurrenceEndDate = DateTime.now().withoutTime; DateTime? _startTime; DateTime? _endTime; + List _selectedDays = List.filled(7, false); + RepeatFrequency? _selectedFrequency = RepeatFrequency.doNotRepeat; + RecurrenceEnd? _selectedRecurrenceEnd = RecurrenceEnd.never; Color _color = Colors.blue; final _form = GlobalKey(); late final _descriptionController = TextEditingController(); + late final _occurrenceController = TextEditingController(); late final _titleController = TextEditingController(); late final _titleNode = FocusNode(); late final _descriptionNode = FocusNode(); @@ -43,6 +48,7 @@ class _AddOrEditEventFormState extends State { super.initState(); _setDefaults(); + _setInitialWeekday(); } @override @@ -52,6 +58,7 @@ class _AddOrEditEventFormState extends State { _descriptionController.dispose(); _titleController.dispose(); + _occurrenceController.dispose(); super.dispose(); } @@ -102,6 +109,7 @@ class _AddOrEditEventFormState extends State { } _startDate = date.withoutTime; + updateWeekdaysSelection(); if (mounted) { setState(() {}); @@ -137,6 +145,8 @@ class _AddOrEditEventFormState extends State { )); } else { _endDate = date.withoutTime; + _recurrenceEndDate = _endDate; + updateWeekdaysSelection(); } if (mounted) { @@ -247,6 +257,245 @@ class _AddOrEditEventFormState extends State { hintText: "Event Description", ), ), + Align( + alignment: Alignment.centerLeft, + child: Text( + "Repeat", + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.w500, + fontSize: 17, + ), + ), + ), + Row( + children: [ + Radio( + value: RepeatFrequency.doNotRepeat, + groupValue: _selectedFrequency, + onChanged: (value) { + setState( + () => _selectedFrequency = value, + ); + }, + ), + Text( + "Do not repeat", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ), + ], + ), + Row( + children: [ + Radio( + value: RepeatFrequency.daily, + groupValue: _selectedFrequency, + onChanged: (value) { + setState( + () => _selectedFrequency = value, + ); + }, + ), + Text( + "Daily", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ) + ], + ), + Row( + children: [ + Radio( + value: RepeatFrequency.weekly, + groupValue: _selectedFrequency, + onChanged: (value) { + setState( + () => _selectedFrequency = value, + ); + }, + ), + Text( + "Weekly", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ), + ], + ), + Row( + children: [ + Radio( + value: RepeatFrequency.monthly, + groupValue: _selectedFrequency, + onChanged: (value) { + setState( + () => _selectedFrequency = value, + ); + }, + ), + Text( + "Monthly", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ), + ], + ), + if (_selectedFrequency == RepeatFrequency.weekly) ...[ + Wrap( + children: List.generate(AppConstants.weekTitles.length, + (int index) { + return ChoiceChip( + label: Text(AppConstants.weekTitles[index]), + showCheckmark: false, + selected: _selectedDays[index], + onSelected: (bool selected) { + setState(() { + _selectedDays[index] = selected; + if (!_selectedDays.contains(true)) { + _selectedDays[_startDate.weekday - 1] = true; + } + }); + }, + shape: CircleBorder(), + ); + }).toList(), + ), + ], + SizedBox(height: 15), + if (_selectedFrequency != RepeatFrequency.doNotRepeat) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Reoccurrence ends on: ", + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.w500, + fontSize: 17, + ), + ), + Row( + children: [ + Radio( + value: RecurrenceEnd.never, + groupValue: _selectedRecurrenceEnd, + onChanged: (value) => setState( + () => _selectedRecurrenceEnd = value, + ), + ), + Text( + "Never", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ), + ], + ), + Row( + children: [ + Radio( + value: RecurrenceEnd.on, + groupValue: _selectedRecurrenceEnd, + onChanged: (value) => setState( + () => _selectedRecurrenceEnd = value, + ), + ), + Text( + "On", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ) + ], + ), + Row( + children: [ + Radio( + value: RecurrenceEnd.after, + groupValue: _selectedRecurrenceEnd, + onChanged: (value) => setState( + () => _selectedRecurrenceEnd = value, + ), + ), + Text( + "After", + style: TextStyle( + color: AppColors.black, + fontSize: 17, + ), + ), + ], + ), + ], + ), + SizedBox(height: 15), + if (_selectedRecurrenceEnd == RecurrenceEnd.on) + DateTimeSelectorFormField( + initialDateTime: _recurrenceEndDate, + decoration: AppConstants.inputDecoration.copyWith( + labelText: "Ends on", + ), + onSelect: (date) { + if (date.withoutTime.isBefore(_endDate.withoutTime)) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Recurrence ends after end date'), + )); + } else { + _recurrenceEndDate = date.withoutTime; + } + + if (mounted) { + setState(() {}); + } + }, + validator: (value) { + if (value == null || value == "") { + return "Please select end date."; + } + + // TODO(Shubham): Add validation of end occurrence >= end date + return null; + }, + textStyle: TextStyle( + color: AppColors.black, + fontSize: 17.0, + ), + onSave: (date) => _recurrenceEndDate = date ?? _recurrenceEndDate, + type: DateTimeSelectionType.date, + ), + if (_selectedRecurrenceEnd == RecurrenceEnd.after) + TextFormField( + controller: _occurrenceController, + style: TextStyle( + color: AppColors.black, + fontSize: 17.0, + ), + keyboardType: TextInputType.number, + minLines: 1, + maxLength: 3, + validator: (value) { + if (value == null || value.trim() == '') { + return "Please specify occurrences"; + } + + return null; + }, + decoration: AppConstants.inputDecoration.copyWith( + hintText: "30", + suffixText: 'Occurrences', + counterText: '', + ), + ), SizedBox( height: 15.0, ), @@ -285,6 +534,15 @@ class _AddOrEditEventFormState extends State { _form.currentState?.save(); + final recurrenceSettings = RecurrenceSettings.withCalculatedEndDate( + startDate: _startDate, + endDate: _recurrenceEndDate, + frequency: _selectedFrequency ?? RepeatFrequency.daily, + weekdays: _selectedIndexes, + interval: int.tryParse(_occurrenceController.text), + recurrenceEndOn: _selectedRecurrenceEnd ?? RecurrenceEnd.never, + ); + final event = CalendarEventData( date: _startDate, endTime: _endTime, @@ -293,12 +551,42 @@ class _AddOrEditEventFormState extends State { color: _color, title: _titleController.text.trim(), description: _descriptionController.text.trim(), + recurrenceSettings: recurrenceSettings, ); widget.onEventAdd?.call(event); _resetForm(); } + /// Get list of weekdays in indices from the selected days + List get _selectedIndexes { + List selectedIndexes = []; + for (int i = 0; i < _selectedDays.length; i++) { + if (_selectedDays[i] == true) { + selectedIndexes.add(i); + } + } + return selectedIndexes; + } + + void updateWeekdaysSelection() { + _selectedDays.fillRange(0, _selectedDays.length, false); + if (_endDate.difference(_startDate).inDays >= 7) { + _selectedDays.fillRange(0, _selectedDays.length, true); + } + DateTime current = _startDate; + while (current.isBefore(_endDate) || current.isAtSameMomentAs(_endDate)) { + _selectedDays[current.weekday - 1] = true; + current = current.add(Duration(days: 1)); + } + } + + /// Set initial selected week to start date + void _setInitialWeekday() { + final currentWeekday = DateTime.now().weekday - 1; + _selectedDays[currentWeekday] = true; + } + void _setDefaults() { if (widget.event == null) return; @@ -310,12 +598,15 @@ class _AddOrEditEventFormState extends State { _endTime = event.endTime ?? _endTime; _titleController.text = event.title; _descriptionController.text = event.description ?? ''; + _selectedFrequency = event.recurrenceSettings?.frequency; + // TODO(Shubham): Update recurrence end date } void _resetForm() { _form.currentState?.reset(); _startDate = DateTime.now().withoutTime; _endDate = DateTime.now().withoutTime; + // TODO(Shubham): Update recurrence end date _startTime = null; _endTime = null; _color = Colors.blue; diff --git a/example/lib/widgets/day_view_widget.dart b/example/lib/widgets/day_view_widget.dart index bc8cf3f8..51720e29 100644 --- a/example/lib/widgets/day_view_widget.dart +++ b/example/lib/widgets/day_view_widget.dart @@ -38,6 +38,8 @@ class DayViewWidget extends StatelessWidget { MaterialPageRoute( builder: (_) => DetailsPage( event: events.first, + date: + date, // TODO(Shubham): Update if not required for delete following event ), ), ); diff --git a/example/lib/widgets/month_view_widget.dart b/example/lib/widgets/month_view_widget.dart index bacec7eb..bdaf73f8 100644 --- a/example/lib/widgets/month_view_widget.dart +++ b/example/lib/widgets/month_view_widget.dart @@ -24,6 +24,7 @@ class MonthViewWidget extends StatelessWidget { MaterialPageRoute( builder: (_) => DetailsPage( event: event, + date: date, ), ), ); diff --git a/example/lib/widgets/week_view_widget.dart b/example/lib/widgets/week_view_widget.dart index 765e8fcb..1fadaa82 100644 --- a/example/lib/widgets/week_view_widget.dart +++ b/example/lib/widgets/week_view_widget.dart @@ -33,6 +33,8 @@ class WeekViewWidget extends StatelessWidget { MaterialPageRoute( builder: (_) => DetailsPage( event: events.first, + date: date, + // TODO(Shubham): Update if not required for delete following event ), ), ); diff --git a/lib/src/calendar_event_data.dart b/lib/src/calendar_event_data.dart index e6b053fd..9eeb0b7c 100644 --- a/lib/src/calendar_event_data.dart +++ b/lib/src/calendar_event_data.dart @@ -44,6 +44,9 @@ class CalendarEventData { /// Define style of description. final TextStyle? descriptionStyle; + /// Define reoccurrence settings + final RecurrenceSettings? recurrenceSettings; + /// {@macro calendar_event_data_doc} CalendarEventData({ required this.title, @@ -55,6 +58,7 @@ class CalendarEventData { this.endTime, this.titleStyle, this.descriptionStyle, + this.recurrenceSettings, DateTime? endDate, }) : _endDate = endDate?.withoutTime, date = date.withoutTime; @@ -99,6 +103,12 @@ class CalendarEventData { } } + /// Checks if the given date is in the list of excluded dates. + /// Returns true if the date is excluded, otherwise false. + bool isExcluded(DateTime date) { + return recurrenceSettings?.excludeDates?.contains(date) ?? false; + } + /// Returns a boolean that defines whether current event is occurring on /// [currentDate] or not. /// @@ -119,11 +129,13 @@ class CalendarEventData { "title": title, "description": description, "endDate": endDate, + "recurrenceSettings": recurrenceSettings, }; /// Returns new object of [CalendarEventData] with the updated values defined /// as the arguments. /// + // TODO(Shubham): Add all other remaining fields CalendarEventData copyWith({ String? title, String? description, @@ -135,6 +147,7 @@ class CalendarEventData { TextStyle? descriptionStyle, DateTime? endDate, DateTime? date, + RecurrenceSettings? recurrenceSettings, }) { return CalendarEventData( title: title ?? this.title, @@ -147,6 +160,7 @@ class CalendarEventData { endDate: endDate ?? this.endDate, event: event ?? this.event, titleStyle: titleStyle ?? this.titleStyle, + recurrenceSettings: recurrenceSettings ?? this.recurrenceSettings, ); } diff --git a/lib/src/enumerations.dart b/lib/src/enumerations.dart index 5659e4e9..105b84d2 100644 --- a/lib/src/enumerations.dart +++ b/lib/src/enumerations.dart @@ -48,3 +48,29 @@ enum LineStyle { /// Dashed line dashed, } + +/// Defines reoccurrence of event: Daily, weekly, monthly or yearly +enum RepeatFrequency { + doNotRepeat, + daily, + weekly, + monthly, + yearly, +} + +/// Defines reoccurrence event ends on: +/// `never` to repeat without any end date specified, +/// `on` to repeat till date specified +/// `after` repeat till defined number of occurrence. +enum RecurrenceEnd { + never, + on, + after, +} + +// TODO(Shubham): Add docs +enum DeleteEvent { + all, + current, + following, +} diff --git a/lib/src/event_controller.dart b/lib/src/event_controller.dart index 1a131a95..20169d47 100644 --- a/lib/src/event_controller.dart +++ b/lib/src/event_controller.dart @@ -6,9 +6,7 @@ import 'dart:collection'; import 'package:flutter/material.dart'; -import 'calendar_event_data.dart'; -import 'extensions.dart'; -import 'typedefs.dart'; +import '../calendar_view.dart'; class EventController extends ChangeNotifier { /// Calendar controller to control all the events related operations like, @@ -73,7 +71,224 @@ class EventController extends ChangeNotifier { //#endregion + //#region Private Methods + // isAtSameMomentAs is used to include end date + // Ex. If event end date is 15-11-24 then event will be added on 15-11-24 as well. + bool _isDailyRecurrence({ + required DateTime currentDate, + required DateTime eventEnd, + required RecurrenceSettings recurrenceSettings, + }) { + final recurrenceEndDate = recurrenceSettings.endDate; + final interval = recurrenceSettings.interval; + + switch (recurrenceSettings.recurrenceEndOn) { + case RecurrenceEnd.never: + if (recurrenceEndDate == null) { + return true; + } else { + // TODO(Shubham): Is same moment may not required + return currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate); + } + case RecurrenceEnd.on: + return recurrenceEndDate != null && + (currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate)); + case RecurrenceEnd.after: + if (interval == null) { + return false; + } + return recurrenceEndDate != null && + ((currentDate.isBefore(recurrenceEndDate)) || + currentDate.isAtSameMomentAs(recurrenceEndDate)); + } + } + + bool _isWeeklyRecurrence({ + required DateTime currentDate, + required DateTime eventStartDate, + required RecurrenceSettings recurrenceSettings, + }) { + final recurrenceEndDate = recurrenceSettings.endDate; + // Check if the date’s weekday is in the recurrence weekdays + final isMatchingWeekday = + recurrenceSettings.weekdays.contains(currentDate.weekday - 1); + if (!isMatchingWeekday) { + return false; + } + + switch (recurrenceSettings.recurrenceEndOn) { + // TODO(Shubham): Review redundant code + case RecurrenceEnd.never: + if (recurrenceEndDate == null) { + return true; + } else { + // TODO(Shubham): Is same moment may not required + return currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate); + } + case RecurrenceEnd.on: + final recurrenceEndDate = recurrenceSettings.endDate; + return recurrenceEndDate != null && + (currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate)); + + case RecurrenceEnd.after: + return recurrenceEndDate != null && + (currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate)); + } + } + + bool _isMonthlyRecurrence({ + required DateTime currentDate, + required DateTime startDate, + required RecurrenceSettings recurrenceSettings, + }) { + final recurrenceEndDate = recurrenceSettings.endDate; + final interval = recurrenceSettings.interval; + + // Exclude past events + if (currentDate.isBefore(startDate)) { + return false; + } + + if (currentDate.day != startDate.day) { + return false; + } + + switch (recurrenceSettings.recurrenceEndOn) { + case RecurrenceEnd.never: + if (currentDate.day == startDate.day && recurrenceEndDate == null) { + return true; + } else { + return recurrenceEndDate != null && + currentDate.isBefore(recurrenceEndDate); + } + case RecurrenceEnd.on: + return recurrenceEndDate != null && + (currentDate.isBefore(recurrenceEndDate) || + currentDate.isAtSameMomentAs(recurrenceEndDate)); + // Calculate the last valid occurrence based on the interval count + // `interval - 1` is used here because: + // - The `interval` represents the total number of occurrences, including the starting month. + // - Subtracting 1 allows us to calculate the number of additional months to add + // after the start month to get to the final occurrence. + // For example, if `interval = 3`, occurrences should happen in: + // - Start month (1st occurrence), plus two additional months to reach the 3rd occurrence. + case RecurrenceEnd.after: + if (interval == null || interval <= 1) { + return false; + } + if (recurrenceEndDate == null) { + return false; + } + return ((currentDate.isBefore(recurrenceEndDate) || + (currentDate.isAtSameMomentAs(recurrenceEndDate))) && + (currentDate.day == startDate.day)); + } + } + + bool _handleRecurrence({ + required DateTime currentDate, + required DateTime eventStartDate, + required DateTime eventEndDate, + required RecurrenceSettings recurrenceSettings, + }) { + if (recurrenceSettings.excludeDates?.contains(currentDate) ?? false) { + return false; + } + switch (recurrenceSettings.frequency) { + case RepeatFrequency.daily: + return _isDailyRecurrence( + currentDate: currentDate, + eventEnd: eventEndDate, + recurrenceSettings: recurrenceSettings, + ); + case RepeatFrequency.weekly: + return _isWeeklyRecurrence( + currentDate: currentDate, + eventStartDate: eventStartDate, + recurrenceSettings: recurrenceSettings, + ); + case RepeatFrequency.monthly: + return _isMonthlyRecurrence( + currentDate: currentDate, + startDate: eventStartDate, + recurrenceSettings: recurrenceSettings, + ); + case RepeatFrequency.yearly: + // TODO(Shubham): Handle this case. + break; + case RepeatFrequency.doNotRepeat: + break; + } + return false; + } + + void _deleteCurrentEvent(DateTime date, CalendarEventData event) { + List excludeDates = event.recurrenceSettings?.excludeDates ?? []; + excludeDates.add(date); + final updatedRecurrenceSettings = + event.recurrenceSettings?.copyWith(excludeDates: excludeDates); + final updatedEvent = + event.copyWith(recurrenceSettings: updatedRecurrenceSettings); + update(event, updatedEvent); + } + + /// If the selected date to delete the event is the same as the event's start date, delete all recurrences. + /// Otherwise, delete the event on the selected date and all subsequent recurrences. + void _deleteFollowingEvents(DateTime date, CalendarEventData event) { + final newEndDate = date.subtract( + Duration(days: 1), + ); + final updatedRecurrenceSettings = event.recurrenceSettings?.copyWith( + endDate: newEndDate, + ); + if (date == event.date) { + remove(event); + } else { + final updatedEvent = + event.copyWith(recurrenceSettings: updatedRecurrenceSettings); + update(event, updatedEvent); + } + } + //#endregion + //#region Public Methods + /// Deletes a recurring event based on the specified deletion type. + /// + /// This method handles the deletion of recurring events by determining the type of deletion + /// requested (all events, the current event, or following events) and performing the appropriate action. + /// + /// Takes the following parameters: + /// - [date]: The date of the event to be deleted. + /// - [event]: The event data to be deleted. + /// - [deleteEventType]: The `DeleteEventType` of deletion to perform (all events, the current event, or following events). + /// + /// The method performs the following actions based on the [deleteEventType]: + /// - [DeleteEvent.all]: Removes the entire series of events. + /// - [DeleteEvent.current]: Deletes only the current event. + /// - [DeleteEvent.following]: Deletes the current event and all subsequent events. + void deleteRecurrenceEvent({ + required DateTime date, + required CalendarEventData event, + required DeleteEvent deleteEventType, + }) { + switch (deleteEventType) { + case DeleteEvent.all: + remove(event); + break; + case DeleteEvent.current: + _deleteCurrentEvent(date, event); + break; + case DeleteEvent.following: + _deleteFollowingEvents(date, event); + break; + } + } + /// Add all the events in the list /// If there is an event with same date then void addAll(List> events) { @@ -137,11 +352,61 @@ class EventController extends ChangeNotifier { {bool includeFullDayEvents = true}) { //ignore: deprecated_member_use_from_same_package if (_eventFilter != null) return _eventFilter!.call(date, this.events); - return _calendarData.getEventsOnDay(date.withoutTime, includeFullDayEvents: includeFullDayEvents); } + /// Retrieves all events for a given date, including repeated events that are not excluded on that day. + /// + /// This method combines events that occur on the specified date with repeated events that are not excluded. + /// It filters out any events that are marked as excluded for the given date. + /// + /// Takes a [date] parameter representing the date for which to retrieve events. + /// Returns a list of [CalendarEventData] objects representing all events on the specified date. + List> getAllEventsOnDay(DateTime date) { + final events = + getEventsOnDay(date).where((event) => !event.isExcluded(date)).toList(); + final repeatedEvents = + getRepeatedEvents(date).where((event) => !event.isExcluded(date)); + events.addAll(repeatedEvents); + + return events; + } + + /// Filters list of repeated events to show in the cell for given date + /// from all the repeated events. + /// Event reoccurrence will only show after today's date and event's day. + List> getRepeatedEvents(DateTime date) { + // Past event date may not support + if (!date.isAfter(DateTime.now())) { + return []; + } + + final repeatedEvents = _calendarData.repeatedEvents; + List> events = []; + + for (final event in repeatedEvents) { + if (!date.isAfter(event.date)) { + continue; + } + final recurrenceSettings = event.recurrenceSettings; + // if event is not repeating or date is in excluded + if (recurrenceSettings == null) { + continue; + } + final isRecurrence = _handleRecurrence( + currentDate: date, + eventStartDate: event.date, + eventEndDate: event.endDate, + recurrenceSettings: recurrenceSettings, + ); + if (isRecurrence) { + events.add(event); + } + } + return events; + } + /// Returns full day events on given day. List> getFullDayEvent(DateTime date) { return _calendarData.getFullDayEvent(date.withoutTime); @@ -179,6 +444,10 @@ class CalendarData { /// available in this list as global itemList of all events). final _eventList = >[]; + /// If recurrence settings exist then get all the repeated events + List> get repeatedEvents => + _eventList.where((event) => event.recurrenceSettings != null).toList(); + UnmodifiableListView> get events => UnmodifiableListView(_eventList); @@ -249,7 +518,6 @@ class CalendarData { // TODO: improve this... if (_eventList.contains(event)) return; - if (event.isFullDayEvent) { addFullDayEvent(event); } else if (event.isRangingEvent) { @@ -329,7 +597,6 @@ class CalendarData { if (includeFullDayEvents) { events.addAll(getFullDayEvent(date)); } - return events; } diff --git a/lib/src/modals.dart b/lib/src/modals.dart index 5fc80acb..78d36f1e 100644 --- a/lib/src/modals.dart +++ b/lib/src/modals.dart @@ -83,3 +83,117 @@ class LiveTimeIndicatorSettings { showBullet: false, ); } + +/// Set `frequency = RepeatFrequency.daily` to repeat every day after current date & event day. +/// Set `frequency = RepeatFrequency.weekly` & provide list of weekdays to repeat on. +/// [startDate]: Defines start date of repeating events. +/// [endDate]: Defines end date of repeating events. +/// [interval]: Defines repetition of event after given [interval] in days. +/// [frequency]: Defines repeat daily, weekly, monthly or yearly. +/// [weekdays]: Contains list of weekdays to repeat on. +/// By default weekday of event is considered if not provided. +class RecurrenceSettings { + final DateTime startDate; + late DateTime? endDate; // TODO(Shubham): Review + final int? interval; + final RepeatFrequency frequency; + final RecurrenceEnd recurrenceEndOn; + final List weekdays; + final List? excludeDates; + + RecurrenceSettings({ + required this.startDate, + this.endDate, + this.interval, + this.frequency = RepeatFrequency.weekly, + this.recurrenceEndOn = RecurrenceEnd.never, + this.excludeDates, + List? weekdays, + }) : weekdays = weekdays ?? [startDate.weekday]; + + // TODO(Shubham): Add asserts + // Set by calculating end date for daily + // Ex. If current date is 11-11-24 and interval is 5 then new end date will be + // 15-11-24. Interval - 1 is added because event has been already occurred once on start date. + + // Set by calculating end date for weekly + // Ex. If current date is 1-11-24 and interval is 5 then new end date will be 29-11-24. + RecurrenceSettings.withCalculatedEndDate({ + required this.startDate, + required DateTime endDate, + this.interval, + this.frequency = RepeatFrequency.weekly, + this.recurrenceEndOn = RecurrenceEnd.never, + this.excludeDates, + List? weekdays, + }) : weekdays = weekdays ?? [startDate.weekday] { + if (recurrenceEndOn == RecurrenceEnd.after && interval != null) { + switch (frequency) { + case RepeatFrequency.doNotRepeat: + // TODO: Handle this case. + break; + case RepeatFrequency.daily: + final updatedEndDate = endDate.add(Duration(days: interval! - 1)); + this.endDate = updatedEndDate; + break; + case RepeatFrequency.weekly: + // TODO(Shubham): Review default interval + final occurrences = interval ?? 1; + final updatedEndDate = + startDate.add(Duration(days: (occurrences - 1) * 7)); + this.endDate = updatedEndDate; + break; + case RepeatFrequency.monthly: + final occurrences = interval ?? 1; + final updatedEndDate = DateTime( + startDate.year, + startDate.month + (occurrences - 1), + startDate.day, + ); + debugPrint('Start date: $startDate'); + debugPrint('Updated end date: $updatedEndDate'); + this.endDate = updatedEndDate; + break; + case RepeatFrequency.yearly: + // TODO: Handle this case. + break; + } + } else if (recurrenceEndOn == RecurrenceEnd.never) { + this.endDate = null; + } else { + this.endDate = endDate; + } + debugPrint('End date: ${endDate}'); + } + + @override + String toString() { + return "start date: ${startDate}, " + "end date: ${endDate}, " + "interval: ${interval}, " + "frequency: ${frequency} " + "weekdays: ${weekdays.toString()}" + "recurrence Ends on: ${recurrenceEndOn}" + "exclude dates: ${excludeDates}"; + } + + RecurrenceSettings copyWith({ + DateTime? startDate, + DateTime? endDate, + int? interval, + RepeatFrequency? frequency, + RecurrenceEnd? recurrenceEndOn, + List? weekdays, + List? excludeDates, + }) { + return RecurrenceSettings( + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + interval: interval ?? this.interval, + frequency: frequency ?? this.frequency, + recurrenceEndOn: recurrenceEndOn ?? this.recurrenceEndOn, + weekdays: weekdays ?? this.weekdays, + excludeDates: excludeDates ?? this.excludeDates, + ); + } +} diff --git a/lib/src/month_view/month_view.dart b/lib/src/month_view/month_view.dart index 51690910..1ead1029 100644 --- a/lib/src/month_view/month_view.dart +++ b/lib/src/month_view/month_view.dart @@ -698,7 +698,8 @@ class _MonthPageBuilder extends StatelessWidget { itemCount: 42, shrinkWrap: true, itemBuilder: (context, index) { - final events = controller.getEventsOnDay(monthDays[index]); + final events = controller.getAllEventsOnDay(monthDays[index]); + return GestureDetector( onTap: () => onCellTap?.call(events, monthDays[index]), onLongPress: () => onDateLongPress?.call(monthDays[index]),