diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart index dd82d739..11c42f3b 100644 --- a/lib/pages/access_management/bloc/access_bloc.dart +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/hour_picker_dialog.dart'; import 'package:syncrow_web/services/access_mang_api.dart'; diff --git a/lib/pages/access_management/bloc/access_state.dart b/lib/pages/access_management/bloc/access_state.dart index 0790a735..cdaf9aad 100644 --- a/lib/pages/access_management/bloc/access_state.dart +++ b/lib/pages/access_management/bloc/access_state.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; abstract class AccessState extends Equatable { const AccessState(); diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart deleted file mode 100644 index 98d65fde..00000000 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart +++ /dev/null @@ -1,7 +0,0 @@ -part of 'selected_bookable_space_bloc.dart'; - -class SelectedBookableSpaceState { - final String? selectedSpaceId; - - const SelectedBookableSpaceState({this.selectedSpaceId}); -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart deleted file mode 100644 index 9d1b4a5b..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart +++ /dev/null @@ -1,68 +0,0 @@ - - - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; - -class SidebarBloc extends Bloc { - SidebarBloc() : super(SidebarState( - allRooms: [], - displayedRooms: [], - isLoading: true, - )) { - on(_onLoadRooms); - on(_onSelectRoom); - on(_onSearchRooms); - } - - Future _onLoadRooms( - LoadRoomsEvent event, - Emitter emit, - ) async { - try { - emit(state.copyWith(isLoading: true)); - - await Future.delayed(const Duration(seconds: 1)); - final rooms = List.generate(15, (index) => BookableRoom( - id: index, - name: 'Meeting Room ${index + 1}', - capacity: [4, 6, 8, 10][index % 4], - iconAsset: Assets.AtoZIcon, - )); - - emit(state.copyWith( - allRooms: rooms, - displayedRooms: rooms, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( - isLoading: false, - errorMessage: 'Failed to load rooms', - )); - } - } - - void _onSelectRoom( - SelectRoomEvent event, - Emitter emit, - ) { - emit(state.copyWith(selectedRoomId: event.roomId)); - } - - void _onSearchRooms( - SearchRoomsEvent event, - Emitter emit, - ) { - if (event.query.isEmpty) { - emit(state.copyWith(displayedRooms: state.allRooms)); - } else { - final filtered = state.allRooms.where((room) => - room.name.toLowerCase().contains(event.query.toLowerCase())).toList(); - emit(state.copyWith(displayedRooms: filtered)); - } - } -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart deleted file mode 100644 index 3fa504ef..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart +++ /dev/null @@ -1,16 +0,0 @@ - -abstract class SidebarEvent {} - -class LoadRoomsEvent extends SidebarEvent {} - -class SelectRoomEvent extends SidebarEvent { - final int roomId; - - SelectRoomEvent(this.roomId); -} - -class SearchRoomsEvent extends SidebarEvent { - final String query; - - SearchRoomsEvent(this.query); -} diff --git a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart deleted file mode 100644 index 5b30a9a0..00000000 --- a/lib/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart +++ /dev/null @@ -1,37 +0,0 @@ - - - - -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; - -class SidebarState { - final List allRooms; - final List displayedRooms; - final int? selectedRoomId; - final bool isLoading; - final String? errorMessage; - - SidebarState({ - required this.allRooms, - required this.displayedRooms, - this.selectedRoomId, - this.isLoading = false, - this.errorMessage, - }); - - SidebarState copyWith({ - List? allRooms, - List? displayedRooms, - int? selectedRoomId, - bool? isLoading, - String? errorMessage, - }) { - return SidebarState( - allRooms: allRooms ?? this.allRooms, - displayedRooms: displayedRooms ?? this.displayedRooms, - selectedRoomId: selectedRoomId ?? this.selectedRoomId, - isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage ?? this.errorMessage, - ); - } -} 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 new file mode 100644 index 00000000..5cc0decb --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/bookable_spaces_service.dart @@ -0,0 +1,50 @@ +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/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class BookableSpacesService implements BookingSystemService { + const BookableSpacesService(this._httpService); + + final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load bookable spaces'; + + @override + Future getBookableSpaces({ + required int page, + required int size, + required String search, + }) 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) { + print('Response JSON: $json'); + return PaginatedBookableSpaces.fromJson( + json as Map, + ); + }); + return response; + } 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/bookable_room.dart b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart new file mode 100644 index 00000000..b8aa58b9 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart @@ -0,0 +1,53 @@ +// bookable_space_model.dart +class BookableSpaceModel { + final String uuid; + final String spaceName; + final String virtualLocation; + final BookableConfig bookableConfig; + + BookableSpaceModel({ + required this.uuid, + required this.spaceName, + required this.virtualLocation, + required this.bookableConfig, + }); + + factory BookableSpaceModel.fromJson(Map json) { + return BookableSpaceModel( + uuid: json['uuid'] as String, + spaceName: json['spaceName'] as String, + virtualLocation: json['virtualLocation'] as String, + bookableConfig: BookableConfig.fromJson( + json['bookableConfig'] as Map), + ); + } +} + +class BookableConfig { + final String uuid; + final List daysAvailable; + final String startTime; + final String endTime; + final bool active; + final int points; + + BookableConfig({ + required this.uuid, + required this.daysAvailable, + required this.startTime, + required this.endTime, + required this.active, + required this.points, + }); + + factory BookableConfig.fromJson(Map json) { + return BookableConfig( + uuid: json['uuid'] as String, + daysAvailable: (json['daysAvailable'] as List).cast(), + startTime: json['startTime'] as String, + endTime: json['endTime'] as String, + active: json['active'] as bool, + points: json['points'] as int, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart new file mode 100644 index 00000000..b4b79bc2 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart @@ -0,0 +1,40 @@ + + +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class PaginatedBookableSpaces { + final List data; + final String message; + final int page; + final int size; + final int totalItem; + final int totalPage; + final bool hasNext; + final bool hasPrevious; + + PaginatedBookableSpaces({ + required this.data, + required this.message, + required this.page, + required this.size, + required this.totalItem, + required this.totalPage, + required this.hasNext, + required this.hasPrevious, + }); + + factory PaginatedBookableSpaces.fromJson(Map json) { + return PaginatedBookableSpaces( + data: (json['data'] as List) + .map((item) => BookableSpaceModel.fromJson(item)) + .toList(), + message: json['message'] as String, + page: json['page'] as int, + size: json['size'] as int, + totalItem: json['totalItem'] as int, + totalPage: json['totalPage'] as int, + hasNext: json['hasNext'] as bool, + hasPrevious: json['hasPrevious'] as bool, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/models/product.dart b/lib/pages/access_management/booking_system/domain/models/product.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart new file mode 100644 index 00000000..40e4e5ab --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/DebouncedBookingSystemService.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +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'; + +class DebouncedBookingSystemService implements BookingSystemService { + final BookingSystemService _inner; + final Duration debounceDuration; + + Timer? _debounceTimer; + Completer? _lastCompleter; + + // Store last parameters + int? _lastPage; + int? _lastSize; + bool? _lastIncludeSpaces; + String? _lastSearch; + + DebouncedBookingSystemService( + this._inner, { + this.debounceDuration = const Duration(milliseconds: 500), + }); + + @override + Future getBookableSpaces({ + required int page, + required int size, + required String search, + }) { + _debounceTimer?.cancel(); + _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!, + ); + if (!completer.isCompleted) { + completer.complete(result); + } + } catch (e, st) { + if (!completer.isCompleted) { + completer.completeError(e, st); + } + } + }); + + return completer.future; + } +} 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 new file mode 100644 index 00000000..40a9a8e4 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/booking_system_service.dart @@ -0,0 +1,10 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart'; + +abstract class BookingSystemService { + Future getBookableSpaces({ + required int page, + required int size, + required String search, + + }); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/model/bookable_room.dart b/lib/pages/access_management/booking_system/model/bookable_room.dart deleted file mode 100644 index 9f85984e..00000000 --- a/lib/pages/access_management/booking_system/model/bookable_room.dart +++ /dev/null @@ -1,13 +0,0 @@ -class BookableRoom { - final int id; - final String name; - final int capacity; - final String? iconAsset; - - BookableRoom({ - required this.id, - required this.name, - this.capacity = 4, - this.iconAsset, - }); -} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart diff --git a/lib/pages/access_management/booking_system/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/calendar/events_state.dart rename to lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart similarity index 95% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart index 6cf56fc7..d6687087 100644 --- a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart @@ -1,5 +1,5 @@ import 'package:bloc/bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'date_selection_state.dart'; class DateSelectionBloc extends Bloc { diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart diff --git a/lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart similarity index 100% rename from lib/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart rename to lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart similarity index 64% rename from lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart rename to lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart index 23eaff61..70b46c1a 100644 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart @@ -1,4 +1,5 @@ import 'package:bloc/bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; part 'selected_bookable_space_event.dart'; part 'selected_bookable_space_state.dart'; @@ -6,7 +7,8 @@ class SelectedBookableSpaceBloc extends Bloc { SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) { on((event, emit) { - emit(SelectedBookableSpaceState(selectedSpaceId: event.spaceId)); + emit(SelectedBookableSpaceState( + selectedBookableSpace: event.bookableSpace)); }); } } diff --git a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart similarity index 68% rename from lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart rename to lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart index d7fc931c..c74c13df 100644 --- a/lib/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart @@ -5,7 +5,7 @@ abstract class SelectedBookableSpaceEvent { } class SelectBookableSpace extends SelectedBookableSpaceEvent { - final dynamic spaceId; + final BookableSpaceModel bookableSpace; - const SelectBookableSpace(this.spaceId); + const SelectBookableSpace(this.bookableSpace); } diff --git a/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart new file mode 100644 index 00000000..8509d5c3 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart @@ -0,0 +1,9 @@ +part of 'selected_bookable_space_bloc.dart'; + +class SelectedBookableSpaceState { + final BookableSpaceModel? selectedBookableSpace; + + const SelectedBookableSpaceState( + { this.selectedBookableSpace,} + ); +} 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 new file mode 100644 index 00000000..9abf032a --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.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'; + +class SidebarBloc extends Bloc { + final BookingSystemService _bookingService; + Timer? _searchDebounce; + int _currentPage = 1; + final int _pageSize = 20; + String _currentSearch = ''; + + SidebarBloc(this._bookingService) + : super(SidebarState( + allRooms: [], + displayedRooms: [], + isLoading: true, + hasMore: true, + )) { + on(_onLoadBookableSpaces); + on(_onLoadMoreSpaces); + on(_onSelectRoom); + on(_onSearchRooms); + on(_onResetSearch); + } + + Future _onLoadBookableSpaces( + LoadBookableSpaces event, + Emitter emit, + ) async { + try { + emit(state.copyWith(isLoading: true, errorMessage: null)); + _currentPage = 1; + _currentSearch = ''; + + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + + final rooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, + currentPage: _currentPage, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Failed to load rooms: ${e.toString()}', + )); + } + } + + Future _onLoadMoreSpaces( + LoadMoreSpaces event, + Emitter emit, + ) async { + if (!state.hasMore || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + _currentPage++; + + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + + final newRooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + + final updatedRooms = [...state.allRooms, ...newRooms]; + + emit(state.copyWith( + allRooms: updatedRooms, + displayedRooms: updatedRooms, + isLoadingMore: false, + hasMore: paginatedSpaces.hasNext, + currentPage: _currentPage, + )); + } catch (e) { + _currentPage--; + emit(state.copyWith( + isLoadingMore: false, + errorMessage: 'Failed to load more rooms: ${e.toString()}', + )); + } + } + + Future _onSearchRooms( + SearchRoomsEvent event, + Emitter emit, + ) async { + try { + _currentSearch = event.query; + _currentPage = 1; + emit(state.copyWith(isLoading: true, errorMessage: null)); + final paginatedSpaces = await _bookingService.getBookableSpaces( + page: _currentPage, + size: _pageSize, + search: _currentSearch, + ); + final rooms = paginatedSpaces.data.map((space) { + return BookableSpaceModel( + uuid: space.uuid, + spaceName: space.spaceName, + virtualLocation: space.virtualLocation, + bookableConfig: BookableConfig( + uuid: space.bookableConfig.uuid, + daysAvailable: space.bookableConfig.daysAvailable, + startTime: space.bookableConfig.startTime, + endTime: space.bookableConfig.endTime, + active: space.bookableConfig.active, + points: space.bookableConfig.points, + ), + ); + }).toList(); + emit(state.copyWith( + allRooms: rooms, + displayedRooms: rooms, + isLoading: false, + hasMore: paginatedSpaces.hasNext, + totalPages: paginatedSpaces.totalPage, + currentPage: _currentPage, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'Search failed: ${e.toString()}', + )); + } + } + + void _onResetSearch( + ResetSearch event, + Emitter emit, + ) { + _currentSearch = ''; + add(LoadBookableSpaces()); + } + + void _onSelectRoom( + SelectRoomEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedRoomId: event.roomId)); + } + + @override + Future close() { + _searchDebounce?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart new file mode 100644 index 00000000..5dd1a3c8 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart @@ -0,0 +1,27 @@ +abstract class SidebarEvent {} + +class LoadBookableSpaces extends SidebarEvent {} + +class SelectRoomEvent extends SidebarEvent { + final String roomId; + + SelectRoomEvent(this.roomId); +} + +class SearchRoomsEvent extends SidebarEvent { + final String query; + + SearchRoomsEvent(this.query); +} + +// Add these to your sidebar_event.dart file +class LoadMoreSpaces extends SidebarEvent {} + +class ResetSearch extends SidebarEvent {} + +// Add to sidebar_event.dart +class ExecuteSearch extends SidebarEvent { + final String query; + + ExecuteSearch(this.query); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart new file mode 100644 index 00000000..7b35474e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class SidebarState { + final List allRooms; + final List displayedRooms; + final bool isLoading; + final bool isLoadingMore; + final String? errorMessage; + final String? selectedRoomId; + final bool hasMore; + final int totalPages; + final int currentPage; + + SidebarState({ + required this.allRooms, + required this.displayedRooms, + required this.isLoading, + this.isLoadingMore = false, + this.errorMessage, + this.selectedRoomId, + this.hasMore = true, + this.totalPages = 0, + this.currentPage = 1, + }); + + SidebarState copyWith({ + List? allRooms, + List? displayedRooms, + bool? isLoading, + bool? isLoadingMore, + String? errorMessage, + String? selectedRoomId, + bool? hasMore, + int? totalPages, + int? currentPage, + }) { + return SidebarState( + allRooms: allRooms ?? this.allRooms, + displayedRooms: displayedRooms ?? this.displayedRooms, + isLoading: isLoading ?? this.isLoading, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + errorMessage: errorMessage ?? this.errorMessage, + selectedRoomId: selectedRoomId ?? this.selectedRoomId, + hasMore: hasMore ?? this.hasMore, + totalPages: totalPages ?? this.totalPages, + currentPage: currentPage ?? this.currentPage, + ); + } +} diff --git a/lib/pages/access_management/model/password_model.dart b/lib/pages/access_management/booking_system/presentation/model/password_model.dart similarity index 100% rename from lib/pages/access_management/model/password_model.dart rename to lib/pages/access_management/booking_system/presentation/model/password_model.dart diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart similarity index 66% rename from lib/pages/access_management/booking_system/view/booking_page.dart rename to lib/pages/access_management/booking_system/presentation/view/booking_page.dart index 072dfcd7..522b0f25 100644 --- a/lib/pages/access_management/booking_system/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -2,14 +2,14 @@ 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/bloc/date_selection/date_selection_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/date_selection/date_selection_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/booking_sidebar.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/weekly_calendar_page.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'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; +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/weekly_calendar_page.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -81,29 +81,39 @@ class _BookingPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - children: [ - Expanded( - flex: 2, - child: BlocBuilder( - builder: (context, state) { - return BookingSidebar( - onRoomSelected: (id) { - context - .read() - .add(SelectBookableSpace(id)); - }, - ); - }, + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, ), - ), - Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return Container( - color: Colors.grey[300], - child: CustomCalendarPage( + ], + ), + 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); @@ -111,12 +121,12 @@ class _BookingPageState extends State { .read() .add(SelectDate(newDate)); }, - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), Expanded( @@ -157,34 +167,38 @@ class _BookingPageState extends State { borderRadius: BorderRadius.circular(10), boxShadow: const [ BoxShadow( - color: ColorsManager.textGray, - blurRadius: 12, - offset: Offset(0, 4), + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), ), ], ), child: Row( children: [ IconButton( + iconSize: 15, icon: const Icon(Icons.arrow_back_ios, - color: Colors.black), + color: ColorsManager.lightGrayColor), onPressed: () { context .read() .add(PreviousWeek()); }, ), + const SizedBox(width: 10), Text( _getMonthYearText(weekStart, weekEnd), style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, ), ), + const SizedBox(width: 10), IconButton( + iconSize: 15, icon: const Icon(Icons.arrow_forward_ios, - color: Colors.black), + color: ColorsManager.lightGrayColor), onPressed: () { context .read() @@ -199,12 +213,23 @@ class _BookingPageState extends State { ], ), Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return WeeklyCalendarPage( - weekStart: dateState.weekStart, - selectedDate: dateState.selectedDate, - eventController: _eventController, + child: BlocBuilder( + builder: (context, roomState) { + // NOTE: Assuming `SelectedBookableSpaceState` has a `selectedBookableSpace` property. + 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, + ); + }, ); }, ), diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart new file mode 100644 index 00000000..1796f331 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -0,0 +1,251 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.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/access_management/booking_system/presentation/view/widgets/room_list_item.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'; + +class BookingSidebar extends StatelessWidget { + final void Function(BookableSpaceModel) onRoomSelected; + + const BookingSidebar({ + super.key, + required this.onRoomSelected, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SidebarBloc(BookableSpacesService( + HTTPService(), + )) + ..add(LoadBookableSpaces()), + child: _SidebarContent(onRoomSelected: onRoomSelected), + ); + } +} + +class _SidebarContent extends StatefulWidget { + final void Function(BookableSpaceModel) onRoomSelected; + + const _SidebarContent({ + required this.onRoomSelected, + }); + + @override + State<_SidebarContent> createState() => __SidebarContentState(); +} + +class __SidebarContentState extends State<_SidebarContent> { + final TextEditingController searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + Timer? _searchDebounce; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + context.read().add(LoadMoreSpaces()); + } + } + + void _handleSearch(String value) { + // Cancel previous debounce timer + _searchDebounce?.cancel(); + + // Set up new debounce timer + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + context.read().add(SearchRoomsEvent(value)); + }); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state.currentPage == 1 && searchController.text.isNotEmpty) { + searchController.clear(); + } + }, + builder: (context, state) { + return Column( + children: [ + const _SidebarHeader(title: 'Spaces'), + Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, -2), + blurRadius: 4, + spreadRadius: 0, + ), + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, 2), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.counterBackgroundColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + Expanded( + child: TextField( + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: ColorsManager.blackColor, + ), + controller: searchController, + onChanged: _handleSearch, + decoration: InputDecoration( + hintText: 'Search', + suffixIcon: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + height: 20, + child: SvgPicture.asset( + Assets.searchIconUser, + color: ColorsManager.primaryTextColor, + ), + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none), + ), + ), + ), + if (searchController.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + context.read().add(ResetSearch()); + }, + ), + ], + ), + ), + ), + ), + ), + ), + if (state.isLoading) + const Expanded( + child: Center(child: CircularProgressIndicator()), + ) + else if (state.errorMessage != null) + Expanded( + child: Center(child: Text(state.errorMessage!)), + ) + else + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: + state.displayedRooms.length + (state.hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == state.displayedRooms.length) { + return _buildLoadMoreIndicator(state); + } + + final room = state.displayedRooms[index]; + return RoomListItem( + room: room, + isSelected: state.selectedRoomId == room.uuid, + onTap: () { + context + .read() + .add(SelectRoomEvent(room.uuid)); + widget.onRoomSelected(room); + }, + ); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildLoadMoreIndicator(SidebarState state) { + if (state.isLoadingMore) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } else if (state.hasMore) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: Text('Scroll to load more')), + ); + } else { + return const SizedBox.shrink(); + } + } +} + +class _SidebarHeader extends StatelessWidget { + final String title; + + const _SidebarHeader({ + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.primaryTextColor, + fontSize: 20, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart new file mode 100644 index 00000000..eb758311 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CustomCalendarPage extends StatefulWidget { + final DateTime selectedDate; + final Function(int day, int month, int year) onDateChanged; + + const CustomCalendarPage({ + super.key, + required this.selectedDate, + required this.onDateChanged, + }); + + @override + State createState() => _CustomCalendarPageState(); +} + +class _CustomCalendarPageState extends State { + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.selectedDate; + } + + @override + void didUpdateWidget(CustomCalendarPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedDate != oldWidget.selectedDate) { + setState(() { + _selectedDate = widget.selectedDate; + }); + } + } + + @override + Widget build(BuildContext context) { + final config = CalendarDatePicker2Config( + calendarType: CalendarDatePicker2Type.single, + selectedDayHighlightColor: const Color(0xFF3B82F6), + selectedDayTextStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + dayTextStyle: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + weekdayLabelTextStyle: const TextStyle( + color: ColorsManager.grey50, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + controlsTextStyle: const TextStyle( + color: Color(0xFF232D3A), + fontWeight: FontWeight.w400, + fontSize: 18, + ), + centerAlignModePicker: false, + disableMonthPicker: true, + firstDayOfWeek: 1, + weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], + ); + + return CalendarDatePicker2( + config: config, + value: [_selectedDate], + onValueChanged: (dates) { + final picked = dates.first; + if (picked != null) { + setState(() { + _selectedDate = picked; + }); + widget.onDateChanged(picked.day, picked.month, picked.year); + } + }, + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart similarity index 87% rename from lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart rename to lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart index afccafdb..c7c660c1 100644 --- a/lib/pages/access_management/booking_system/view/widgets/icon_text_button.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart @@ -20,13 +20,13 @@ class SvgTextButton extends StatelessWidget { required this.onPressed, this.backgroundColor = ColorsManager.circleRolesBackground, this.svgColor = const Color(0xFF496EFF), - this.labelColor = Colors.black87, + this.labelColor = Colors.black, this.borderRadius = 10.0, this.boxShadow = const [ BoxShadow( - color: ColorsManager.textGray, - blurRadius: 12, - offset: Offset(0, 4), + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), ), ], this.svgSize = 24.0, @@ -53,15 +53,14 @@ class SvgTextButton extends StatelessWidget { svgAsset, width: svgSize, height: svgSize, - color: svgColor, ), const SizedBox(width: 12), Text( label, style: TextStyle( color: labelColor, - fontSize: 16, - fontWeight: FontWeight.w500, + fontSize: 12, + fontWeight: FontWeight.w400, ), ), ], diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart new file mode 100644 index 00000000..4a4b608d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RoomListItem extends StatelessWidget { + final BookableSpaceModel room; + final bool isSelected; + final VoidCallback onTap; + + const RoomListItem({ + required this.room, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return RadioListTile( + value: room.uuid, + contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16), + groupValue: isSelected ? room.uuid : null, + visualDensity: const VisualDensity(vertical: -4), + onChanged: (value) => onTap(), + activeColor: ColorsManager.primaryColor, + title: Text( + room.spaceName, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + fontWeight: FontWeight.w700, + fontSize: 12), + ), + subtitle: Text( + room.virtualLocation, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 10, + fontWeight: FontWeight.w400, + color: ColorsManager.textGray, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart similarity index 90% rename from lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart rename to lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index c6eb2f40..d815ee2e 100644 --- a/lib/pages/access_management/booking_system/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -7,18 +7,32 @@ class WeeklyCalendarPage extends StatelessWidget { final DateTime weekStart; final DateTime selectedDate; final EventController eventController; + final String? startTime; + final String? endTime; const WeeklyCalendarPage({ super.key, required this.weekStart, required this.selectedDate, required this.eventController, + this.startTime, + this.endTime, }); @override Widget build(BuildContext context) { + final startHour = _parseHour(startTime, defaultValue: 0); + final endHour = _parseHour(endTime, defaultValue: 24); + if (endTime == null || endTime!.isEmpty) { + return const Center( + child: Text( + 'Please select a bookable space to view the calendar.', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + ); + } + final weekDays = _getWeekDays(weekStart); - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return LayoutBuilder( builder: (context, constraints) { @@ -27,9 +41,8 @@ class WeeklyCalendarPage extends StatelessWidget { const int totalDays = 7; final double dayColumnWidth = (calendarWidth - timeLineWidth) / totalDays; - final selectedDayIndex = (selectedDate != null) - ? weekDays.indexWhere((d) => isSameDay(d, selectedDate)) - : -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( @@ -38,8 +51,8 @@ class WeeklyCalendarPage extends StatelessWidget { key: ValueKey(weekStart), controller: eventController, initialDay: weekStart, - startHour: 7, - endHour: 18, + startHour: startHour - 1, + endHour: endHour, heightPerMinute: 1.1, showLiveTimeLineInAllDays: false, showVerticalLines: true, @@ -191,7 +204,6 @@ class WeeklyCalendarPage extends StatelessWidget { ); }, ), - // Highlight the selected day column if (selectedDayIndex >= 0) Positioned( left: timeLineWidth + dayColumnWidth * selectedDayIndex, @@ -234,3 +246,15 @@ class WeeklyCalendarPage extends StatelessWidget { bool isSameDay(DateTime d1, DateTime d2) { return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day; } + +int _parseHour(String? time, {required int defaultValue}) { + if (time == null || time.isEmpty || !time.contains(':')) { + return defaultValue; + } + try { + return int.parse(time.split(':')[0]); + } catch (e) { + // Optionally log the error, e.g., print('Error parsing time: $e'); + return defaultValue; + } +} diff --git a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart deleted file mode 100644 index 2849da5d..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/booking_sidebar.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_bloc.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/bloc/sidebar/sidebar_state.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/room_list_item.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; - -class BookingSidebar extends StatelessWidget { - final void Function(int) onRoomSelected; - - const BookingSidebar({ - super.key, - required this.onRoomSelected, - }); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SidebarBloc()..add(LoadRoomsEvent()), - child: _SidebarContent(onRoomSelected: onRoomSelected), - ); - } -} - -class _SidebarContent extends StatelessWidget { - final void Function(int) onRoomSelected; - - const _SidebarContent({ - required this.onRoomSelected, - }); - - @override - Widget build(BuildContext context) { - final TextEditingController searchController = TextEditingController(); - - return Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(3, 0), - blurRadius: 6, - spreadRadius: 0, - ), - ], - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - const _SidebarHeader(title: 'Spaces'), - Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, -2), - blurRadius: 4, - spreadRadius: 0, - ), - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, 2), - blurRadius: 4, - spreadRadius: 0, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - child: Container( - decoration: BoxDecoration( - color: ColorsManager.counterBackgroundColor, - borderRadius: BorderRadius.circular(8.0), - ), - child: TextField( - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.blackColor, - ), - controller: searchController, - onChanged: (value) { - context - .read() - .add(SearchRoomsEvent(value)); - }, - decoration: InputDecoration( - hintText: 'Search', - suffixIcon: Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 20, - height: 20, - child: SvgPicture.asset( - Assets.searchIconUser, - color: ColorsManager.primaryTextColor, - ), - ), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none), - ), - ), - ), - ), - ), - ), - ), - if (state.isLoading) - const Expanded( - child: Center(child: CircularProgressIndicator()), - ) - else if (state.errorMessage != null) - Expanded( - child: Center(child: Text(state.errorMessage!)), - ) - else - Expanded( - child: ListView.builder( - itemCount: state.displayedRooms.length, - itemBuilder: (context, index) { - final room = state.displayedRooms[index]; - return RoomListItem( - room: room, - isSelected: state.selectedRoomId == room.id, - onTap: () { - context - .read() - .add(SelectRoomEvent(room.id)); - onRoomSelected(room.id); - }, - ); - }, - ), - ), - ], - ); - }, - ), - ); - } -} - -class _SidebarHeader extends StatelessWidget { - final String title; - - const _SidebarHeader({ - required this.title, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.primaryTextColor, - fontSize: 20, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart deleted file mode 100644 index a523ae61..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/custom_calendar_page.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomCalendarPage extends StatefulWidget { - final DateTime selectedDate; - final Function(int day, int month, int year) onDateChanged; - - const CustomCalendarPage({ - super.key, - required this.selectedDate, - required this.onDateChanged, - }); - - @override - State createState() => _CustomCalendarPageState(); -} - -class _CustomCalendarPageState extends State { - late DateTime _selectedDate; - - @override - void initState() { - super.initState(); - _selectedDate = widget.selectedDate; - } - - @override - void didUpdateWidget(CustomCalendarPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.selectedDate != oldWidget.selectedDate) { - setState(() { - _selectedDate = widget.selectedDate; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - Expanded( - child: CalendarDatePicker( - initialDate: _selectedDate, - firstDate: DateTime(2000), - lastDate: DateTime(2100), - onDateChanged: (date) { - widget.onDateChanged(date.day, date.month, date.year); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart deleted file mode 100644 index 40f148ff..00000000 --- a/lib/pages/access_management/booking_system/view/widgets/room_list_item.dart +++ /dev/null @@ -1,51 +0,0 @@ - -import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/model/bookable_room.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class RoomListItem extends StatelessWidget { - final BookableRoom room; - final bool isSelected; - final VoidCallback onTap; - - const RoomListItem({ - required this.room, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - hoverColor: ColorsManager.primaryColor.withOpacity(0.05), - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - IgnorePointer( - child: Radio( - value: room.id, - groupValue: isSelected ? room.id : null, - onChanged: (value) {}, - activeColor: ColorsManager.primaryColor, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - room.name, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.textGray, - fontWeight: - isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index e035d252..4e31f23f 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart'; import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/services/access_mang_api.dart b/lib/services/access_mang_api.dart index a780a12b..db3cb554 100644 --- a/lib/services/access_mang_api.dart +++ b/lib/services/access_mang_api.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:syncrow_web/pages/access_management/model/password_model.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 40fca1fa..a36d1193 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -84,4 +84,5 @@ abstract class ColorsManager { static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); static const Color grey25 = Color(0xFFF9F9F9); + static const Color grey50 = Color(0xFF718096); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..f908db85 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,7 +46,8 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; - static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; + static const String getCommunityListv2 = + '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = @@ -138,4 +139,6 @@ abstract class ApiEndpoints { static const String assignDeviceToRoom = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; + + static const String getBookableSpaces = '/bookable-spaces'; } diff --git a/pubspec.yaml b/pubspec.yaml index cba59019..67bf5328 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,8 +63,8 @@ dependencies: bloc: ^9.0.0 geocoding: ^4.0.0 gauge_indicator: ^0.4.3 - # syncfusion_flutter_calendar: ^30.1.38 calendar_view: ^1.4.0 + calendar_date_picker2: ^2.0.1