how to override getOccurrenceAppointment to use it with custom DataSource

Hello! I have created my Custom Appointment Object and I override all the methods but I am having issues when creating a recurrent event, RangeError (index): Invalid value: Valid value range is empty: -1

is working good when I create a normal event, the issue is when I try to create a recurrent event

it looks like is not mapping the properties of FitnessClass with the CalendarDataSource override methods

calendarData.appointments is empty.


Im not sure what I'm missing.


this is my custom Object:


class FitnessClassDataSource extends CalendarDataSource<FitnessClass> {
FitnessClassDataSource(this.source);
List<FitnessClass> source;

@override
List<dynamic> get appointments => source;

@override
Object? getId(int index) {
return source[index].id;
}

@override
DateTime getStartTime(int index) {
return source[index].startTime;
}

@override
DateTime getEndTime(int index) {
return source[index].endTime;
}

@override
String? getStartTimeZone(int index) {
return source[index].startTimeZone;
}

@override
String? getEndTimeZone(int index) {
return source[index].endTimeZone;
}

@override
bool isAllDay(int index) {
return source[index].isAllDay;
}

@override
Color getColor(int index) {
return source[index].color;
}

@override
String? getNotes(int index) {
return source[index].notes;
}

@override
String getSubject(int index) {
return source[index].fitnessClassTitle;
}

@override
Object? getRecurrenceId(int index) {
return source[index].recurrenceId;
}

@override
String? getRecurrenceRule(int index) {
return source[index].recurrenceRule;
}

@override
List<DateTime>? getRecurrenceExceptionDates(int index) {
return source[index].recurrenceExceptionDates;
}
}

class FitnessClass {
FitnessClass({
required this.startTime,
required this.endTime,
this.isAllDay = false,
required this.fitnessClassTitle,
this.color = Colors.blue,
this.startTimeZone,
this.endTimeZone,
this.recurrenceRule,
this.recurrenceExceptionDates,
this.notes = "",
this.recurrenceId,
this.id,
required this.maxAttendees,
}) {
recurrenceRule = recurrenceId != null ? null : recurrenceRule;
_appointmentType = _getAppointmentType();
id = id ?? hashCode;
}

DateTime startTime;
DateTime endTime;
bool isAllDay;
String fitnessClassTitle;
Color color;
String? startTimeZone;
String? endTimeZone;
String? recurrenceRule;
List<DateTime>? recurrenceExceptionDates;
String? notes;
Object? recurrenceId;
Object? id;
String maxAttendees;
AppointmentType _appointmentType = AppointmentType.normal;

FitnessClass copyWith({
DateTime? startTime,
DateTime? endTime,
bool? isAllDay,
String? fitnessClassTitle,
Color? color,
String? startTimeZone,
String? endTimeZone,
String? recurrenceRule,
List<DateTime>? recurrenceExceptionDates,
String? notes,
Object? recurrenceId,
Object? id,
String? maxAttendees,
}) =>
FitnessClass(
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
isAllDay: isAllDay ?? this.isAllDay,
fitnessClassTitle: fitnessClassTitle ?? this.fitnessClassTitle,
color: color ?? this.color,
startTimeZone: startTimeZone ?? this.startTimeZone,
endTimeZone: endTimeZone ?? this.endTimeZone,
recurrenceRule: recurrenceRule ?? this.recurrenceRule,
recurrenceExceptionDates:
recurrenceExceptionDates ?? this.recurrenceExceptionDates,
notes: notes ?? this.notes,
recurrenceId: recurrenceId ?? this.recurrenceId,
id: id ?? this.id,
maxAttendees: maxAttendees ?? this.maxAttendees,
);

AppointmentType get appointmentType => _appointmentType;

/// Here we used isOccurrenceAppointment keyword to identify the
/// occurrence appointment When we clone the pattern appointment for
/// occurrence appointment we have append the string in the notes and here we
/// identify based on the string and removed the appended string.
AppointmentType _getAppointmentType() {
if (recurrenceId != null) {
return AppointmentType.changedOccurrence;
} else if (recurrenceRule != null && recurrenceRule!.isNotEmpty) {
if (notes != null && notes!.contains('isOccurrenceAppointment')) {
notes = notes!.replaceAll('isOccurrenceAppointment', '');
return AppointmentType.occurrence;
}

return AppointmentType.pattern;
}
return AppointmentType.normal;
}

factory FitnessClass.fromJson(String str) =>
FitnessClass.fromMap(json.decode(str));

String toJson() => json.encode(toMap());

factory FitnessClass.fromMap(Map<String, dynamic> json) => FitnessClass(
startTime: DateTime.parse(json["startTime"]),
endTime: DateTime.parse(json["endTime"]),
isAllDay: json["isAllDay"],
fitnessClassTitle: json["fitnessClassTitle"],
color: json["color"],
startTimeZone: json["startTimeZone"],
endTimeZone: json["endTimeZone"],
recurrenceRule: json["recurrenceRule"],
recurrenceExceptionDates: List<DateTime>.from(
json["recurrenceExceptionDates"].map((x) => DateTime.parse(x))),
notes: json["notes"],
recurrenceId: json["recurrenceId"],
id: json["id"],
maxAttendees: json["maxAttendees"] = "15",
);

Map<String, dynamic> toMap() => {
"startTime": startTime.toIso8601String(),
"endTime": endTime.toIso8601String(),
"isAllDay": isAllDay,
"fitnessClassTitle": fitnessClassTitle,
"color": color,
"startTimeZone": startTimeZone,
"endTimeZone": endTimeZone,
"recurrenceRule": recurrenceRule,
"recurrenceExceptionDates": List<dynamic>.from(
recurrenceExceptionDates?.map((x) => x.toIso8601String()) ?? []),
"notes": notes,
"recurrenceId": recurrenceId,
"id": id,
"maxAttendees": maxAttendees,
};

@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode {
return hashValues(
startTimeZone,
endTimeZone,
recurrenceRule,
isAllDay,
notes,
recurrenceId,
id,
appointmentType,
startTime,
endTime,
fitnessClassTitle,
maxAttendees,
color,
hashList(recurrenceExceptionDates),
);
}
}

