diff --git a/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart new file mode 100644 index 00000000..aa3307d3 --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart @@ -0,0 +1,170 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteCalendarService implements CalendarSystemService { + const RemoteCalendarService(this._httpService); + + final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load Calendar'; + + @override + Future getCalendarEvents({ + required String spaceId, + }) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.getCalendarEvents, + queryParameters: { + 'spaceId': spaceId, + }, + expectedResponseModel: (json) { + return CalendarEventsResponse.fromJson( + json as Map, + ); + }, + ); + + return CalendarEventsResponse.fromJson(response as Map); + } on DioException catch (e) { + final responseData = e.response?.data; + if (responseData is Map) { + final errorMessage = responseData['error']?['message'] as String? ?? + responseData['message'] as String? ?? + _defaultErrorMessage; + throw APIException(errorMessage); + } + throw APIException(_defaultErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: ${e.toString()}'); + } + } +} + +class FakeRemoteCalendarService implements CalendarSystemService { + const FakeRemoteCalendarService(this._httpService, {this.useDummy = false}); + + final HTTPService _httpService; + final bool useDummy; + static const _defaultErrorMessage = 'Failed to load Calendar'; + + @override + Future getCalendarEvents({ + required String spaceId, + }) async { + if (useDummy) { + final dummyJson = { + 'statusCode': 200, + 'message': 'Successfully fetched all bookings', + 'data': [ + { + 'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80', + 'date': '2025-07-11T10:22:00.626Z', + 'startTime': '09:00:00', + 'endTime': '12:00:00', + 'cost': 10, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + }, + { + 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', + 'date': '2025-07-11T10:22:00.626Z', + 'startTime': '12:00:00', + 'endTime': '13:00:00', + 'cost': 10, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + }, + { + 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', + 'date': '2025-07-13T10:22:00.626Z', + 'startTime': '15:30:00', + 'endTime': '19:00:00', + 'cost': 20, + 'user': { + 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', + 'firstName': 'salsabeel', + 'lastName': 'abuzaid', + 'email': 'test@test.com', + 'companyName': null + }, + 'space': { + 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', + 'spaceName': '2(1)' + } + } + ], + 'success': true + }; + final response = CalendarEventsResponse.fromJson(dummyJson); + + // Filter events by spaceId + final filteredData = response.data.where((event) { + return event.space.uuid == spaceId; + }).toList(); + print('Filtering events for spaceId: $spaceId'); + print('Found ${filteredData.length} matching events'); + return filteredData.isNotEmpty + ? CalendarEventsResponse( + statusCode: response.statusCode, + message: response.message, + data: filteredData, + success: response.success, + ) + : CalendarEventsResponse( + statusCode: 404, + message: 'No events found for spaceId: $spaceId', + data: [], + success: false, + ); + } + + try { + final response = await _httpService.get( + path: ApiEndpoints.getCalendarEvents, + queryParameters: { + 'spaceId': spaceId, + }, + expectedResponseModel: (json) { + return CalendarEventsResponse.fromJson( + json as Map, + ); + }, + ); + + return CalendarEventsResponse.fromJson(response as Map); + } on DioException catch (e) { + final responseData = e.response?.data; + if (responseData is Map) { + final errorMessage = responseData['error']?['message'] as String? ?? + responseData['message'] as String? ?? + _defaultErrorMessage; + throw APIException(errorMessage); + } + throw APIException(_defaultErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: ${e.toString()}'); + } + } +} diff --git a/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart new file mode 100644 index 00000000..4b8f1ba1 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart @@ -0,0 +1,134 @@ +class CalendarEventBooking { + final String uuid; + final DateTime date; + final String startTime; + final String endTime; + final int cost; + final BookingUser user; + final BookingSpace space; + + CalendarEventBooking({ + required this.uuid, + required this.date, + required this.startTime, + required this.endTime, + required this.cost, + required this.user, + required this.space, + }); + + factory CalendarEventBooking.fromJson(Map json) { + return CalendarEventBooking( + uuid: json['uuid'] as String? ?? '', + date: json['date'] != null + ? DateTime.parse(json['date'] as String) + : DateTime.now(), + startTime: json['startTime'] as String? ?? '', + endTime: json['endTime'] as String? ?? '', + cost: _parseInt(json['cost']), + user: json['user'] != null + ? BookingUser.fromJson(json['user'] as Map) + : BookingUser.empty(), + space: json['space'] != null + ? BookingSpace.fromJson(json['space'] as Map) + : BookingSpace.empty(), + ); + } + + static int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; + } +} + +class BookingUser { + final String uuid; + final String firstName; + final String lastName; + final String email; + final String? companyName; + + BookingUser({ + required this.uuid, + required this.firstName, + required this.lastName, + required this.email, + this.companyName, + }); + + factory BookingUser.fromJson(Map json) { + return BookingUser( + uuid: json['uuid'] as String? ?? '', + firstName: json['firstName'] as String? ?? '', + lastName: json['lastName'] as String? ?? '', + email: json['email'] as String? ?? '', + companyName: json['companyName'] as String?, + ); + } + + factory BookingUser.empty() { + return BookingUser( + uuid: '', + firstName: '', + lastName: '', + email: '', + companyName: null, + ); + } +} + +class BookingSpace { + final String uuid; + final String spaceName; + + BookingSpace({ + required this.uuid, + required this.spaceName, + }); + + factory BookingSpace.fromJson(Map json) { + return BookingSpace( + uuid: json['uuid'] as String? ?? '', + spaceName: json['spaceName'] as String? ?? '', + ); + } + + factory BookingSpace.empty() { + return BookingSpace( + uuid: '', + spaceName: '', + ); + } +} + +class CalendarEventsResponse { + final int statusCode; + final String message; + final List data; + final bool success; + + CalendarEventsResponse({ + required this.statusCode, + required this.message, + required this.data, + required this.success, + }); + + factory CalendarEventsResponse.fromJson(Map json) { + return CalendarEventsResponse( + statusCode: _parseInt(json['statusCode']), + message: json['message'] as String? ?? '', + data: (json['data'] as List? ?? []) + .map((e) => CalendarEventBooking.fromJson(e as Map)) + .toList(), + success: json['success'] as bool? ?? false, + ); + } +} + +int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; +} diff --git a/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart new file mode 100644 index 00000000..9e178040 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart @@ -0,0 +1,7 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; + +abstract class CalendarSystemService { + Future getCalendarEvents({ + required String spaceId, + }); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart index 431720af..da782d74 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart @@ -2,13 +2,17 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; + part 'events_event.dart'; part 'events_state.dart'; class CalendarEventsBloc extends Bloc { final EventController eventController = EventController(); + final CalendarSystemService calendarService; - CalendarEventsBloc() : super(EventsInitial()) { + CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) { on(_onLoadEvents); on(_onAddEvent); on(_onStartTimer); @@ -22,53 +26,24 @@ class CalendarEventsBloc extends Bloc { ) async { emit(EventsLoading()); try { - final events = _generateDummyEventsForWeek(event.weekStart); + final response = await calendarService.getCalendarEvents( + spaceId: event.spaceId, + ); + final events = + response.data.map(_toCalendarEventData).toList(); eventController.addAll(events); - emit(EventsLoaded( - events: events, - initialDate: event.weekStart, - weekDays: _getWeekDays(event.weekStart), - )); + emit(EventsLoaded(events: events)); } catch (e) { emit(EventsError('Failed to load events')); } } - List _generateDummyEventsForWeek(DateTime weekStart) { - final events = []; - - for (int i = 0; i < 7; i++) { - final date = weekStart.add(Duration(days: i)); - - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 9, minute: 0), - endTime: date.copyWith(hour: 10, minute: 30), - title: 'Team Meeting', - description: 'Daily standup', - color: Colors.blue, - )); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 14, minute: 0), - endTime: date.copyWith(hour: 15, minute: 0), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - )); - } - - return events; - } - void _onAddEvent(AddEvent event, Emitter emit) { eventController.add(event.event); if (state is EventsLoaded) { final loaded = state as EventsLoaded; emit(EventsLoaded( events: [...eventController.events], - initialDate: loaded.initialDate, - weekDays: loaded.weekDays, )); } } @@ -86,47 +61,44 @@ class CalendarEventsBloc extends Bloc { final newWeekDays = _getWeekDays(event.weekDate); emit(EventsLoaded( events: loaded.events, - initialDate: event.weekDate, - weekDays: newWeekDays, )); } } - List _generateDummyEvents() { - final now = DateTime.now(); - return [ - CalendarEventData( - date: now, - startTime: now.copyWith(hour: 8, minute: 00, second: 0), - endTime: now.copyWith(hour: 9, minute: 00, second: 0), - title: 'Team Meeting', - description: 'Weekly team sync', - color: Colors.blue, - ), - CalendarEventData( - date: now, - startTime: now.copyWith(hour: 9, minute: 00, second: 0), - endTime: now.copyWith(hour: 10, minute: 30, second: 0), - title: 'Team Meeting', - description: 'Weekly team sync', - color: Colors.blue, - ), - CalendarEventData( - date: now.add(const Duration(days: 1)), - startTime: now.copyWith(hour: 14, day: now.day + 1), - endTime: now.copyWith(hour: 15, day: now.day + 1), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - ), - CalendarEventData( - date: now.add(const Duration(days: 2)), - startTime: now.copyWith(hour: 11, day: now.day + 2), - endTime: now.copyWith(hour: 12, day: now.day + 2), - title: 'Lunch with Team', - color: Colors.orange, - ), - ]; + CalendarEventData _toCalendarEventData(CalendarEventBooking booking) { + final date = booking.date; + + final localDate = date.toLocal(); + + final startParts = booking.startTime.split(':').map(int.parse).toList(); + final endParts = booking.endTime.split(':').map(int.parse).toList(); + + final startTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + startParts[0], + startParts[1], + ); + + final endTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + endParts[0], + endParts[1], + ); + + return CalendarEventData( + date: startTime, + startTime: startTime, + endTime: endTime, + title: + '${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}', + description: 'Cost: ${booking.cost}', + color: Colors.blue, + event: booking, + ); } List _getWeekDays(DateTime date) { diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart index e23e65de..4f4cafcf 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart @@ -6,13 +6,20 @@ abstract class CalendarEventsEvent { } class LoadEvents extends CalendarEventsEvent { + final String spaceId; final DateTime weekStart; - const LoadEvents({required this.weekStart}); + final DateTime weekEnd; + + const LoadEvents({ + required this.spaceId, + required this.weekStart, + required this.weekEnd, + }); } class AddEvent extends CalendarEventsEvent { final CalendarEventData event; - AddEvent(this.event); + const AddEvent(this.event); } class StartTimer extends CalendarEventsEvent {} @@ -23,3 +30,8 @@ class GoToWeek extends CalendarEventsEvent { final DateTime weekDate; GoToWeek(this.weekDate); } + +class CheckWeekHasEvents extends CalendarEventsEvent { + final DateTime weekStart; + const CheckWeekHasEvents(this.weekStart); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart index b7263ec8..bc0c2e31 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart @@ -9,13 +9,9 @@ class EventsLoading extends CalendarEventState {} class EventsLoaded extends CalendarEventState { final List events; - final DateTime initialDate; - final List weekDays; EventsLoaded({ required this.events, - required this.initialDate, - required this.weekDays, }); } diff --git a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart index 357cac41..0ff9aaf6 100644 --- a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; import 'package:calendar_view/calendar_view.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart'; @@ -9,7 +10,9 @@ import 'package:syncrow_web/pages/access_management/booking_system/presentation/ import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -35,33 +38,20 @@ class _BookingPageState extends State { super.dispose(); } - List _generateDummyEventsForWeek(DateTime weekStart) { - final List events = []; - for (int i = 0; i < 7; i++) { - final date = weekStart.add(Duration(days: i)); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 9, minute: 0), - endTime: date.copyWith(hour: 10, minute: 30), - title: 'Team Meeting', - description: 'Daily standup', - color: Colors.blue, - )); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 14, minute: 0), - endTime: date.copyWith(hour: 15, minute: 0), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - )); - } - return events; - } + void _dispatchLoadEvents(BuildContext context) { + final selectedRoom = + context.read().state.selectedBookableSpace; + final dateState = context.read().state; - void _loadEventsForWeek(DateTime weekStart) { - _eventController.removeWhere((_) => true); - _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + if (selectedRoom != null) { + context.read().add( + LoadEvents( + spaceId: selectedRoom.uuid, + weekStart: dateState.weekStart, + weekEnd: dateState.weekStart.add(const Duration(days: 6)), + ), + ); + } } @override @@ -70,197 +60,181 @@ class _BookingPageState extends State { providers: [ BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => DateSelectionBloc()), + BlocProvider( + create: (_) => CalendarEventsBloc( + calendarService: + FakeRemoteCalendarService(HTTPService(), useDummy: true), + ), + ), ], - child: BlocListener( - listenWhen: (previous, current) => - previous.weekStart != current.weekStart, - listener: (context, state) { - _loadEventsForWeek(state.weekStart); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(3, 0), - blurRadius: 6, - spreadRadius: 0, - ), - ], - ), - child: Column( - children: [ - Expanded( - flex: 2, - child: BlocBuilder( - builder: (context, state) { - return BookingSidebar( - onRoomSelected: (selectedRoom) { - context - .read() - .add(SelectBookableSpace(selectedRoom)); - }, - ); - }, + child: Builder( + builder: (context) => + BlocListener( + listenWhen: (prev, curr) => curr is EventsLoaded, + listener: (context, state) { + if (state is EventsLoaded) { + _eventController.removeWhere((_) => true); + _eventController.addAll(state.events); + } + }, + child: BlocListener( + listener: (context, state) => _dispatchLoadEvents(context), + child: BlocListener( + listener: (context, state) => _dispatchLoadEvents(context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, + ), + ], + ), + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (selectedRoom) { + context + .read() + .add(SelectBookableSpace(selectedRoom)); + }, + ); + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return CustomCalendarPage( + selectedDate: dateState.selectedDate, + onDateChanged: (day, month, year) { + final newDate = DateTime(year, month, day); + context + .read() + .add(SelectDate(newDate)); + context.read().add( + SelectDateFromSidebarCalendar(newDate)); + }, + ); + }, + ), + ), + ], ), ), - Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return CustomCalendarPage( - selectedDate: dateState.selectedDate, - onDateChanged: (day, month, year) { - final newDate = DateTime(year, month, day); - context - .read() - .add(SelectDate(newDate)); - context - .read() - .add(SelectDateFromSidebarCalendar(newDate)); - }, - ); - }, - ), - ), - ], - ), - ), - ), - Expanded( - flex: 4, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgTextButton( - svgAsset: Assets.homeIcon, - label: 'Manage Bookable Spaces', - onPressed: () {}, - ), - const SizedBox(width: 20), - SvgTextButton( - svgAsset: Assets.groupIcon, - label: 'Manage Users', - onPressed: () {}, - ), - ], - ), - BlocBuilder( - builder: (context, state) { - final weekStart = state.weekStart; - final weekEnd = - weekStart.add(const Duration(days: 6)); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: ColorsManager.circleRolesBackground, - borderRadius: BorderRadius.circular(10), - boxShadow: const [ - BoxShadow( - color: ColorsManager.lightGrayColor, - blurRadius: 4, - offset: Offset(0, 1), + ), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () {}, + ), + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}, ), ], ), - child: Row( - children: [ - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_back_ios, - color: ColorsManager.lightGrayColor), - onPressed: () { + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return WeekNavigation( + weekStart: weekStart, + weekEnd: weekEnd, + onPreviousWeek: () { context .read() .add(PreviousWeek()); }, - ), - const SizedBox(width: 10), - Text( - _getMonthYearText(weekStart, weekEnd), - style: const TextStyle( - color: ColorsManager.lightGrayColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(width: 10), - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_forward_ios, - color: ColorsManager.lightGrayColor), - onPressed: () { + onNextWeek: () { context .read() .add(NextWeek()); }, - ), - ], + ); + }, ), - ); - }, - ), - ], - ), - Expanded( - child: BlocBuilder( - builder: (context, roomState) { - final selectedRoom = roomState.selectedBookableSpace; - return BlocBuilder( - builder: (context, dateState) { - return WeeklyCalendarPage( - startTime: - selectedRoom?.bookableConfig.startTime, - endTime: selectedRoom?.bookableConfig.endTime, - weekStart: dateState.weekStart, - selectedDate: dateState.selectedDate, - eventController: _eventController, - selectedDateFromSideBarCalender: context - .watch() - .state - .selectedDateFromSideBarCalender, - ); - }, - ); - }, + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, roomState) { + final selectedRoom = + roomState.selectedBookableSpace; + return BlocBuilder( + builder: (context, dateState) { + return BlocListener( + listenWhen: (prev, curr) => + curr is EventsLoaded, + listener: (context, state) { + if (state is EventsLoaded) { + _eventController + .removeWhere((_) => true); + _eventController.addAll(state.events); + } + }, + child: WeeklyCalendarPage( + startTime: selectedRoom + ?.bookableConfig.startTime, + endTime: selectedRoom + ?.bookableConfig.endTime, + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + selectedDateFromSideBarCalender: context + .watch() + .state + .selectedDateFromSideBarCalender, + ), + ); + }, + ); + }, + ), + ), + ], ), ), - ], - ), + ), + ], ), ), - ], + ), ), ), ); } - - String _getMonthYearText(DateTime start, DateTime end) { - final startMonth = DateFormat('MMM').format(start); - final endMonth = DateFormat('MMM').format(end); - final year = start.year == end.year - ? start.year.toString() - : '${start.year}-${end.year}'; - - if (start.month == end.month) { - return '$startMonth $year'; - } else { - return '$startMonth - $endMonth $year'; - } - } } diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart new file mode 100644 index 00000000..6c0f9cb2 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart @@ -0,0 +1,60 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class EventTileWidget extends StatelessWidget { + final List> events; + + const EventTileWidget({ + super.key, + required this.events, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = + event.endTime != null && event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.lightGrayBorderColor + : ColorsManager.blue1.withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart new file mode 100644 index 00000000..da74d07f --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class HatchedColumnBackground extends StatelessWidget { + final Color backgroundColor; + final Color lineColor; + final double opacity; + final double stripeSpacing; + final BorderRadius? borderRadius; + + const HatchedColumnBackground({ + super.key, + required this.backgroundColor, + required this.lineColor, + this.opacity = 0.15, + this.stripeSpacing = 12, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _HatchedBackgroundPainter( + backgroundColor: backgroundColor, + opacity: opacity, + lineColor: lineColor, + stripeSpacing: stripeSpacing, + borderRadius: borderRadius, + ), + size: Size.infinite, + ); + } +} + +class _HatchedBackgroundPainter extends CustomPainter { + final Color backgroundColor; + final double opacity; + final Color lineColor; + final double stripeSpacing; + final BorderRadius? borderRadius; + + _HatchedBackgroundPainter({ + required this.backgroundColor, + required this.opacity, + required this.lineColor, + required this.stripeSpacing, + this.borderRadius, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final RRect rrect = borderRadius?.toRRect(rect) ?? + RRect.fromRectAndRadius(rect, Radius.zero); + final backgroundPaint = Paint() + ..color = backgroundColor.withOpacity(0.02) + ..style = PaintingStyle.fill; + canvas.drawRRect(rrect, backgroundPaint); + canvas.save(); + canvas.clipRRect(rrect); + final linePaint = Paint() + ..color = lineColor + ..strokeWidth = 0.5 + ..style = PaintingStyle.stroke; + final maxExtent = + math.sqrt(size.width * size.width + size.height * size.height); + + canvas.translate(0, size.height); + canvas.rotate(-math.pi / 4); + double y = -maxExtent; + while (y < maxExtent) { + canvas.drawLine( + Offset(-maxExtent, y), + Offset(maxExtent, y), + linePaint, + ); + y += stripeSpacing; + } + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) { + return backgroundColor != oldDelegate.backgroundColor || + opacity != oldDelegate.opacity || + lineColor != oldDelegate.lineColor || + stripeSpacing != oldDelegate.stripeSpacing || + borderRadius != oldDelegate.borderRadius; + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart new file mode 100644 index 00000000..eada3b97 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class TimeLineWidget extends StatelessWidget { + final DateTime date; + + const TimeLineWidget({Key? key, required this.date}) : super(key: key); + + @override + Widget build(BuildContext context) { + int hour = + date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart new file mode 100644 index 00000000..57e35c6d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekDayHeader extends StatelessWidget { + final DateTime date; + final bool isSelectedDay; + + const WeekDayHeader({ + Key? key, + required this.date, + required this.isSelectedDay, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, + ), + ), + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: + isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart new file mode 100644 index 00000000..bdc65b8e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekNavigation extends StatelessWidget { + final DateTime weekStart; + final DateTime weekEnd; + final VoidCallback onPreviousWeek; + final VoidCallback onNextWeek; + + const WeekNavigation({ + Key? key, + required this.weekStart, + required this.weekEnd, + required this.onPreviousWeek, + required this.onNextWeek, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_back_ios, + color: ColorsManager.lightGrayColor), + onPressed: onPreviousWeek, + ), + const SizedBox(width: 10), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 10), + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_forward_ios, + color: ColorsManager.lightGrayColor), + onPressed: onNextWeek, + ), + ], + ), + ); + } + + String _getMonthYearText(DateTime start, DateTime end) { + final startMonth = DateFormat('MMM').format(start); + final endMonth = DateFormat('MMM').format(end); + final year = start.year == end.year + ? start.year.toString() + : '${start.year}-${end.year}'; + + if (start.month == end.month) { + return '$startMonth $year'; + } else { + return '$startMonth - $endMonth $year'; + } + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 5c38e2fc..0dd343a7 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:calendar_view/calendar_view.dart'; -import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class WeeklyCalendarPage extends StatelessWidget { @@ -60,18 +63,39 @@ class WeeklyCalendarPage extends StatelessWidget { const double timeLineWidth = 80; const int totalDays = 7; - + final DateTime highlightStart = DateTime(2025, 7, 10); + final DateTime highlightEnd = DateTime(2025, 7, 19); return LayoutBuilder( builder: (context, constraints) { final double calendarWidth = constraints.maxWidth; final double dayColumnWidth = (calendarWidth - timeLineWidth) / totalDays - 0.1; + bool isInRange(DateTime date, DateTime start, DateTime end) { + return !date.isBefore(start) && !date.isAfter(end); + } return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( children: [ WeekView( + weekDetectorBuilder: ({ + required date, + required height, + required heightPerMinute, + required minuteSlotSize, + required width, + }) { + return isInRange(date, highlightStart, highlightEnd) + ? HatchedColumnBackground( + backgroundColor: ColorsManager.grey800, + lineColor: ColorsManager.textGray, + opacity: 0.3, + stripeSpacing: 12, + borderRadius: BorderRadius.circular(8), + ) + : const SizedBox(); + }, pageViewPhysics: const NeverScrollableScrollPhysics(), key: ValueKey(weekStart), controller: eventController, @@ -88,70 +112,13 @@ class WeeklyCalendarPage extends StatelessWidget { height: 0, ), weekDayBuilder: (date) { - final index = weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - return Column( - children: [ - Text( - DateFormat('EEE').format(date).toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: isSelectedDay ? Colors.blue : Colors.black, - ), - ), - Text( - DateFormat('d').format(date), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20, - color: isSelectedDay - ? ColorsManager.blue1 - : ColorsManager.blackColor, - ), - ), - ], + return WeekDayHeader( + date: date, + isSelectedDay: isSameDay(date, selectedDate), ); }, timeLineBuilder: (date) { - int hour = date.hour == 0 - ? 12 - : (date.hour > 12 ? date.hour - 12 : date.hour); - String period = date.hour >= 12 ? 'PM' : 'AM'; - return Container( - height: 60, - alignment: Alignment.center, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: '$hour', - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: ColorsManager.blackColor, - ), - ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 2, top: 6), - child: Text( - period, - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 12, - color: ColorsManager.blackColor, - letterSpacing: 1, - ), - ), - ), - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, - ), - ], - ), - ), - ); + return TimeLineWidget(date: date); }, timeLineWidth: timeLineWidth, weekPageHeaderBuilder: (start, end) => Container(), @@ -174,49 +141,8 @@ class WeeklyCalendarPage extends StatelessWidget { ), ), eventTileBuilder: (date, events, boundary, start, end) { - return Container( - margin: - const EdgeInsets.symmetric(vertical: 2, horizontal: 2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: events.map((event) { - final bool isEventEnded = event.endTime != null && - event.endTime!.isBefore(DateTime.now()); - return Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: isEventEnded - ? ColorsManager.lightGrayBorderColor - : ColorsManager.blue1.withOpacity(0.25), - borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('h:mm a').format(event.startTime!), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.black87, - ), - ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ); - }).toList(), - ), + return EventTileWidget( + events: events, ); }, ), diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index a36d1193..55bfef1d 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -69,7 +69,6 @@ abstract class ColorsManager { static const Color invitedOrange = Color(0xFFFFE193); static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color lightGrayBorderColor = Color(0xB2D5D5D5); - //background: #F8F8F8; static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); @@ -85,4 +84,5 @@ abstract class ColorsManager { static const Color minBlueDot = Color(0xFF023DFE); static const Color grey25 = Color(0xFFF9F9F9); static const Color grey50 = Color(0xFF718096); + static const Color grey800 = Color(0xffF8F8F8); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index f908db85..8797f0cd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -141,4 +141,5 @@ abstract class ApiEndpoints { static const String saveSchedule = '/schedule/{deviceUuid}'; static const String getBookableSpaces = '/bookable-spaces'; + static const String getCalendarEvents = '/api'; }