From 3e95bf4473adc96298de910d6f327c02a884ab09 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 10 Jul 2025 10:56:10 +0300 Subject: [PATCH] Refactor booking system: replace individual parameters with LoadBookableSpacesParam for improved clarity and maintainability --- .../services/bookable_spaces_service.dart | 37 ++- .../domain/load_bookable_spaces_param.dart | 36 +++ .../DebouncedBookingSystemService.dart | 26 +- .../services/booking_system_service.dart | 8 +- .../bloc/sidebar/sidebar_bloc.dart | 31 +- .../view/widgets/weekly_calendar_page.dart | 305 +++++++++--------- 6 files changed, 244 insertions(+), 199 deletions(-) create mode 100644 lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart diff --git a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart index 2f3e08b8..72efcc1b 100644 --- a/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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'; @@ -13,25 +14,29 @@ class BookableSpacesService implements BookingSystemService { @override Future getBookableSpaces({ - required int page, - required int size, - required String search, + required LoadCommunitiesParam param, }) async { try { final response = await _httpService.get( - path: ApiEndpoints.getBookableSpaces, - queryParameters: { - 'page': page, - 'size': size, - 'active': true, - 'configured': true, - if (search.isNotEmpty && search != 'null') 'search': search, - }, - expectedResponseModel: (json) { - return PaginatedBookableSpaces.fromJson( - json as Map, - ); - }); + path: ApiEndpoints.getBookableSpaces, + queryParameters: { + 'page': param.page, + 'size': param.size, + 'active': true, + 'configured': true, + if (param.search != null && + param.search.isNotEmpty && + param.search != 'null') + 'search': param.search, + if (param.includeSpaces != null) + 'includeSpaces': param.includeSpaces, + }, + expectedResponseModel: (json) { + return PaginatedBookableSpaces.fromJson( + json as Map, + ); + }, + ); return response; } on DioException catch (e) { final responseData = e.response?.data; diff --git a/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart new file mode 100644 index 00000000..f2b2e5fe --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +class LoadBookableSpacesParam extends Equatable { + const LoadBookableSpacesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.active = true, + this.configured = true, + }); + + final int page; + final int size; + final String search; + final bool active; + final bool configured; + + LoadBookableSpacesParam copyWith({ + int? page, + int? size, + String? search, + bool? active, + bool? configured, + }) { + return LoadBookableSpacesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + active: active ?? this.active, + configured: configured ?? this.configured, + ); + } + + @override + List get props => [page, size, search, active, configured]; +} diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart index 252810e2..66a2c01f 100644 --- a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; class DebouncedBookingSystemService implements BookingSystemService { final BookingSystemService _inner; @@ -9,11 +11,6 @@ class DebouncedBookingSystemService implements BookingSystemService { Timer? _debounceTimer; Completer? _lastCompleter; - int? _lastPage; - int? _lastSize; - bool? _lastIncludeSpaces; - String? _lastSearch; - DebouncedBookingSystemService( this._inner, { this.debounceDuration = const Duration(milliseconds: 500), @@ -21,27 +18,20 @@ class DebouncedBookingSystemService implements BookingSystemService { @override Future getBookableSpaces({ - required int page, - required int size, - required String search, + required LoadCommunitiesParam param, }) { _debounceTimer?.cancel(); - _lastCompleter?.completeError(StateError("Cancelled by new search")); + if (_lastCompleter != null && !_lastCompleter!.isCompleted) { + _lastCompleter! + .completeError(StateError("Cancelled by new search")); + } final completer = Completer(); _lastCompleter = completer; - _lastPage = page; - _lastSize = size; - _lastSearch = search; - _debounceTimer = Timer(debounceDuration, () async { try { - final result = await _inner.getBookableSpaces( - page: _lastPage!, - size: _lastSize!, - search: _lastSearch!, - ); + final result = await _inner.getBookableSpaces(param: param); if (!completer.isCompleted) { completer.complete(result); } diff --git a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart index 40a9a8e4..b6d82d23 100644 --- a/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart +++ b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart @@ -1,10 +1,8 @@ import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; abstract class BookingSystemService { Future getBookableSpaces({ - required int page, - required int size, - required String search, - + required LoadCommunitiesParam param, }); -} \ No newline at end of file +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart index 0b9c9295..874971de 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -3,10 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; class SidebarBloc extends Bloc { final BookingSystemService _bookingService; - Timer? _searchDebounce; int _currentPage = 1; final int _pageSize = 20; String _currentSearch = ''; @@ -35,9 +35,11 @@ class SidebarBloc extends Bloc { _currentSearch = ''; final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ), ); emit(state.copyWith( @@ -67,9 +69,12 @@ class SidebarBloc extends Bloc { _currentPage++; final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + // Add any other required params + ), ); final updatedRooms = [...state.allRooms, ...paginatedSpaces.data]; @@ -79,6 +84,7 @@ class SidebarBloc extends Bloc { displayedRooms: updatedRooms, isLoadingMore: false, hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, currentPage: _currentPage, )); } catch (e) { @@ -99,11 +105,13 @@ class SidebarBloc extends Bloc { _currentPage = 1; emit(state.copyWith(isLoading: true, errorMessage: null)); final paginatedSpaces = await _bookingService.getBookableSpaces( - page: _currentPage, - size: _pageSize, - search: _currentSearch, + param: LoadCommunitiesParam( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + // Add other fields if required + ), ); - emit(state.copyWith( allRooms: paginatedSpaces.data, displayedRooms: paginatedSpaces.data, @@ -137,7 +145,6 @@ class SidebarBloc extends Bloc { @override Future close() { - _searchDebounce?.cancel(); return super.close(); } } 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 9b521368..fd086de5 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 @@ -40,181 +40,191 @@ class WeeklyCalendarPage extends StatelessWidget { const double timeLineWidth = 80; const int totalDays = 7; final double dayColumnWidth = - (calendarWidth - timeLineWidth) / totalDays; + (calendarWidth - timeLineWidth) / totalDays - 0.1; final selectedDayIndex = weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return Padding( padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), child: Stack( children: [ - WeekView( - key: ValueKey(weekStart), - controller: eventController, - initialDay: weekStart, - startHour: startHour - 1, - endHour: endHour, - heightPerMinute: 1.1, - showLiveTimeLineInAllDays: false, - showVerticalLines: true, - emulateVerticalOffsetBy: -80, - startDay: WeekDays.monday, - liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( - showBullet: false, - height: 0, - ), - weekDayBuilder: (date) { - final weekDays = _getWeekDays(weekStart); - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); - final index = weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - final isToday = isSameDay(date, DateTime.now()); - - return Container( - decoration: isSelectedDay - ? BoxDecoration( - color: ColorsManager.blue1.withOpacity(0.2), - borderRadius: BorderRadius.circular(6), - ) - : isToday - ? BoxDecoration( - color: ColorsManager.blue1.withOpacity(0.08), - borderRadius: BorderRadius.circular(6), - ) - : null, - child: 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, - ), - ), - ], + AbsorbPointer( + absorbing: false, + ignoringSemantics: false, + child: WeekView( + scrollPhysics: const NeverScrollableScrollPhysics(), + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: startHour - 1, + endHour: endHour, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, ), - ); - }, - 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( + weekDayBuilder: (date) { + final weekDays = _getWeekDays(weekStart); + final selectedDayIndex = weekDays + .indexWhere((d) => isSameDay(d, selectedDate)); + final index = + weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + final isToday = isSameDay(date, DateTime.now()); + + return Column( children: [ - TextSpan( - text: '$hour', - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: ColorsManager.blackColor, + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, ), ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 2, top: 6), - child: Text( - period, + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ); + }, + 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.w400, - fontSize: 12, + fontWeight: FontWeight.w700, + fontSize: 24, color: ColorsManager.blackColor, - letterSpacing: 1, ), ), - ), - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, + 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, + ), + ], + ), + ), + ); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 60, + weekNumberBuilder: (firstDayOfWeek) => Padding( + padding: const EdgeInsets.only( + right: 15, + bottom: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + firstDayOfWeek.timeZoneName.replaceAll(':00', ''), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontSize: 12, + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), ), ], ), ), - ); - }, - timeLineWidth: timeLineWidth, - weekPageHeaderBuilder: (start, end) => Container(), - weekTitleHeight: 60, - weekNumberBuilder: (firstDayOfWeek) => Text( - firstDayOfWeek.timeZoneName, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), - 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.grayColor - : ColorsManager.lightGrayColor - .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, - ), + 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), ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), + 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(), - ), - ); - }, - ), + ), + ); + }).toList(), + ), + ); + }, + )), if (selectedDayIndex >= 0) Positioned( - left: timeLineWidth + dayColumnWidth * selectedDayIndex, + left: (timeLineWidth + 3) + + (dayColumnWidth - 8) * (selectedDayIndex - 0.01), top: 0, bottom: 0, width: dayColumnWidth, child: IgnorePointer( child: Container( margin: const EdgeInsets.symmetric( - vertical: 0, horizontal: 2), - color: ColorsManager.blue1.withOpacity(0.1), + vertical: 0, horizontal: 4), + color: ColorsManager.spaceColor.withOpacity(0.07), ), ), ), @@ -253,7 +263,6 @@ int _parseHour(String? time, {required int defaultValue}) { } try { return int.parse(time.split(':')[0]); - } catch (e) { return defaultValue; }