7 Replies

MS Muniappan Subramanian Syncfusion Team September 5, 2022 12:55 PM UTC

Hi Jean,


# Regarding how to override getOccurrenceAppointment to use it with custom DataSource

Currently, we are analyzing the mentioned issue. We will validate and update you further on or before (September 7th, 2022). We appreciate your patience until then.


Regards,
Muniappan S.



MS Muniappan Subramanian Syncfusion Team September 7, 2022 11:53 AM UTC

Hi Jean, 


#Regarding I am having issues when creating a recurrent event

As per the given code snippets we have followed to check the reported issue and we are unable to replicate the issue from our end. We have prepared the simple sample, please find the attached sample for the same, 


Please find the output for the same, 

Recurring.png



Additional information

SfCalendar version: 20.2.46 


Please check our sample and let us know if you still facing the same issue. if not, please modify the sample based on your scenario and revert us back with more details. It will be helpful for us to check on it and provide you with the solution at the earliest.


We hope that this helps you. Please let us know if you need further assistance. 


Regards, 

Muniappan S 


Attachment: occurrence_appointments_f14755c8.zip


CS Cody Stepp February 2, 2024 12:23 AM UTC

I'm having a similar issue in my application and tracked it down to the "isOccurrenceAppointment" getting added and stripped out from the appointment.dart

AppointmentType _getAppointmentType() {
if (recurrenceId != null) {
return AppointmentType.changedOccurrence;
} else if (recurrenceRule != null && recurrenceRule!.isNotEmpty) {
if (notes != null && notes!.contains('isOccurrenceAppointment')) {
notes = notes!.replaceAll('isOccurrenceAppointment', '');
return AppointmentType.occurrence;
}

return AppointmentType.pattern;
}
return AppointmentType.normal;
}


so when it hits hits the getAppointmentType in my CustomAppointment Class the appointment doesn't have that flag to indicate that it is an "occurrence" instead of a "pattern" so it returns a -1 as it tries to find the pattern parent

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';

