Repository: linagora/tmail-flutter
Pull request #3610: TF-3578 Calendar event show free busy status
Author: login: tddang-linagora
Created: 2025-04-02T04:26:26Z | Merged: 2025-04-04T07:13:40Z

## Issue
- #3578 

## Dependency

- Need merged: https://github.com/linagora/jmap-dart-client/pull/107

## Demo

https://github.com/user-attachments/assets/b175be09-cce1-4fb7-a704-56b3e8c77295




Diff:
diff --git a/contact/pubspec.lock b/contact/pubspec.lock
index c9ddc3944d..ae6919613c 100644
--- a/contact/pubspec.lock
+++ b/contact/pubspec.lock
@@ -653,7 +653,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart
index dc8e032773..4a7f17f794 100644
--- a/core/lib/presentation/extensions/color_extension.dart
+++ b/core/lib/presentation/extensions/color_extension.dart
@@ -247,6 +247,7 @@ extension AppColor on Color {
   static const m3Neutral70 = Color(0xFFAEAAAE);
   static const grayBackgroundColor = Color(0xFFF3F6F9);
   static const m3SurfaceBackground = Color(0xFF1C1B1F);
+  static const warningColor = Color(0xFFFFC107);
 
   static const mapGradientColor = [
     [Color(0xFF21D4FD), Color(0xFFB721FF)],
diff --git a/email_recovery/pubspec.lock b/email_recovery/pubspec.lock
index 368b0e4767..cde1eb3ab3 100644
--- a/email_recovery/pubspec.lock
+++ b/email_recovery/pubspec.lock
@@ -296,7 +296,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock
index 329ea1540f..13c974fe05 100644
--- a/fcm/pubspec.lock
+++ b/fcm/pubspec.lock
@@ -296,7 +296,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/forward/pubspec.lock b/forward/pubspec.lock
index 329ea1540f..13c974fe05 100644
--- a/forward/pubspec.lock
+++ b/forward/pubspec.lock
@@ -296,7 +296,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/lib/features/email/data/network/calendar_event_api.dart b/lib/features/email/data/network/calendar_event_api.dart
index 91c4ddb1c2..ba3adf82ed 100644
--- a/lib/features/email/data/network/calendar_event_api.dart
+++ b/lib/features/email/data/network/calendar_event_api.dart
@@ -1,9 +1,14 @@
 import 'dart:async';
 
+import 'package:core/utils/app_logger.dart';
 import 'package:jmap_dart_client/http/http_client.dart';
 import 'package:jmap_dart_client/jmap/account_id.dart';
 import 'package:jmap_dart_client/jmap/core/id.dart';
+import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart';
+import 'package:jmap_dart_client/jmap/core/response/response_object.dart';
 import 'package:jmap_dart_client/jmap/jmap_request.dart';
+import 'package:jmap_dart_client/jmap/mail/calendar/attendance/get_calendar_event_attendance_method.dart';
+import 'package:jmap_dart_client/jmap/mail/calendar/attendance/get_calendar_event_attendance_response.dart';
 import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_method.dart';
 import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_response.dart';
 import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_method.dart';
@@ -23,8 +28,20 @@ class CalendarEventAPI {
 
   Future<List<BlobCalendarEvent>> parse(AccountId accountId, Set<Id> blobIds) async {
     final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
+
+    // Parse
     final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds);
     final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod);
+
+    // Free/Busy query
+    final calendarEventAttendanceGetMethod = GetCalendarEventAttendanceMethod(
+      accountId,
+      blobIds.toList(),
+    );
+    final calendarEventAttendanceGetInvocation = requestBuilder.invocation(
+      calendarEventAttendanceGetMethod,
+    );
+
     final response = await (requestBuilder
         ..usings(calendarEventParseMethod.requiredCapabilities))
       .build()
@@ -33,11 +50,24 @@ class CalendarEventAPI {
     final calendarEventParseResponse = response.parse<CalendarEventParseResponse>(
       calendarEventParseInvocation.methodCallId,
       CalendarEventParseResponse.deserialize);
+    final calendarEventAttendanceGetResponse = _parseCalendarEventAttendance(
+      response,
+      calendarEventAttendanceGetInvocation.methodCallId,
+    );
+    final calendarBlobIdStatusMap = Map.fromEntries(
+      (calendarEventAttendanceGetResponse?.list ?? []).map(
+        (calendarEventAttendance) => MapEntry(
+          calendarEventAttendance.blobId,
+          calendarEventAttendance.isFree,
+        ),
+      ),
+    );
 
     if (calendarEventParseResponse?.parsed?.isNotEmpty == true) {
       return calendarEventParseResponse!.parsed!.entries
         .map((entry) => BlobCalendarEvent(
           blobId: entry.key,
+          isFree: calendarBlobIdStatusMap[entry.key] ?? true,
           calendarEventList: entry.value))
         .toList();
     } else if (calendarEventParseResponse?.notParsable?.isNotEmpty == true) {
@@ -49,6 +79,20 @@ class CalendarEventAPI {
     }
   }
 
+  GetCalendarEventAttendanceResponse? _parseCalendarEventAttendance(
+    ResponseObject response,
+    MethodCallId methodCallId,
+  ) {
+    try {
+      return response.parse<GetCalendarEventAttendanceResponse>(
+        methodCallId,
+        GetCalendarEventAttendanceResponse.deserialize);
+    } catch (e) {
+      logError('CalendarEventAPI.parse free/busy query error: $e');
+      return null;
+    }
+  }
+
   Future<CalendarEventAcceptResponse> acceptEventInvitation(
     AccountId accountId,
     Set<Id> blobIds,
diff --git a/lib/features/email/data/repository/calendar_event_repository_impl.dart b/lib/features/email/data/repository/calendar_event_repository_impl.dart
index c0e48e4179..8c5df318ae 100644
--- a/lib/features/email/data/repository/calendar_event_repository_impl.dart
+++ b/lib/features/email/data/repository/calendar_event_repository_impl.dart
@@ -66,6 +66,7 @@ class CalendarEventRepositoryImpl extends CalendarEventRepository {
         calendarEventList: await Future.wait(blobCalendarEvent.calendarEventList.map((calendarEvent) {
           return _transformCalendarEventDescription(calendarEvent, transformConfiguration);
         })),
+        isFree: blobCalendarEvent.isFree,
       );
     }));
   }
diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart
index f8f86ba93d..a47f7fc64d 100644
--- a/lib/features/email/presentation/controller/single_email_controller.dart
+++ b/lib/features/email/presentation/controller/single_email_controller.dart
@@ -211,6 +211,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
 
   CalendarEvent? get calendarEvent => blobCalendarEvent.value?.calendarEventList.firstOrNull;
   Id? get _displayingEventBlobId => blobCalendarEvent.value?.blobId;
+  bool get isCalendarEventFree => blobCalendarEvent.value?.isFree ?? true;
 
   AccountId? get accountId => mailboxDashBoardController.accountId.value;
 
diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart
index 4b68b2bd68..aaf9ef2b8c 100644
--- a/lib/features/email/presentation/email_view.dart
+++ b/lib/features/email/presentation/email_view.dart
@@ -371,6 +371,7 @@ class EmailView extends GetWidget<SingleEmailController> {
                 presentationEmail: controller.currentEmail,
                 onMailtoAttendeesAction: controller.handleMailToAttendees,
                 openEmailAddressDetailAction: controller.openEmailAddressDialog,
+                isFree: controller.isCalendarEventFree,
               )),
               if (_validateDisplayEventActionBanner(
                   context: context,
diff --git a/lib/features/email/presentation/model/blob_calendar_event.dart b/lib/features/email/presentation/model/blob_calendar_event.dart
index 5a333ef864..9fba0be5ec 100644
--- a/lib/features/email/presentation/model/blob_calendar_event.dart
+++ b/lib/features/email/presentation/model/blob_calendar_event.dart
@@ -5,9 +5,14 @@ import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart';
 class BlobCalendarEvent with EquatableMixin {
   final Id blobId;
   final List<CalendarEvent> calendarEventList;
+  final bool isFree;
 
-  BlobCalendarEvent({required this.blobId, required this.calendarEventList});
+  BlobCalendarEvent({
+    required this.blobId,
+    required this.calendarEventList,
+    this.isFree = true,
+  });
 
   @override
-  List<Object?> get props => [blobId, calendarEventList];
+  List<Object?> get props => [blobId, calendarEventList, isFree];
 }
\ No newline at end of file
diff --git a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart
index 874e389f22..47983e926a 100644
--- a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart
+++ b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart
@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
 class EventTimeInformationWidgetStyles {
   static const double maxWidth = 100;
   static const double textSize = 16;
+  static const double horizontalSpacing = 8;
   static const Color labelColor = AppColor.colorSubTitleEventActionText;
   static const Color valueColor = Colors.black;
+  static const Color conflictColor = AppColor.warningColor;
 }
\ No newline at end of file
diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart
index 798a476208..4f4877cf13 100644
--- a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart
+++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart
@@ -32,6 +32,7 @@ class CalendarEventInformationWidget extends StatelessWidget {
   final PresentationEmail? presentationEmail;
   final OnMailtoAttendeesAction? onMailtoAttendeesAction;
   final OnOpenEmailAddressDetailAction? openEmailAddressDetailAction;
+  final bool isFree;
 
   final _responsiveUtils = Get.find<ResponsiveUtils>();
 
@@ -40,6 +41,7 @@ class CalendarEventInformationWidget extends StatelessWidget {
     required this.calendarEvent,
     required this.onCalendarEventReplyActionClick,
     required this.calendarEventReplying,
+    required this.isFree,
     this.onOpenNewTabAction,
     this.onOpenComposerAction,
     this.presentationEmail,
@@ -250,7 +252,10 @@ class CalendarEventInformationWidget extends StatelessWidget {
     if (dateTimeEvent.isNotEmpty) {
       return Padding(
         padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding),
-        child: EventTimeInformationWidget(timeEvent: dateTimeEvent),
+        child: EventTimeInformationWidget(
+          timeEvent: dateTimeEvent,
+          isFree: isFree,
+        ),
       );
     } else {
       return const SizedBox.shrink();
diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
index 1130e5a23e..eb75d74bcd 100644
--- a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
+++ b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
@@ -1,7 +1,10 @@
 
+import 'package:core/presentation/extensions/color_extension.dart';
+import 'package:core/presentation/resources/image_paths.dart';
 import 'package:core/presentation/utils/responsive_utils.dart';
 import 'package:core/presentation/utils/style_utils.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
 import 'package:tmail_ui_user/features/email/presentation/styles/event_time_information_widget_styles.dart';
 import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -9,17 +12,21 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
   });
 
   @override
   Widget build(BuildContext context) {
     final responsiveUtils = Get.find<ResponsiveUtils>();
+    final imagePaths = Get.find<ImagePaths>();
+    
     return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
+      crossAxisAlignment: CrossAxisAlignment.center,
       children: [
         SizedBox(
           width: EventTimeInformationWidgetStyles.maxWidth,
@@ -32,7 +39,7 @@ class EventTimeInformationWidget extends StatelessWidget {
             ),
           ),
         ),
-        Expanded(child: Text(
+        Flexible(child: Text(
           timeEvent,
           overflow: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultTextOverFlow,
           softWrap: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultSoftWrap,
@@ -42,7 +49,21 @@ class EventTimeInformationWidget extends StatelessWidget {
             fontWeight: FontWeight.w500,
             color: EventTimeInformationWidgetStyles.valueColor
           ),
-        ))
+        )),
+        if (!isFree)
+          ...[
+            const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing),
+            Tooltip(
+              message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime,
+              child: SvgPicture.asset(
+                imagePaths.icError,
+                width: 20,
+                height: 20,
+                fit: BoxFit.fill,
+                colorFilter: EventTimeInformationWidgetStyles.conflictColor.asFilter(),
+              ),
+            ),
+          ]
       ],
     );
   }
diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb
index b1b9fd0c5e..dfc59c1443 100644
--- a/lib/l10n/intl_messages.arb
+++ b/lib/l10n/intl_messages.arb
@@ -4370,6 +4370,12 @@
     "placeholders_order": [],
     "placeholders": {}
   },
+  "youHaveAnotherEventAtThatSameTime": "You have another event at that same time",
+  "@youHaveAnotherEventAtThatSameTime": {
+    "type": "text",
+    "placeholders_order": [],
+    "placeholders": {}
+  },
   "exitFullscreen": "Exit fullscreen",
   "@exitFullscreen": {
     "type": "text",
diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart
index a3c395a693..12b91dfdf7 100644
--- a/lib/main/localizations/app_localizations.dart
+++ b/lib/main/localizations/app_localizations.dart
@@ -4591,6 +4591,13 @@ class AppLocalizations {
     );
   }
 
+  String get youHaveAnotherEventAtThatSameTime {
+    return Intl.message(
+      'You have another event at that same time',
+      name: 'youHaveAnotherEventAtThatSameTime',
+    );
+  }
+
   String get exitFullscreen {
     return Intl.message(
       'Exit fullscreen',
diff --git a/model/pubspec.lock b/model/pubspec.lock
index b45d5351c2..7a38fb7728 100644
--- a/model/pubspec.lock
+++ b/model/pubspec.lock
@@ -645,7 +645,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/pubspec.lock b/pubspec.lock
index ee51633d41..cb53b5f9c6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1325,7 +1325,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock
index 329ea1540f..13c974fe05 100644
--- a/rule_filter/pubspec.lock
+++ b/rule_filter/pubspec.lock
@@ -296,7 +296,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/server_settings/pubspec.lock b/server_settings/pubspec.lock
index eba1996bf5..e4f7c775f1 100644
--- a/server_settings/pubspec.lock
+++ b/server_settings/pubspec.lock
@@ -288,7 +288,7 @@ packages:
     description:
       path: "."
       ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      resolved-ref: "708658c13e013882fde91c5afba886bd67641f8f"
       url: "https://github.com/linagora/jmap-dart-client.git"
     source: git
     version: "0.3.3"
diff --git a/test/features/email/data/network/calendar_event_api_test.dart b/test/features/email/data/network/calendar_event_api_test.dart
new file mode 100644
index 0000000000..241027b95d
--- /dev/null
+++ b/test/features/email/data/network/calendar_event_api_test.dart
@@ -0,0 +1,277 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:jmap_dart_client/http/http_client.dart';
+import 'package:jmap_dart_client/jmap/core/id.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:model/extensions/account_id_extensions.dart';
+import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart';
+import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart';
+import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart';
+
+import '../../../../fixtures/account_fixtures.dart';
+import 'calendar_event_api_test.mocks.dart';
+
+@GenerateNiceMocks([MockSpec<HttpClient>()])
+void main() {
+  group('CalendarEventAPI::test', () {
+    late HttpClient httpClient;
+    late CalendarEventAPI calendarEventAPI;
+
+    setUp(() {
+      httpClient = MockHttpClient();
+      calendarEventAPI = CalendarEventAPI(httpClient);
+    });
+
+    group('parse::test', () {
+      test('SHOULD return list of BlobCalendarEvent WHEN parse method success', () async {
+        final blobIds = {Id('blob_1'), Id('blob_2')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "parsed": {
+                  "blob_1": [
+                    {
+                      "uid": "event1",
+                      "title": "Meeting 1",
+                      "start": "2024-03-20T10:00:00Z",
+                      "duration": "PT1H"
+                    }
+                  ],
+                  "blob_2": [
+                    {
+                      "uid": "event2",
+                      "title": "Meeting 2",
+                      "start": "2024-03-21T14:00:00Z",
+                      "duration": "PT2H"
+                    }
+                  ]
+                }
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": [
+                  {
+                    "blobId": "blob_1",
+                    "isFree": true
+                  },
+                  {
+                    "blobId": "blob_2",
+                    "isFree": false
+                  }
+                ]
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        final result = await calendarEventAPI.parse(
+          AccountFixtures.aliceAccountId,
+          blobIds
+        );
+
+        verify(httpClient.post(
+          '',
+          data: {
+            "using": [
+              "urn:ietf:params:jmap:core",
+              "com:linagora:params:calendar:event",
+            ],
+            "methodCalls": [
+              [
+                "CalendarEvent/parse",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c0"
+              ],
+              [
+                "CalendarEventAttendance/get",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c1"
+              ]
+            ]
+          },
+          cancelToken: anyNamed('cancelToken'),
+        )).called(1);
+
+        expect(result, isA<List<BlobCalendarEvent>>());
+        expect(result.length, 2);
+        expect(result[0].blobId.value, 'blob_1');
+        expect(result[0].isFree, true);
+        expect(result[1].blobId.value, 'blob_2');
+        expect(result[1].isFree, false);
+      });
+
+      test('SHOULD throw NotParsableCalendarEventException WHEN response has notParsable', () async {
+        final blobIds = {Id('blob_1')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "notParsable": ["blob_1"]
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": []
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        expect(
+          () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds),
+          throwsA(isA<NotParsableCalendarEventException>())
+        );
+      });
+
+      test('SHOULD throw NotFoundCalendarEventException WHEN response has notFound', () async {
+        final blobIds = {Id('blob_1')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "notFound": ["blob_1"]
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": []
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        expect(
+          () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds),
+          throwsA(isA<NotFoundCalendarEventException>())
+        );
+      });
+
+      test('SHOULD throw NotParsableCalendarEventException WHEN response is empty', () async {
+        final blobIds = {Id('blob_1')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": []
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        expect(
+          () => calendarEventAPI.parse(AccountFixtures.aliceAccountId, blobIds),
+          throwsA(isA<NotParsableCalendarEventException>())
+        );
+      });
+
+      test('SHOULD return calendar events with default isFree value WHEN CalendarEventAttendance/get returns error', () async {
+        final blobIds = {Id('blob_1')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "parsed": {
+                  "blob_1": [
+                    {
+                      "uid": "event1",
+                      "title": "Meeting 1",
+                      "start": "2024-03-20T10:00:00Z",
+                      "duration": "PT1H"
+                    }
+                  ]
+                }
+              },
+              "c0"
+            ],
+            [
+              "error",
+              {
+                "type": "invalidArguments",
+                "description": "Invalid blobIds"
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        final result = await calendarEventAPI.parse(
+          AccountFixtures.aliceAccountId,
+          blobIds
+        );
+
+        expect(result, isA<List<BlobCalendarEvent>>());
+        expect(result.length, 1);
+        expect(result[0].blobId.value, 'blob_1');
+        expect(result[0].isFree, true);
+      });
+    });
+  });
+}
\ No newline at end of file
diff --git a/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart b/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart
new file mode 100644
index 0000000000..88af3deda0
--- /dev/null
+++ b/test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:get/get.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart';
+import 'package:core/presentation/utils/responsive_utils.dart';
+import 'package:core/presentation/resources/image_paths.dart';
+
+import '../../../../../fixtures/widget_fixtures.dart';
+
+void main() {
+  setUp(() {
+    Get.put<ResponsiveUtils>(ResponsiveUtils());
+    Get.put<ImagePaths>(ImagePaths());
+  });
+
+  tearDown(() {
+    Get.reset();
+  });
+
+  group('EventTimeInformationWidget tests', () {
+    Widget makeTestableWidget({
+      required String timeEvent,
+      required bool isFree,
+    }) {
+      return WidgetFixtures.makeTestableWidget(
+        child: EventTimeInformationWidget(
+          timeEvent: timeEvent,
+          isFree: isFree,
+        ),
+      );
+    }
+
+    testWidgets('should show error icon with tooltip when isFree is false', (tester) async {
+      // arrange
+      final widget = makeTestableWidget(
+        timeEvent: '10:00 AM - 11:00 AM',
+        isFree: false,
+      );
+
+      // act
+      await tester.pumpWidget(widget);
+      await tester.pumpAndSettle();
+
+      // assert
+      final svgFinder = find.byWidgetPredicate((widget) {
+        return widget is SvgPicture
+          && (widget.bytesLoader as SvgAssetLoader).assetName == Get.find<ImagePaths>().icError;
+      });
+      expect(svgFinder, findsOneWidget);
+      expect(find.byType(Tooltip), findsOneWidget);
+    });
+
+    testWidgets('should not show error icon when isFree is true', (tester) async {
+      // arrange
+      final widget = makeTestableWidget(
+        timeEvent: '10:00 AM - 11:00 AM',
+        isFree: true,
+      );
+
+      // act
+      await tester.pumpWidget(widget);
+      await tester.pumpAndSettle();
+
+      // assert
+      expect(find.byType(SvgPicture), findsNothing);
+      expect(find.byType(Tooltip), findsNothing);
+    });
+  });
+}
\ No newline at end of file


Reviews:
user: login: dab246
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T06:03:23Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T06:41:24Z
user: login: dab246
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T06:52:39Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T09:28:20Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:20:48Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:20:52Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:20:56Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:21:00Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:21:04Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:21:09Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T04:21:10Z
user: login: dab246
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T09:33:50Z
user: login: tddang-linagora
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T02:03:42Z
user: login: dab246
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-04T03:59:05Z
user: login: hoangdat
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T04:09:03Z
user: login: hoangdat
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T04:15:18Z

Review comments:
diff_hunk: @@ -42,7 +44,18 @@ class EventTimeInformationWidget extends StatelessWidget {
             fontWeight: FontWeight.w500,
             color: EventTimeInformationWidgetStyles.valueColor
           ),
-        ))
+        )),
+        if (!isFree)
+          ...[
+            const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing),
+            Tooltip(
+              message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime,
+              child: const Icon(
+                Icons.info,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: dab246
body: Please use icon in Figma: 

<img width="764" alt="Screenshot 2025-04-02 at 11 59 55" src="https://github.com/user-attachments/assets/96704792-3f2a-4278-8fed-9a36923b6066" />

created_at: 2025-04-02T05:01:18Z
author_association: MEMBER
diff_hunk: @@ -23,8 +26,18 @@ class CalendarEventAPI {
 
   Future<List<BlobCalendarEvent>> parse(AccountId accountId, Set<Id> blobIds) async {
     final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
+
+    // Parse
     final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds);
     final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod);
+
+    // Free/Busy query
+    final calendarFreeBusyQueryMethod = GetCalendarEventAttendanceMethod(
+      accountId,
+      blobIds.toList(),
+    );
path: lib/features/email/data/network/calendar_event_api.dart
user: login: dab246
body: It's not just for `FreeBusy`, we should name it correctly

![Screenshot 2025-04-02 at 12 48 27](https://github.com/user-attachments/assets/43aca605-8d61-4aac-a361-96e95cc6b358)

created_at: 2025-04-02T05:50:14Z
author_association: MEMBER
diff_hunk: @@ -33,11 +46,24 @@ class CalendarEventAPI {
     final calendarEventParseResponse = response.parse<CalendarEventParseResponse>(
       calendarEventParseInvocation.methodCallId,
       CalendarEventParseResponse.deserialize);
+    GetCalendarEventAttendanceResponse? calendarFreeBusyQueryResponse;
+    try {
+      calendarFreeBusyQueryResponse = response.parse<GetCalendarEventAttendanceResponse>(
+        calendarFreeBusyQueryInvocation.methodCallId,
+        GetCalendarEventAttendanceResponse.deserialize);
+    } catch (e) {
+      logError('CalendarEventAPI.parse free/busy query error: $e');
+    }
path: lib/features/email/data/network/calendar_event_api.dart
user: login: dab246
body: Please split function to handle it
created_at: 2025-04-02T05:50:46Z
author_association: MEMBER
diff_hunk: @@ -33,11 +46,24 @@ class CalendarEventAPI {
     final calendarEventParseResponse = response.parse<CalendarEventParseResponse>(
       calendarEventParseInvocation.methodCallId,
       CalendarEventParseResponse.deserialize);
+    GetCalendarEventAttendanceResponse? calendarFreeBusyQueryResponse;
+    try {
+      calendarFreeBusyQueryResponse = response.parse<GetCalendarEventAttendanceResponse>(
+        calendarFreeBusyQueryInvocation.methodCallId,
+        GetCalendarEventAttendanceResponse.deserialize);
+    } catch (e) {
+      logError('CalendarEventAPI.parse free/busy query error: $e');
+    }
 
     if (calendarEventParseResponse?.parsed?.isNotEmpty == true) {
       return calendarEventParseResponse!.parsed!.entries
         .map((entry) => BlobCalendarEvent(
           blobId: entry.key,
+          isFree: calendarFreeBusyQueryResponse
+            ?.list
+            .where((attendance) => attendance.blobId == entry.key)
+            .firstOrNull
+            ?.isFree ?? true,
path: lib/features/email/data/network/calendar_event_api.dart
user: login: dab246
body: We should create `Map<BlobId, bool> ` to store value `isFree`  of list blobIds. Helps with faster access, avoiding wasting memory and browsing the list multiple times.
created_at: 2025-04-02T05:53:31Z
author_association: MEMBER
diff_hunk: @@ -42,7 +44,18 @@ class EventTimeInformationWidget extends StatelessWidget {
             fontWeight: FontWeight.w500,
             color: EventTimeInformationWidgetStyles.valueColor
           ),
-        ))
+        )),
+        if (!isFree)
+          ...[
+            const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing),
+            Tooltip(
+              message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime,
+              child: const Icon(
+                Icons.info,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: dab246
body: Please use icon in Figma: 

<img width="764" alt="Screenshot 2025-04-02 at 11 59 55" src="https://github.com/user-attachments/assets/9c3f9787-f0fd-4bf1-af7f-6cf75e5eb953" />

created_at: 2025-04-02T05:56:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,228 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:jmap_dart_client/http/http_client.dart';
+import 'package:jmap_dart_client/jmap/core/id.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:model/extensions/account_id_extensions.dart';
+import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart';
+import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart';
+import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart';
+
+import '../../../../fixtures/account_fixtures.dart';
+import 'calendar_event_api_test.mocks.dart';
+
+@GenerateNiceMocks([MockSpec<HttpClient>()])
+void main() {
+  group('CalendarEventAPI::test', () {
+    late HttpClient httpClient;
+    late CalendarEventAPI calendarEventAPI;
+
+    setUp(() {
+      httpClient = MockHttpClient();
+      calendarEventAPI = CalendarEventAPI(httpClient);
+    });
+
+    group('parse::test', () {
+      test('SHOULD return list of BlobCalendarEvent WHEN parse method success', () async {
+        final blobIds = {Id('blob_1'), Id('blob_2')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "parsed": {
+                  "blob_1": [
+                    {
+                      "uid": "event1",
+                      "title": "Meeting 1",
+                      "start": "2024-03-20T10:00:00Z",
+                      "duration": "PT1H"
+                    }
+                  ],
+                  "blob_2": [
+                    {
+                      "uid": "event2",
+                      "title": "Meeting 2",
+                      "start": "2024-03-21T14:00:00Z",
+                      "duration": "PT2H"
+                    }
+                  ]
+                }
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": [
+                  {
+                    "blobId": "blob_1",
+                    "isFree": true
+                  },
+                  {
+                    "blobId": "blob_2",
+                    "isFree": false
+                  }
+                ]
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        final result = await calendarEventAPI.parse(
+          AccountFixtures.aliceAccountId,
+          blobIds
+        );
+
+        verify(httpClient.post(
+          '',
+          data: {
+            "using": [
+              "urn:ietf:params:jmap:core",
+              "com:linagora:params:calendar:event",
+            ],
+            "methodCalls": [
+              [
+                "CalendarEvent/parse",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c0"
+              ],
+              [
+                "CalendarEventAttendance/get",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c1"
+              ]
+            ]
+          },
+          cancelToken: anyNamed('cancelToken'),
+        )).called(1);
+
+        expect(result, isA<List<BlobCalendarEvent>>());
+        expect(result.length, 2);
+        expect(result[0].blobId.value, 'blob_1');
+        expect(result[0].isFree, true);
+        expect(result[1].blobId.value, 'blob_2');
+        expect(result[1].isFree, false);
+      });
+
+      test('SHOULD throw NotParsableCalendarEventException WHEN response has notParsable', () async {
path: test/features/email/data/network/calendar_event_api_test.dart
user: login: dab246
body: We also should write test Exception for `CalendarEventAttendance/get` method
created_at: 2025-04-02T06:00:28Z
author_association: MEMBER
diff_hunk: @@ -9,17 +9,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: dab246
body: ```suggestion
    this.isFree = true,
```

To avoid charging more than where it's called.
created_at: 2025-04-02T06:01:33Z
author_association: MEMBER
diff_hunk: @@ -9,17 +9,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: tddang-linagora
body: Not necessary. At this moment there is only 1 reference.
created_at: 2025-04-02T06:41:24Z
author_association: COLLABORATOR
diff_hunk: @@ -9,17 +9,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: dab246
body: I see more changes here:

![Screenshot 2025-04-02 at 13 51 21](https://github.com/user-attachments/assets/2d7ac2ff-30e3-4ff2-a029-cb82a4520998)

created_at: 2025-04-02T06:52:39Z
author_association: MEMBER
diff_hunk: @@ -9,17 +9,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: tddang-linagora
body: That is from `BlobCalendarEvent` class, not `EventTimeInformationWidget`. Would making `isFree` of `BlobCalendarEvent` optional help?
created_at: 2025-04-02T09:28:20Z
author_association: COLLABORATOR
diff_hunk: @@ -42,7 +44,18 @@ class EventTimeInformationWidget extends StatelessWidget {
             fontWeight: FontWeight.w500,
             color: EventTimeInformationWidgetStyles.valueColor
           ),
-        ))
+        )),
+        if (!isFree)
+          ...[
+            const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing),
+            Tooltip(
+              message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime,
+              child: const Icon(
+                Icons.info,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:20:48Z
author_association: COLLABORATOR
diff_hunk: @@ -23,8 +26,18 @@ class CalendarEventAPI {
 
   Future<List<BlobCalendarEvent>> parse(AccountId accountId, Set<Id> blobIds) async {
     final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
+
+    // Parse
     final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds);
     final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod);
+
+    // Free/Busy query
+    final calendarFreeBusyQueryMethod = GetCalendarEventAttendanceMethod(
+      accountId,
+      blobIds.toList(),
+    );
path: lib/features/email/data/network/calendar_event_api.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:20:52Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +46,24 @@ class CalendarEventAPI {
     final calendarEventParseResponse = response.parse<CalendarEventParseResponse>(
       calendarEventParseInvocation.methodCallId,
       CalendarEventParseResponse.deserialize);
+    GetCalendarEventAttendanceResponse? calendarFreeBusyQueryResponse;
+    try {
+      calendarFreeBusyQueryResponse = response.parse<GetCalendarEventAttendanceResponse>(
+        calendarFreeBusyQueryInvocation.methodCallId,
+        GetCalendarEventAttendanceResponse.deserialize);
+    } catch (e) {
+      logError('CalendarEventAPI.parse free/busy query error: $e');
+    }
path: lib/features/email/data/network/calendar_event_api.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:20:56Z
author_association: COLLABORATOR
diff_hunk: @@ -33,11 +46,24 @@ class CalendarEventAPI {
     final calendarEventParseResponse = response.parse<CalendarEventParseResponse>(
       calendarEventParseInvocation.methodCallId,
       CalendarEventParseResponse.deserialize);
+    GetCalendarEventAttendanceResponse? calendarFreeBusyQueryResponse;
+    try {
+      calendarFreeBusyQueryResponse = response.parse<GetCalendarEventAttendanceResponse>(
+        calendarFreeBusyQueryInvocation.methodCallId,
+        GetCalendarEventAttendanceResponse.deserialize);
+    } catch (e) {
+      logError('CalendarEventAPI.parse free/busy query error: $e');
+    }
 
     if (calendarEventParseResponse?.parsed?.isNotEmpty == true) {
       return calendarEventParseResponse!.parsed!.entries
         .map((entry) => BlobCalendarEvent(
           blobId: entry.key,
+          isFree: calendarFreeBusyQueryResponse
+            ?.list
+            .where((attendance) => attendance.blobId == entry.key)
+            .firstOrNull
+            ?.isFree ?? true,
path: lib/features/email/data/network/calendar_event_api.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:21:00Z
author_association: COLLABORATOR
diff_hunk: @@ -42,7 +44,18 @@ class EventTimeInformationWidget extends StatelessWidget {
             fontWeight: FontWeight.w500,
             color: EventTimeInformationWidgetStyles.valueColor
           ),
-        ))
+        )),
+        if (!isFree)
+          ...[
+            const SizedBox(width: EventTimeInformationWidgetStyles.horizontalSpacing),
+            Tooltip(
+              message: AppLocalizations.of(context).youHaveAnotherEventAtThatSameTime,
+              child: const Icon(
+                Icons.info,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:21:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,228 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:jmap_dart_client/http/http_client.dart';
+import 'package:jmap_dart_client/jmap/core/id.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:model/extensions/account_id_extensions.dart';
+import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart';
+import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart';
+import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart';
+
+import '../../../../fixtures/account_fixtures.dart';
+import 'calendar_event_api_test.mocks.dart';
+
+@GenerateNiceMocks([MockSpec<HttpClient>()])
+void main() {
+  group('CalendarEventAPI::test', () {
+    late HttpClient httpClient;
+    late CalendarEventAPI calendarEventAPI;
+
+    setUp(() {
+      httpClient = MockHttpClient();
+      calendarEventAPI = CalendarEventAPI(httpClient);
+    });
+
+    group('parse::test', () {
+      test('SHOULD return list of BlobCalendarEvent WHEN parse method success', () async {
+        final blobIds = {Id('blob_1'), Id('blob_2')};
+        
+        when(httpClient.post(
+          '',
+          data: anyNamed('data'),
+          cancelToken: anyNamed('cancelToken'),
+        )).thenAnswer((_) async => {
+          "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+          "methodResponses": [
+            [
+              "CalendarEvent/parse",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "parsed": {
+                  "blob_1": [
+                    {
+                      "uid": "event1",
+                      "title": "Meeting 1",
+                      "start": "2024-03-20T10:00:00Z",
+                      "duration": "PT1H"
+                    }
+                  ],
+                  "blob_2": [
+                    {
+                      "uid": "event2",
+                      "title": "Meeting 2",
+                      "start": "2024-03-21T14:00:00Z",
+                      "duration": "PT2H"
+                    }
+                  ]
+                }
+              },
+              "c0"
+            ],
+            [
+              "CalendarEventAttendance/get",
+              {
+                "accountId": AccountFixtures.aliceAccountId.asString,
+                "list": [
+                  {
+                    "blobId": "blob_1",
+                    "isFree": true
+                  },
+                  {
+                    "blobId": "blob_2",
+                    "isFree": false
+                  }
+                ]
+              },
+              "c1"
+            ]
+          ]
+        });
+
+        final result = await calendarEventAPI.parse(
+          AccountFixtures.aliceAccountId,
+          blobIds
+        );
+
+        verify(httpClient.post(
+          '',
+          data: {
+            "using": [
+              "urn:ietf:params:jmap:core",
+              "com:linagora:params:calendar:event",
+            ],
+            "methodCalls": [
+              [
+                "CalendarEvent/parse",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c0"
+              ],
+              [
+                "CalendarEventAttendance/get",
+                {
+                  "accountId": AccountFixtures.aliceAccountId.asString,
+                  "blobIds": ["blob_1", "blob_2"]
+                },
+                "c1"
+              ]
+            ]
+          },
+          cancelToken: anyNamed('cancelToken'),
+        )).called(1);
+
+        expect(result, isA<List<BlobCalendarEvent>>());
+        expect(result.length, 2);
+        expect(result[0].blobId.value, 'blob_1');
+        expect(result[0].isFree, true);
+        expect(result[1].blobId.value, 'blob_2');
+        expect(result[1].isFree, false);
+      });
+
+      test('SHOULD throw NotParsableCalendarEventException WHEN response has notParsable', () async {
path: test/features/email/data/network/calendar_event_api_test.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-02T10:21:08Z
author_association: COLLABORATOR
diff_hunk: @@ -9,17 +9,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
 class EventTimeInformationWidget extends StatelessWidget {
 
   final String timeEvent;
+  final bool isFree;
 
   const EventTimeInformationWidget({
     super.key,
-    required this.timeEvent
+    required this.timeEvent,
+    required this.isFree,
path: lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart
user: login: tddang-linagora
body: Reverted to non-required
created_at: 2025-04-03T04:21:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:get/get.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart';
+import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart';
+import 'package:tmail_ui_user/main/localizations/localization_service.dart';
+import 'package:core/presentation/utils/responsive_utils.dart';
+import 'package:core/presentation/resources/image_paths.dart';
+
+void main() {
+  setUp(() {
+    Get.put<ResponsiveUtils>(ResponsiveUtils());
+    Get.put<ImagePaths>(ImagePaths());
+  });
+
+  tearDown(() {
+    Get.reset();
+  });
+
+  group('EventTimeInformationWidget tests', () {
+    Widget makeTestableWidget({
+      required String timeEvent,
+      required bool isFree,
+    }) {
+      return MaterialApp(
+        localizationsDelegates: const [
+          AppLocalizationsDelegate(),
+          GlobalMaterialLocalizations.delegate,
+          GlobalWidgetsLocalizations.delegate,
+          GlobalCupertinoLocalizations.delegate,
+        ],
+        supportedLocales: LocalizationService.supportedLocales,
+        locale: LocalizationService.defaultLocale,
+        home: Scaffold(
+          body: EventTimeInformationWidget(
+            timeEvent: timeEvent,
+            isFree: isFree,
+          ),
+        ),
+      );
+    }
+
path: test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart
user: login: dab246
body: Please use `makeTestableWidget` in WidgetFixtures
created_at: 2025-04-03T09:33:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:get/get.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart';
+import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart';
+import 'package:tmail_ui_user/main/localizations/localization_service.dart';
+import 'package:core/presentation/utils/responsive_utils.dart';
+import 'package:core/presentation/resources/image_paths.dart';
+
+void main() {
+  setUp(() {
+    Get.put<ResponsiveUtils>(ResponsiveUtils());
+    Get.put<ImagePaths>(ImagePaths());
+  });
+
+  tearDown(() {
+    Get.reset();
+  });
+
+  group('EventTimeInformationWidget tests', () {
+    Widget makeTestableWidget({
+      required String timeEvent,
+      required bool isFree,
+    }) {
+      return MaterialApp(
+        localizationsDelegates: const [
+          AppLocalizationsDelegate(),
+          GlobalMaterialLocalizations.delegate,
+          GlobalWidgetsLocalizations.delegate,
+          GlobalCupertinoLocalizations.delegate,
+        ],
+        supportedLocales: LocalizationService.supportedLocales,
+        locale: LocalizationService.defaultLocale,
+        home: Scaffold(
+          body: EventTimeInformationWidget(
+            timeEvent: timeEvent,
+            isFree: isFree,
+          ),
+        ),
+      );
+    }
+
path: test/features/email/presentation/widgets/calendar_event/event_time_information_widget_test.dart
user: login: tddang-linagora
body: Done
created_at: 2025-04-04T02:03:42Z
author_association: COLLABORATOR
diff_hunk: @@ -652,8 +652,8 @@ packages:
     dependency: "direct main"
     description:
       path: "."
-      ref: main
-      resolved-ref: "00b8c073e9e8b6f1caa831c400a5c31e56424c08"
+      ref: "feature/calendar-event-attendance"
+      resolved-ref: "6fa4350103c382d36fe2ad8df42aa0f347eecb68"
path: contact/pubspec.lock
user: login: hoangdat
body: I merged. please bump version and update here
created_at: 2025-04-04T03:52:54Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T04:37:17Z
body: This PR has been deployed to https://linagora.github.io/tmail-flutter/3610.
author_association: NONE
user: login: dab246
created_at: 2025-04-02T06:03:56Z
body: - [x] What's about display on mobile?
author_association: MEMBER
user: login: tddang-linagora
created_at: 2025-04-03T01:55:21Z
body: >What's about display on mobile?

![Screenshot 2025-04-03 at 08 54 30](https://github.com/user-attachments/assets/8d5a8fdd-15c3-4db3-bbea-cd006c87c0f0)

author_association: COLLABORATOR
user: login: dab246
created_at: 2025-04-04T03:59:32Z
body: - [x] Rebase it
author_association: MEMBER