class CustomDataSource extends CalendarDataSource<CustomAppointment> {
CustomDataSource(this.source, this.resourceColl);
List<CustomAppointment> source;
List<CalendarResource> resourceColl;

@override
List<dynamic>? get appointments => source;

@override
List<CalendarResource>? get resources => resourceColl;

@override
Object? getId(int index) {
return source[index].id;
}

@override
DateTime getStartTime(int index) {
return source[index].startTime;
}

@override
DateTime getEndTime(int index) {
return source[index].endTime;
}

@override
String getSubject(int index) {
return source[index].subject;
}

@override
Color getColor(int index) {
return source[index].color;
}

@override
bool isAllDay(int index) {
return source[index].isAllDay;
}

@override
String? getStartTimeZone(int index) {
return source[index].startTimeZone;
}

@override
String? getEndTimeZone(int index) {
return source[index].endTimeZone;
}

@override
String? getRecurrenceRule(int index) {
return source[index].recurrenceRule;
}

@override
List<DateTime>? getRecurrenceExceptionDates(int index) {
return source[index].recurrenceExceptionDates;
}

@override
String? getNotes(int index) {
return source[index].notes;
}

@override
String? getLocation(int index) {
return source[index].location;
}

@override
List<Object>? getResourceIds(int index) {
return source[index].resourceIds;
}

@override
Object? getRecurrenceId(int index) {
return source[index].recurrenceId;
}

@override
CustomAppointment convertAppointmentToObject(
CustomAppointment customData, Appointment appointment) {
return CustomAppointment(
// Map the properties from Appointment to CustomAppointment
startTimeZone: appointment.startTimeZone,
endTimeZone: appointment.endTimeZone,
recurrenceRule: appointment.recurrenceRule,
isAllDay: appointment.isAllDay,
notes: appointment.notes,
location: appointment.location,
resourceIds: appointment.resourceIds,
recurrenceId: appointment.recurrenceId,
id: appointment.id,
startTime: appointment.startTime,
endTime: appointment.endTime,
subject: appointment.subject,
color: appointment.color,
recurrenceExceptionDates: appointment.recurrenceExceptionDates,

// Include additional properties specific to CustomAppointment
completeDate: customData.completeDate,
);
}

void updateAppointment(
CustomAppointment oldAppointment, CustomAppointment newAppointment) {
int index = appointments!.indexOf(oldAppointment);
if (index != -1) {
appointments?[index] = newAppointment;
notifyListeners(
CalendarDataSourceAction.reset, <CustomAppointment>[newAppointment]);
}
}

// Add methods to get custom properties if needed
}

class CustomAppointment {
CustomAppointment({
required this.startTime,
required this.endTime,
this.isAllDay = false,
required this.subject,
this.color = Colors.blue,
this.startTimeZone,
this.endTimeZone,
this.recurrenceRule,
this.recurrenceExceptionDates,
this.notes = "",
this.recurrenceId,
this.id,
this.location,
this.resourceIds,

// Your custom properties
this.completeDate,

}) {
recurrenceRule = recurrenceId != null ? null : recurrenceRule;
_appointmentType = _getAppointmentType();
id = id ?? hashCode;
}

DateTime startTime;
DateTime endTime;
bool isAllDay;
String subject;
Color color;
String? startTimeZone;
String? endTimeZone;
String? recurrenceRule;
List<DateTime>? recurrenceExceptionDates;
String? notes;
String? location;
List<Object>? resourceIds;
Object? recurrenceId;
Object? id;
AppointmentType _appointmentType = AppointmentType.normal;

// Custom fields
DateTime? completeDate;


CustomAppointment copyWith({
DateTime? startTime,
DateTime? endTime,
bool? isAllDay,
String? subject,
Color? color,
String? startTimeZone,
String? endTimeZone,
String? recurrenceRule,
List<DateTime>? recurrenceExceptionDates,
String? notes,
String? location,
List<Object>? resourceIds,
Object? recurrenceId,
Object? id,
}) =>
CustomAppointment(
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
isAllDay: isAllDay ?? this.isAllDay,
subject: subject ?? this.subject,
color: color ?? this.color,
startTimeZone: startTimeZone ?? this.startTimeZone,
endTimeZone: endTimeZone ?? this.endTimeZone,
recurrenceRule: recurrenceRule ?? this.recurrenceRule,
recurrenceExceptionDates:
recurrenceExceptionDates ?? this.recurrenceExceptionDates,
notes: notes ?? this.notes,
location: location ?? this.location,
resourceIds: resourceIds ?? this.resourceIds,
recurrenceId: recurrenceId ?? this.recurrenceId,
id: id ?? this.id,
);

AppointmentType get appointmentType => _appointmentType;

/// Here we used isOccurrenceAppointment keyword to identify the
/// occurrence appointment When we clone the pattern appointment for
/// occurrence appointment we have append the string in the notes and here we
/// identify based on the string and removed the appended string.
AppointmentType _getAppointmentType() {
if (recurrenceId != null) {
return AppointmentType.changedOccurrence;
} else if (recurrenceRule != null && recurrenceRule!.isNotEmpty) {
if (notes != null && notes!.contains('isOccurrenceAppointment')) {
notes = notes!.replaceAll('isOccurrenceAppointment', '');
return AppointmentType.occurrence;
}

return AppointmentType.pattern;
}
return AppointmentType.normal;
}

factory CustomAppointment.fromJson(String str) =>
CustomAppointment.fromMap(json.decode(str));

String toJson() => json.encode(toMap());

factory CustomAppointment.fromMap(Map<String, dynamic> json) =>
CustomAppointment(
startTime: DateTime.parse(json["startTime"]),
endTime: DateTime.parse(json["endTime"]),
isAllDay: json["isAllDay"],
subject: json["subject"],
color: json["color"],
startTimeZone: json["startTimeZone"],
endTimeZone: json["endTimeZone"],
recurrenceRule: json["recurrenceRule"],
recurrenceExceptionDates: List<DateTime>.from(
json["recurrenceExceptionDates"].map((x) => DateTime.parse(x))),
notes: json["notes"],
location: json["location"],
resourceIds: json["resourceIds"] != null
? List<String>.from(json["resourceIds"].map((x) => x.toString()))
: null,
recurrenceId: json["recurrenceId"],
id: json["id"],
);

Map<String, dynamic> toMap() => {
"startTime": startTime.toIso8601String(),
"endTime": endTime.toIso8601String(),
"isAllDay": isAllDay,
"subject": subject,
"color": color,
"startTimeZone": startTimeZone,
"endTimeZone": endTimeZone,
"recurrenceRule": recurrenceRule,
"recurrenceExceptionDates": List<dynamic>.from(
recurrenceExceptionDates?.map((x) => x.toIso8601String()) ?? []),
"notes": notes,
"location": location,
"resourceIds": resourceIds,
"recurrenceId": recurrenceId,
"id": id,
};

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CustomAppointment &&
other.id == id &&
other.startTime == startTime &&
other.endTime == endTime &&
// Include other properties if they are part of your equality logic
other.subject == subject;
}

@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode {
return Object.hash(
startTimeZone,
endTimeZone,
recurrenceRule,
isAllDay,
notes,
location,
resourceIds,
recurrenceId,
id,
appointmentType,
startTime,
endTime,
subject,
color,
Object.hashAll(recurrenceExceptionDates ?? []),
);
}
}



YG Yuvaraj Gajaraj Syncfusion Team February 2, 2024 09:39 AM UTC

Hi Cody,


We can replicate the reported issue regarding getting the isOccurrenceAppointment value in the appointment's notes field when tapping the drag-and-drop appointment at our end and have logged a bug report for it in our feedback portal. We will fix and include the changes in our upcoming weekly patch release which is expected to be rolled out on 2nd Week of February. We will update you here once the release is rolled out and we appreciate your patience until then. You can also track the status of the bug with the feedback below.


Feedback, https://www.syncfusion.com/feedback/50507/


Regards,

Yuvaraj



CS Cody Stepp February 2, 2024 03:23 PM UTC

I failed to include that the -1 index happens when I try to edit or delete the recurring appointment - I'm using the  (appointment_editor.dart) modified to use my CustomAppointment instead of Appointment - not sure if that will make a difference in your process but thought it might be important.



PS Preethika Selvam Syncfusion Team February 5, 2024 01:52 PM UTC

Hi Cody,


We are validating your query at our end, and we will update further details in one business day within February 06, 2023. We appreciate your patience until then.


Regards,

Preethika Selvam



YG Yuvaraj Gajaraj Syncfusion Team February 7, 2024 02:59 AM UTC

Hi Cody,


Thanks for your patience. We have checked your custom_appointment file and tried to replicate the reported issue at our end. We used the CustomDataSource and CustomAppointment in the shared file and tried to delete the recurrence appointment when performing onTap. Unfortunately, the reported issue is not getting reproduced at our end. In the shared customer_appointment.dart file you have provided a method called updateAppointment and we didn't know where you used it in your sample and couldn't find the method called getPatternAppointment and getOccurrenceAppointment in the shared file. So, we kindly request you to try to replicate the reported issue in the below attached test sample and revert us so that it will help us assist you in a better way.


Regards,

Yuvaraj.


Attachment: f177262_d331bbe6.zip

Loader.
Up arrow icon