diff --git a/assets/icons/add_button_Icon.svg b/assets/icons/add_button_Icon.svg new file mode 100644 index 00000000..f9b8eae7 --- /dev/null +++ b/assets/icons/add_button_Icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/back_button_icon.svg b/assets/icons/back_button_icon.svg new file mode 100644 index 00000000..5cc7b637 --- /dev/null +++ b/assets/icons/back_button_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/clock_icon.svg b/assets/icons/clock_icon.svg new file mode 100644 index 00000000..296aa862 --- /dev/null +++ b/assets/icons/clock_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/no_data_table.svg b/assets/icons/no_data_table.svg new file mode 100644 index 00000000..c97946a2 --- /dev/null +++ b/assets/icons/no_data_table.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/search_icon.svg b/assets/icons/search_icon.svg new file mode 100644 index 00000000..009efd91 --- /dev/null +++ b/assets/icons/search_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/4_sceen_switch.svg b/assets/images/4_sceen_switch.svg new file mode 100644 index 00000000..3765e137 --- /dev/null +++ b/assets/images/4_sceen_switch.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/6_sceen_switch.svg b/assets/images/6_sceen_switch.svg new file mode 100644 index 00000000..fef2291b --- /dev/null +++ b/assets/images/6_sceen_switch.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart index 3c2610db..5ed71116 100644 --- a/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart @@ -18,7 +18,7 @@ class RemoteBookableSpacesService implements BookableSystemService { }) async { try { final response = await _httpService.get( - path: ApiEndpoints.getBookableSpaces, + path: ApiEndpoints.bookableSpaces, queryParameters: { 'page': param.page, 'size': param.size, diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart index c7c660c1..9844e3d8 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; class SvgTextButton extends StatelessWidget { final String svgAsset; + final EdgeInsets? padding; final String label; final VoidCallback onPressed; final Color backgroundColor; @@ -12,16 +13,20 @@ class SvgTextButton extends StatelessWidget { final double borderRadius; final List boxShadow; final double svgSize; - + final double? fontSize; + final FontWeight? fontWeight; const SvgTextButton({ super.key, required this.svgAsset, + this.fontSize, + this.fontWeight, required this.label, required this.onPressed, this.backgroundColor = ColorsManager.circleRolesBackground, this.svgColor = const Color(0xFF496EFF), this.labelColor = Colors.black, this.borderRadius = 10.0, + this.padding, this.boxShadow = const [ BoxShadow( color: ColorsManager.lightGrayColor, @@ -40,7 +45,8 @@ class SvgTextButton extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadius), onTap: onPressed, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: padding ?? + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(borderRadius), diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/view/booking_page.dart new file mode 100644 index 00000000..49d10b50 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/booking_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; + +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingPage extends StatelessWidget { + final PageController pageController; + const BookingPage({ + super.key, + required this.pageController, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Container( + color: Colors.blueGrey[100], + child: const Center( + child: Text( + 'Side bar', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + )), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: SizedBox( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () { + pageController.jumpToPage(2); + }), + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}) + ], + ) + ], + ), + ), + )) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart b/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart new file mode 100644 index 00000000..af24439f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +class NonBookableSpacesDebouncerDecoratorService + implements NonBookableSpacesService { + final NonBookableSpacesService _delegate; + Timer? _debounce; + + NonBookableSpacesDebouncerDecoratorService(this._delegate); + + @override + Future> load( + NonBookableSpacesParams params) { + final completer = Completer>(); + + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () async { + try { + final result = await _delegate.load(params); + completer.complete(result); + } catch (e) { + completer.completeError(e); + } + }); + + return completer.future; + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart new file mode 100644 index 00000000..ceebf262 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 RemoteBookableSpacesService implements BookableSpacesService { + final HTTPService _httpService; + RemoteBookableSpacesService(this._httpService); + static const _defaultErrorMessage = 'Failed to load Bookable Spaces'; + @override + Future> load( + BookableSpacesParam param) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.bookableSpaces, + queryParameters: { + 'configured': true, + 'page': param.currentPage, + }, + expectedResponseModel: (json) { + final result = json as Map; + return PaginatedDataModel.fromJson( + result, + BookableSpacemodel.fromJsonList, + ); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart new file mode 100644 index 00000000..1db35a8e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 RemoteNonBookableSpaces implements NonBookableSpacesService { + final HTTPService _httpService; + + RemoteNonBookableSpaces(this._httpService); + + static const _defaultErrorMessage = 'Failed to load Spaces'; + + @override + Future> load( + NonBookableSpacesParams params) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.bookableSpaces, + queryParameters: { + 'configured': false, + 'page': params.currentPage, + 'search': params.searchedWords, + }, + expectedResponseModel: (json) { + final result = json as Map; + return PaginatedDataModel.fromJson( + result, + BookableSpacemodel.fromJsonList, + ); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart new file mode 100644 index 00000000..5d50fd55 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart @@ -0,0 +1,35 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_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 RemoteSendBookableSpaces implements SendBookableSpacesService { + final HTTPService _httpService; + RemoteSendBookableSpaces(this._httpService); + static const _defaultErrorMessage = 'Failed to load Spaces'; + @override + Future sendBookableSpacesToApi( + SendBookableSpacesToApiParams params) async { + try { + await _httpService.post( + path: ApiEndpoints.bookableSpaces, + body: params.toJson(), + expectedResponseModel: (p0) {}, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart new file mode 100644 index 00000000..1c0b1c8e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart @@ -0,0 +1,40 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_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 RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService { + final HTTPService _httpService; + RemoteUpdateBookableSpaceService(this._httpService); + static const _defaultErrorMessage = 'Failed to load Bookable Spaces'; + @override + Future update( + UpdateBookableSpaceParam updateParam) async { + try { + final response = await _httpService.put( + path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}', + body: updateParam.toJson(), + expectedResponseModel: (json) { + return BookableSpaceConfig.fromJson( + json['data'] as Map); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart new file mode 100644 index 00000000..1e28b686 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class BookableSpaceConfig { + final String configUuid; + final List bookableDays; + final TimeOfDay? bookingStartTime; + final TimeOfDay? bookingEndTime; + final int cost; + final bool availability; + BookableSpaceConfig({ + required this.configUuid, + required this.availability, + required this.bookableDays, + this.bookingEndTime, + this.bookingStartTime, + required this.cost, + }); + factory BookableSpaceConfig.zero() => BookableSpaceConfig( + configUuid: '', + bookableDays: [], + availability: false, + cost: -1, + ); + factory BookableSpaceConfig.fromJson(Map json) => + BookableSpaceConfig( + configUuid: json['uuid'] as String, + bookableDays: (json['daysAvailable'] as List).cast(), + availability: (json['active'] as bool?) ?? false, + bookingStartTime: parseTimeOfDay(json['startTime'] as String), + bookingEndTime: parseTimeOfDay(json['endTime'] as String), + cost: json['points'] as int, + ); + + static TimeOfDay parseTimeOfDay(String timeString) { + final parts = timeString.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + return TimeOfDay(hour: hour, minute: minute); + } + + bool get isValid => + bookableDays.isNotEmpty && + cost >= 0 && + bookingStartTime != null && + bookingEndTime != null; + + BookableSpaceConfig copyWith({ + List? bookableDays, + TimeOfDay? bookingStartTime, + TimeOfDay? bookingEndTime, + int? cost, + bool? availability, + }) { + return BookableSpaceConfig( + configUuid: configUuid, + availability: availability ?? this.availability, + bookableDays: bookableDays ?? this.bookableDays, + cost: cost ?? this.cost, + bookingEndTime: bookingEndTime ?? this.bookingEndTime, + bookingStartTime: bookingStartTime ?? this.bookingStartTime, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart new file mode 100644 index 00000000..70e700be --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart @@ -0,0 +1,58 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; + +class BookableSpacemodel { + final String spaceUuid; + final String spaceName; + final BookableSpaceConfig? spaceConfig; + final String spaceVirtualAddress; + + BookableSpacemodel({ + required this.spaceUuid, + required this.spaceName, + this.spaceConfig, + required this.spaceVirtualAddress, + }); + factory BookableSpacemodel.zero() => BookableSpacemodel( + spaceUuid: '', + spaceName: '', + spaceVirtualAddress: '', + ); + factory BookableSpacemodel.fromJson(Map json) => + BookableSpacemodel( + spaceUuid: json['uuid'] as String, + spaceName: json['spaceName'] as String, + spaceConfig: json['bookableConfig'] == null + ? BookableSpaceConfig.zero() + : BookableSpaceConfig.fromJson( + json['bookableConfig'] as Map), + spaceVirtualAddress: json['virtualLocation'] as String, + ); + + static List fromJsonList(List jsonList) => + jsonList + .map( + (e) => BookableSpacemodel.fromJson(e as Map), + ) + .toList(); + + bool get isValid => + spaceUuid.isNotEmpty && + spaceName.isNotEmpty && + spaceVirtualAddress.isNotEmpty && + spaceConfig != null && + spaceConfig!.isValid; + + BookableSpacemodel copyWith({ + String? spaceUuid, + String? spaceName, + BookableSpaceConfig? spaceConfig, + String? spaceVirtualAddress, + }) { + return BookableSpacemodel( + spaceUuid: spaceUuid ?? this.spaceUuid, + spaceName: spaceName ?? this.spaceName, + spaceConfig: spaceConfig ?? this.spaceConfig, + spaceVirtualAddress: spaceVirtualAddress ?? this.spaceVirtualAddress, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart new file mode 100644 index 00000000..6007424b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart @@ -0,0 +1,10 @@ +class BookableSpacesParam { + final int currentPage; + BookableSpacesParam({ + required this.currentPage, + }); + + Map toJson() => { + 'page': currentPage, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart new file mode 100644 index 00000000..a081ce04 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart @@ -0,0 +1,12 @@ +class NonBookableSpacesParams { +final int currentPage; +final String? searchedWords; + NonBookableSpacesParams({ + required this.currentPage, + this.searchedWords, + }); + + Map toJson() => { + 'page': currentPage, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart new file mode 100644 index 00000000..3e1251dd --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart @@ -0,0 +1,41 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class SendBookableSpacesToApiParams { + final List spaceUuids; + final List daysAvailable; + final String startTime; + final String endTime; + final int points; + SendBookableSpacesToApiParams({ + required this.spaceUuids, + required this.daysAvailable, + required this.startTime, + required this.endTime, + required this.points, + }); + + static SendBookableSpacesToApiParams fromBookableSpacesModel( + List bookableSpaces) { + return SendBookableSpacesToApiParams( + spaceUuids: bookableSpaces.map((space) => space.spaceUuid).toList(), + daysAvailable: bookableSpaces + .expand((space) => space.spaceConfig!.bookableDays) + .toSet() + .toList(), + startTime: formatTimeOfDayTo24HourString( + bookableSpaces.first.spaceConfig!.bookingStartTime!), + endTime: formatTimeOfDayTo24HourString( + bookableSpaces.first.spaceConfig!.bookingEndTime!), + points: bookableSpaces.first.spaceConfig!.cost, + ); + } + + Map toJson() => { + 'spaceUuids': spaceUuids, + 'daysAvailable': daysAvailable, + 'startTime': startTime, + 'endTime': endTime, + 'points': points + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart new file mode 100644 index 00000000..237f3442 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart @@ -0,0 +1,40 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class UpdateBookableSpaceParam { + final String spaceUuid; + final List? bookableDays; + final String? bookingStartTime; + final String? bookingEndTime; + final int? cost; + final bool? availability; + UpdateBookableSpaceParam({ + required this.spaceUuid, + this.bookingStartTime, + this.bookingEndTime, + this.bookableDays, + this.availability, + this.cost, + }); + factory UpdateBookableSpaceParam.fromBookableModel( + BookableSpacemodel bookableSpace) { + return UpdateBookableSpaceParam( + spaceUuid: bookableSpace.spaceUuid, + availability: bookableSpace.spaceConfig!.availability, + bookableDays: bookableSpace.spaceConfig!.bookableDays, + cost: bookableSpace.spaceConfig!.cost, + bookingStartTime: formatTimeOfDayTo24HourString( + bookableSpace.spaceConfig!.bookingStartTime!), + bookingEndTime: formatTimeOfDayTo24HourString( + bookableSpace.spaceConfig!.bookingEndTime!), + ); + } + + Map toJson() => { + if (bookableDays != null) 'daysAvailable': bookableDays, + if (bookingStartTime != null) 'startTime': bookingStartTime, + if (bookingEndTime != null) 'endTime': bookingEndTime, + if (cost != null) 'points': cost, + if (availability != null) 'active': availability, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart new file mode 100644 index 00000000..208589ad --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +abstract class BookableSpacesService { + Future> load( + BookableSpacesParam param); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart new file mode 100644 index 00000000..a5034bbf --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +abstract class NonBookableSpacesService { + Future> load( + NonBookableSpacesParams params); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart new file mode 100644 index 00000000..6b3f40d5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; + +abstract class SendBookableSpacesService{ + Future sendBookableSpacesToApi(SendBookableSpacesToApiParams params); +} \ No newline at end of file diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart new file mode 100644 index 00000000..509c69eb --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; + +abstract class UpdateBookableSpaceService { + Future update(UpdateBookableSpaceParam updateParam); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart new file mode 100644 index 00000000..c947cd69 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart @@ -0,0 +1,66 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'bookable_spaces_event.dart'; +part 'bookable_spaces_state.dart'; + +class BookableSpacesBloc + extends Bloc { + final BookableSpacesService bookableSpacesService; + BookableSpacesBloc(this.bookableSpacesService) + : super(BookableSpacesInitial()) { + on(_onLoadBookableSpaces); + on(_onInsertUpdatedSpaceEven); + } + + Future _onLoadBookableSpaces( + LoadBookableSpacesEvent event, Emitter emit) async { + emit(BookableSpacesLoading()); + try { + final bookableSpaces = await bookableSpacesService.load(event.param); + emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces)); + } on APIException catch (e) { + emit(BookableSpacesError(error: e.message)); + } catch (e) { + emit( + BookableSpacesError(error: e.toString()), + ); + } + } + + void _onInsertUpdatedSpaceEven( + InsertUpdatedSpaceEvent event, Emitter emit) { + emit(InsertingUpdatedSpaceState()); + + if (event.bookableSpace.spaceConfig!.configUuid == + event.updatedBookableSpaceConfig.configUuid) { + final index = event.bookableSpaces.data.indexWhere( + (element) => element.spaceUuid == event.bookableSpace.spaceUuid, + ); + + if (index != -1) { + final original = event.bookableSpaces.data[index]; + + final updatedConfig = original.spaceConfig!.copyWith( + availability: event.updatedBookableSpaceConfig.availability, + bookableDays: event.updatedBookableSpaceConfig.bookableDays, + bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime, + bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime, + cost: event.updatedBookableSpaceConfig.cost, + ); + + final updatedSpace = original.copyWith(spaceConfig: updatedConfig); + + event.bookableSpaces.data[index] = updatedSpace; + } + } + + emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces)); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart new file mode 100644 index 00000000..c73f08dc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart @@ -0,0 +1,24 @@ +part of 'bookable_spaces_bloc.dart'; + +sealed class BookableSpacesEvent extends Equatable { + const BookableSpacesEvent(); + + @override + List get props => []; +} + +class LoadBookableSpacesEvent extends BookableSpacesEvent { + final BookableSpacesParam param; + const LoadBookableSpacesEvent(this.param); +} + +class InsertUpdatedSpaceEvent extends BookableSpacesEvent { + final PaginatedDataModel bookableSpaces; + final BookableSpacemodel bookableSpace; + final BookableSpaceConfig updatedBookableSpaceConfig; + const InsertUpdatedSpaceEvent({ + required this.bookableSpaces, + required this.bookableSpace, + required this.updatedBookableSpaceConfig, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart new file mode 100644 index 00000000..d22f585a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart @@ -0,0 +1,28 @@ +part of 'bookable_spaces_bloc.dart'; + +sealed class BookableSpacesState extends Equatable { + const BookableSpacesState(); + + @override + List get props => []; +} + +final class BookableSpacesInitial extends BookableSpacesState {} + +final class BookableSpacesLoading extends BookableSpacesState {} + +final class BookableSpacesLoaded extends BookableSpacesState { + final PaginatedDataModel bookableSpacesList; + const BookableSpacesLoaded({ + required this.bookableSpacesList, + }); +} + +final class BookableSpacesError extends BookableSpacesState { + final String error; + const BookableSpacesError({ + required this.error, + }); +} + +class InsertingUpdatedSpaceState extends BookableSpacesState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart new file mode 100644 index 00000000..608a44ff --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart @@ -0,0 +1,65 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +part 'non_bookaable_spaces_event.dart'; +part 'non_bookaable_spaces_state.dart'; + +class NonBookableSpacesBloc + extends Bloc { + NonBookableSpacesService nonBookableSpacesService; + + NonBookableSpacesBloc(this.nonBookableSpacesService) + : super(NonBookableSpacesInitial()) { + on(_onCallInitStateEvent); + on(_onLoadUnBookableSpacesEvent); + } + + void _onCallInitStateEvent( + CallInitStateEvent event, Emitter emit) { + emit(NonBookableSpacesInitial()); + } + + Future _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event, + Emitter emit) async { + if (state is NonBookableSpacesLoaded) { + final currState = state as NonBookableSpacesLoaded; + try { + emit(NonBookableSpacesLoading( + lastNonBookableSpaces: currState.nonBookableSpaces)); + + final nonBookableSpacesList = await nonBookableSpacesService.load( + event.nonBookableSpacesParams, + ); + nonBookableSpacesList.data.addAll(currState.nonBookableSpaces.data); + + emit( + NonBookableSpacesLoaded( + nonBookableSpaces: nonBookableSpacesList, + ), + ); + } catch (e) { + emit( + NonBookableSpacesError(e.toString()), + ); + } + } else { + try { + emit(const NonBookableSpacesLoading()); + final nonBookableSpacesList = await nonBookableSpacesService.load( + event.nonBookableSpacesParams, + ); + emit( + NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList), + ); + } catch (e) { + emit( + NonBookableSpacesError(e.toString()), + ); + } + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart new file mode 100644 index 00000000..e185fae6 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart @@ -0,0 +1,17 @@ +part of 'non_bookaable_spaces_bloc.dart'; + +sealed class NonBookableSpacesEvent extends Equatable { + const NonBookableSpacesEvent(); + + @override + List get props => []; +} + +class CallInitStateEvent extends NonBookableSpacesEvent {} + +class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent { + final NonBookableSpacesParams nonBookableSpacesParams; + const LoadUnBookableSpacesEvent({ + required this.nonBookableSpacesParams, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart new file mode 100644 index 00000000..a5804f9d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart @@ -0,0 +1,32 @@ +part of 'non_bookaable_spaces_bloc.dart'; + +sealed class NonBookableSpacesState extends Equatable { + const NonBookableSpacesState(); + + @override + List get props => []; +} + +final class NonBookableSpacesInitial extends NonBookableSpacesState {} + +class NonBookableSpacesLoading extends NonBookableSpacesState { + final PaginatedDataModel? lastNonBookableSpaces; + const NonBookableSpacesLoading({ + this.lastNonBookableSpaces, + }); +} + +class NonBookableSpacesLoaded extends NonBookableSpacesState { + final PaginatedDataModel nonBookableSpaces; + final List selectedBookableSpaces; + const NonBookableSpacesLoaded({ + required this.nonBookableSpaces, + this.selectedBookableSpaces = const [], + }); +} + +class NonBookableSpacesError extends NonBookableSpacesState { + final String error; + const NonBookableSpacesError(this.error); +} + diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart new file mode 100644 index 00000000..a2993db9 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart @@ -0,0 +1,33 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart'; + +part 'send_bookable_spaces_event.dart'; +part 'send_bookable_spaces_state.dart'; + +class SendBookableSpacesBloc + extends Bloc { + SendBookableSpacesService sendBookableSpacesService; + SendBookableSpacesBloc( + this.sendBookableSpacesService, + ) : super(SendBookableSpacesInitial()) { + on(_onSendBookableSpacesToApi); + } + Future _onSendBookableSpacesToApi(SendBookableSpacesToApi event, + Emitter emit) async { + emit(SendBookableSpacesLoading()); + try { + await sendBookableSpacesService.sendBookableSpacesToApi( + SendBookableSpacesToApiParams.fromBookableSpacesModel( + event.selectedBookableSpaces), + ); + emit(SendBookableSpacesSuccess()); + } catch (e) { + emit( + SendBookableSpacesError(e.toString()), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart new file mode 100644 index 00000000..3e82ea0b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart @@ -0,0 +1,13 @@ +part of 'send_bookable_spaces_bloc.dart'; + +sealed class SendBookableSpacesEvent extends Equatable { + const SendBookableSpacesEvent(); + + @override + List get props => []; +} + +class SendBookableSpacesToApi extends SendBookableSpacesEvent { + final List selectedBookableSpaces; + const SendBookableSpacesToApi({required this.selectedBookableSpaces}); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart new file mode 100644 index 00000000..2fce5476 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart @@ -0,0 +1,19 @@ +part of 'send_bookable_spaces_bloc.dart'; + +sealed class SendBookableSpacesState extends Equatable { + const SendBookableSpacesState(); + + @override + List get props => []; +} + +final class SendBookableSpacesInitial extends SendBookableSpacesState {} + +class SendBookableSpacesLoading extends SendBookableSpacesState {} + +class SendBookableSpacesSuccess extends SendBookableSpacesState {} + +class SendBookableSpacesError extends SendBookableSpacesState { + final String error; + const SendBookableSpacesError(this.error); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart new file mode 100644 index 00000000..7ba4eb0a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart @@ -0,0 +1,132 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; + +part 'setup_bookable_spaces_event.dart'; +part 'setup_bookable_spaces_state.dart'; + +class SetupBookableSpacesBloc + extends Bloc { + NonBookableSpacesService nonBookableSpacesService; + + SetupBookableSpacesBloc(this.nonBookableSpacesService) + : super(const SetupBookableSpacesInitial(bookableSpaces: [])) { + on(_onAddToBookableSpaceEvent); + on(_onRemoveFromBookableSpaceEvent); + on(_onAddBookableDays); + on(_onChangeStartTimeEvent); + on(_onChangeEndTimeEvent); + on(_onChangeCostEvent); + + on(_onCheckConfigurValidityEvent); + on(_onEditModeSelected); + } + List get currentBookableSpaces { + return switch (state) { + AddNonBookableSpaceIntoBookableState(:final bookableSpaces) => + bookableSpaces, + RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) => + bookableSpaces, + SetupBookableSpacesInitial(:final bookableSpaces) => bookableSpaces, + InProgressState(:final bookableSpaces) => bookableSpaces, + ValidSaveButtonState(:final bookableSpaces) => bookableSpaces, + UnValidSaveButtonState(:final bookableSpaces) => bookableSpaces, + }; + } + + void _onAddToBookableSpaceEvent( + AddToBookableSpaceEvent event, + Emitter emit, + ) { + emit(InProgressState(bookableSpaces: state.bookableSpaces)); + final updatedSpaces = List.from(state.bookableSpaces); + + updatedSpaces.add(event.nonBookableSpace); + + emit(AddNonBookableSpaceIntoBookableState(bookableSpaces: updatedSpaces)); + } + + void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event, + Emitter emit) { + emit(InProgressState(bookableSpaces: state.bookableSpaces)); + state.bookableSpaces.remove(event.bookableSpace); + emit(RemoveBookableSpaceIntoNonBookableState( + bookableSpaces: state.bookableSpaces)); + } + + void _onAddBookableDays( + AddBookableDaysEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookableDays: event.bookableDays, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeStartTimeEvent( + ChangeStartTimeEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookingStartTime: event.startTime, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeEndTimeEvent( + ChangeEndTimeEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookingEndTime: event.endTime, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeCostEvent( + ChangeCostEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + cost: event.cost, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onCheckConfigurValidityEvent(CheckConfigurValidityEvent event, + Emitter emit) { + if (state.bookableSpaces.first.spaceConfig!.isValid) { + emit(ValidSaveButtonState( + bookableSpaces: state.bookableSpaces, + )); + } else { + emit(UnValidSaveButtonState( + bookableSpaces: state.bookableSpaces, + )); + } + } + + void _onEditModeSelected( + EditModeSelected event, + Emitter emit, + ) { + final updatedList = [event.editingBookableSpace]; + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedList)); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart new file mode 100644 index 00000000..8bc66f95 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart @@ -0,0 +1,59 @@ +part of 'setup_bookable_spaces_bloc.dart'; + +sealed class SetupBookableSpacesEvent extends Equatable { + const SetupBookableSpacesEvent(); + + @override + List get props => []; +} + +class AddToBookableSpaceEvent extends SetupBookableSpacesEvent { + final BookableSpacemodel nonBookableSpace; + const AddToBookableSpaceEvent({ + required this.nonBookableSpace, + }); +} + +class RemoveFromBookableSpaceEvent extends SetupBookableSpacesEvent { + final BookableSpacemodel bookableSpace; + const RemoveFromBookableSpaceEvent({ + required this.bookableSpace, + }); +} + +class AddBookableDaysEvent extends SetupBookableSpacesEvent { + final List bookableDays; + const AddBookableDaysEvent({ + required this.bookableDays, + }); +} + +class ChangeCostEvent extends SetupBookableSpacesEvent { + final int cost; + const ChangeCostEvent({ + required this.cost, + }); +} + +class ChangeStartTimeEvent extends SetupBookableSpacesEvent { + final TimeOfDay startTime; + const ChangeStartTimeEvent({ + required this.startTime, + }); +} + +class ChangeEndTimeEvent extends SetupBookableSpacesEvent { + final TimeOfDay endTime; + const ChangeEndTimeEvent({ + required this.endTime, + }); +} + +class CheckConfigurValidityEvent extends SetupBookableSpacesEvent {} + +class EditModeSelected extends SetupBookableSpacesEvent { + final BookableSpacemodel editingBookableSpace; + const EditModeSelected({ + required this.editingBookableSpace, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart new file mode 100644 index 00000000..5b04f075 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart @@ -0,0 +1,37 @@ +part of 'setup_bookable_spaces_bloc.dart'; + +sealed class SetupBookableSpacesState extends Equatable { + final List bookableSpaces; + const SetupBookableSpacesState({required this.bookableSpaces}); + TimeOfDay? get startTime => + bookableSpaces.first.spaceConfig!.bookingStartTime; + TimeOfDay? get endTime => bookableSpaces.first.spaceConfig!.bookingEndTime; + @override + List get props => []; +} + +final class SetupBookableSpacesInitial extends SetupBookableSpacesState { + const SetupBookableSpacesInitial({required super.bookableSpaces}); +} + +class AddNonBookableSpaceIntoBookableState extends SetupBookableSpacesState { + const AddNonBookableSpaceIntoBookableState({required super.bookableSpaces}); +} + +class InProgressState extends SetupBookableSpacesState { + const InProgressState({required super.bookableSpaces}); +} + +class RemoveBookableSpaceIntoNonBookableState extends SetupBookableSpacesState { + const RemoveBookableSpaceIntoNonBookableState({ + required super.bookableSpaces, + }); +} + +class ValidSaveButtonState extends SetupBookableSpacesState { + const ValidSaveButtonState({required super.bookableSpaces}); +} + +class UnValidSaveButtonState extends SetupBookableSpacesState { + const UnValidSaveButtonState({required super.bookableSpaces}); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart new file mode 100644 index 00000000..4258f5aa --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart @@ -0,0 +1,22 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'steps_state.dart'; + +class StepsCubit extends Cubit { + StepsCubit() : super(StepOneState()); + + void initDialogValue() { + emit(StepOneState()); + } + + void editValueInit() { + emit(StepTwoState()); + } + + void goToNextStep() { + if (state is StepOneState) { + emit(StepTwoState()); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart new file mode 100644 index 00000000..183fdb88 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart @@ -0,0 +1,12 @@ +part of 'steps_cubit.dart'; + +sealed class StepsState extends Equatable { + const StepsState(); + + @override + List get props => []; +} + +final class StepOneState extends StepsState {} + +final class StepTwoState extends StepsState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart new file mode 100644 index 00000000..09a11e5e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart @@ -0,0 +1,16 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'toggle_points_switch_state.dart'; + +class TogglePointsSwitchCubit extends Cubit { + TogglePointsSwitchCubit() : super(UnActivatePointsSwitch()); + + void activateSwitch() { + emit(ActivatePointsSwitch()); + } + + void unActivateSwitch() { + emit(UnActivatePointsSwitch()); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart new file mode 100644 index 00000000..be078660 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart @@ -0,0 +1,13 @@ +part of 'toggle_points_switch_cubit.dart'; + +sealed class TogglePointsSwitchState extends Equatable { + const TogglePointsSwitchState(); + + @override + List get props => []; +} + + +class ActivatePointsSwitch extends TogglePointsSwitchState {} + +class UnActivatePointsSwitch extends TogglePointsSwitchState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart new file mode 100644 index 00000000..39e42b5b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart @@ -0,0 +1,36 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'update_bookable_spaces_event.dart'; +part 'update_bookable_spaces_state.dart'; + +class UpdateBookableSpacesBloc + extends Bloc { + final UpdateBookableSpaceService updateBookableSpaceService; + UpdateBookableSpacesBloc(this.updateBookableSpaceService) + : super(UpdateBookableSpacesInitial()) { + on(_onUpdateBookableSpace); + } + + Future _onUpdateBookableSpace(UpdateBookableSpace event, + Emitter emit) async { + emit(UpdateBookableSpaceLoading(event.updatedParam.spaceUuid)); + try { + final updatedSpace = + await updateBookableSpaceService.update(event.updatedParam); + + emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace)); + event.onSuccess?.call(); + } on APIException catch (e) { + emit(UpdateBookableSpaceFailure(error: e.message)); + } catch (e) { + emit( + UpdateBookableSpaceFailure(error: e.toString()), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart new file mode 100644 index 00000000..1abcea0b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart @@ -0,0 +1,17 @@ +part of 'update_bookable_spaces_bloc.dart'; + +sealed class UpdateBookableSpaceEvent extends Equatable { + const UpdateBookableSpaceEvent(); + + @override + List get props => []; +} + +class UpdateBookableSpace extends UpdateBookableSpaceEvent { + final void Function()? onSuccess; + final UpdateBookableSpaceParam updatedParam; + const UpdateBookableSpace({ + required this.updatedParam, + this.onSuccess, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart new file mode 100644 index 00000000..e173dd21 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart @@ -0,0 +1,30 @@ +part of 'update_bookable_spaces_bloc.dart'; + +sealed class UpdateBookableSpacesState extends Equatable { + const UpdateBookableSpacesState(); + + @override + List get props => []; +} + +final class UpdateBookableSpacesInitial extends UpdateBookableSpacesState {} + +final class UpdateBookableSpaceLoading extends UpdateBookableSpacesState { + final String updatingSpaceUuid; + + const UpdateBookableSpaceLoading(this.updatingSpaceUuid); +} + +final class UpdateBookableSpaceSuccess extends UpdateBookableSpacesState { + final BookableSpaceConfig bookableSpaceConfig; + const UpdateBookableSpaceSuccess({ + required this.bookableSpaceConfig, + }); +} + +final class UpdateBookableSpaceFailure extends UpdateBookableSpacesState { + final String error; + const UpdateBookableSpaceFailure({ + required this.error, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart new file mode 100644 index 00000000..155049e4 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart'; + +import 'package:syncrow_web/services/api/http_service.dart'; + +class ManageBookableSpacesPage extends StatefulWidget { + final PageController pageController; + const ManageBookableSpacesPage({ + super.key, + required this.pageController, + }); + + @override + State createState() => + _ManageBookableSpacesPageState(); +} + +class _ManageBookableSpacesPageState extends State { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => BookableSpacesBloc( + RemoteBookableSpacesService(HTTPService()), + )..add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + ), + ), + ), + BlocProvider( + create: (context) => UpdateBookableSpacesBloc( + RemoteUpdateBookableSpaceService(HTTPService()), + ), + ) + ], + child: ManageBookableSpacesWidget( + pageController: widget.pageController, + ), + ); + } +} + +class ManageBookableSpacesWidget extends StatelessWidget { + final PageController pageController; + + const ManageBookableSpacesWidget({ + super.key, + required this.pageController, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 35, + ), + child: Column( + children: [ + Expanded( + flex: 10, + child: RowOfButtonsTitleWidget(pageController: pageController)), + const SizedBox( + height: 10, + ), + const Expanded( + flex: 85, + child: TableOfBookableSpacesWidget(), + ), + const SizedBox( + height: 15, + ), + const Expanded( + flex: 5, + child: PaginationButtonsWidget(), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart new file mode 100644 index 00000000..07be929f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SetupBookableSpacesDialog extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + SetupBookableSpacesDialog({ + super.key, + this.editingBookableSpace, + }); + + @override + State createState() => + _SetupBookableSpacesDialogState(); +} + +class _SetupBookableSpacesDialogState extends State { + final TextEditingController pointsController = TextEditingController(); + @override + void dispose() { + pointsController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: widget.editingBookableSpace == null + ? (context) => StepsCubit()..initDialogValue() + : (context) => StepsCubit()..editValueInit(), + ), + BlocProvider( + create: (context) => NonBookableSpacesBloc( + NonBookableSpacesDebouncerDecoratorService( + RemoteNonBookableSpaces(HTTPService()), + ), + )..add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: + NonBookableSpacesParams(currentPage: 1), + ), + ), + ), + BlocProvider( + create: widget.editingBookableSpace == null + ? (context) => SetupBookableSpacesBloc( + RemoteNonBookableSpaces(HTTPService())) + : (context) => SetupBookableSpacesBloc( + RemoteNonBookableSpaces(HTTPService())) + ..add( + EditModeSelected( + editingBookableSpace: widget.editingBookableSpace!), + ), + ), + BlocProvider( + create: (context) => SendBookableSpacesBloc( + RemoteSendBookableSpaces(HTTPService()), + ), + ) + ], + child: AlertDialog( + backgroundColor: ColorsManager.whiteColors, + contentPadding: EdgeInsets.zero, + title: Center( + child: Text( + 'Set Up a Bookable Spaces', + style: TextStyle( + fontWeight: FontWeight.w700, + color: ColorsManager.dialogBlueTitle, + fontSize: 15, + ), + ), + ), + content: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Divider(), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Expanded( + flex: 3, + child: StepperPartWidget(), + ), + const SizedBox( + height: 588, + child: VerticalDivider( + thickness: 0.5, + width: 1, + ), + ), + Expanded( + flex: 7, + child: DetailsStepsWidget( + pointsController: pointsController, + editingBookableSpace: widget.editingBookableSpace, + ), + ) + ], + ), + Builder(builder: (context) { + final stepsState = context.watch().state; + final bookableSpaces = + context.watch().state.bookableSpaces; + return stepsState is StepOneState + ? const NextFirstStepButton() + : SaveSecondStepButton( + pointsController: pointsController, + isEditingMode: widget.editingBookableSpace != null, + bookableSpaces: bookableSpaces, + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart new file mode 100644 index 00000000..738a2ecc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class BookableSpaceSwitchActivationWidget extends StatelessWidget { + final PaginatedDataModel bookableSpaces; + final BookableSpacemodel space; + const BookableSpaceSwitchActivationWidget({ + super.key, + required this.bookableSpaces, + required this.space, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Transform.scale( + scale: 0.7, + child: + BlocConsumer( + listener: (context, updateState) { + if (updateState is UpdateBookableSpaceSuccess) { + context.read().add( + InsertUpdatedSpaceEvent( + bookableSpaces: bookableSpaces, + bookableSpace: space, + updatedBookableSpaceConfig: + updateState.bookableSpaceConfig, + ), + ); + } + }, + builder: (context, updateState) { + final isLoading = updateState is UpdateBookableSpaceLoading && + updateState.updatingSpaceUuid == space.spaceUuid; + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + return Switch( + trackOutlineColor: WidgetStateProperty.resolveWith( + (Set states) { + return ColorsManager.whiteColors; + }), + value: space.spaceConfig!.availability, + activeTrackColor: ColorsManager.dialogBlueTitle, + inactiveTrackColor: ColorsManager.grayBorder, + thumbColor: WidgetStateProperty.resolveWith( + (Set states) { + return ColorsManager.whiteColors; + }), + onChanged: (value) { + context.read().add( + UpdateBookableSpace( + updatedParam: UpdateBookableSpaceParam( + spaceUuid: space.spaceUuid, + availability: value, + )), + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart new file mode 100644 index 00000000..c434fc3d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class BookingPeriodWidget extends StatelessWidget { + final BookableSpacemodel? editingBookableSpace; + const BookingPeriodWidget({ + super.key, + this.editingBookableSpace, + }); + + @override + Widget build(BuildContext context) { + final state = context.watch().state; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + const Text('Booking Period'), + ], + ), + const SizedBox(height: 5), + Container( + width: 230, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: ColorsManager.circleRolesBackground, + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 4, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues(alpha: 0.15), + ) + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TimePickerWidget( + title: editingBookableSpace?.spaceConfig?.bookingStartTime + ?.format(context) ?? + 'Start Time', + onTimePicked: (pickedStartTime) { + if (pickedStartTime == null) return; + + if (state.endTime != null && + isEndTimeAfterStartTime( + pickedStartTime, state.endTime!)) { + _showInvalidSnackBar( + context, "You can't choose Start Time after End Time"); + return; + } + + context.read().add( + ChangeStartTimeEvent(startTime: pickedStartTime), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 10), + const Icon( + Icons.arrow_right_alt, + color: ColorsManager.grayColor, + size: 13, + ), + TimePickerWidget( + title: editingBookableSpace?.spaceConfig?.bookingEndTime + ?.format(context) ?? + 'End Time', + onTimePicked: (pickedEndTime) { + if (pickedEndTime == null) return; + + if (state.startTime != null && + isEndTimeAfterStartTime( + state.startTime!, pickedEndTime)) { + _showInvalidSnackBar( + context, "You can't choose End Time before Start Time"); + return; + } + + context.read().add( + ChangeEndTimeEvent(endTime: pickedEndTime), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 15), + Container( + width: 30, + height: 32, + alignment: Alignment.center, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + child: SvgPicture.asset( + Assets.clockIcon, + height: 18, + color: ColorsManager.blackColor.withValues(alpha: 0.4), + ), + ), + ], + ), + ), + ], + ); + } + + void _showInvalidSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart new file mode 100644 index 00000000..0e13370f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonsDividerBottomDialogWidget extends StatelessWidget { + final String title; + final void Function()? onNextPressed; + final void Function() onCancelPressed; + const ButtonsDividerBottomDialogWidget({ + super.key, + required this.title, + required this.onNextPressed, + required this.onCancelPressed, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider( + thickness: 0.5, + height: 1, + ), + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + onTap: onCancelPressed, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.blackColor), + ), + ), + ), + ), + Expanded( + child: + BlocConsumer( + listener: (context, nonBookableState) { + if (nonBookableState is NonBookableSpacesInitial) { + context.pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Operation Done Successfully', + style: TextStyle(color: ColorsManager.activeGreen), + ), + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + ), + ); + } else if (nonBookableState is NonBookableSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + nonBookableState.error, + style: const TextStyle(color: ColorsManager.red), + ), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + builder: (context, nonBookableState) { + return TextButton( + onPressed: onNextPressed, + child: Text( + title, + ), + ); + }, + ), + ) + ], + ) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart new file mode 100644 index 00000000..862b11fa --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CheckBoxSpaceWidget extends StatelessWidget { + final BookableSpacemodel nonBookableSpace; + + const CheckBoxSpaceWidget({ + super.key, + required this.nonBookableSpace, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + BlocBuilder( + builder: (context, state) { + final isChecked = switch (state) { + AddNonBookableSpaceIntoBookableState( + bookableSpaces: final spaces + ) => + spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid), + RemoveBookableSpaceIntoNonBookableState( + bookableSpaces: final spaces + ) => + spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid), + _ => false, + }; + + return CustomCheckboxWidget( + value: isChecked, + onChanged: (value) { + final bloc = context.read(); + + if (value ?? false) { + bloc.add(AddToBookableSpaceEvent( + nonBookableSpace: nonBookableSpace, + )); + } else { + bloc.add(RemoveFromBookableSpaceEvent( + bookableSpace: nonBookableSpace, + )); + } + }, + ); + }, + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + nonBookableSpace.spaceName, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 12, + color: ColorsManager.titleGray, + ), + ), + Text( + nonBookableSpace.spaceVirtualAddress, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 10, + color: ColorsManager.titleGray, + ), + ), + ], + )), + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart new file mode 100644 index 00000000..5525d6a4 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ColumnTitleWidget extends StatelessWidget { + final bool isFirst; + final bool isLast; + final String title; + const ColumnTitleWidget({ + super.key, + required this.title, + required this.isFirst, + required this.isLast, + }); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 10), + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: isFirst + ? const BorderRadius.only( + topLeft: Radius.circular(12), + ) + : isLast + ? const BorderRadius.only( + topRight: Radius.circular(12), + ) + : null, + ), + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 12, + ), + )); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart new file mode 100644 index 00000000..35e600e8 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CustomCheckboxWidget extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + final double? outHeight; + final double? outWidth; + final double? iconSize; + const CustomCheckboxWidget({ + super.key, + required this.value, + required this.onChanged, + this.outWidth, + this.outHeight, + this.iconSize, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + width: outWidth ?? 17, + height: outHeight ?? 17, + decoration: BoxDecoration( + color: value ? Colors.white : ColorsManager.checkBoxFillColor, + border: value + ? Border.all(color: ColorsManager.secondaryColor, width: 1) + : Border.all(color: ColorsManager.checkBoxBorderGray, width: 1), + borderRadius: BorderRadius.circular(4), + ), + child: value + ? Center( + child: Container( + width: outWidth != null ? outWidth! - 4 : 13, + height: outHeight != null ? outHeight! - 4 : 13, + decoration: BoxDecoration( + color: ColorsManager.secondaryColor, + borderRadius: BorderRadius.circular(2), + ), + child: const Icon( + Icons.check, + size: 12, + color: Colors.white, + ), + ), + ) + : null, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart new file mode 100644 index 00000000..a72104fe --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart @@ -0,0 +1,55 @@ +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CustomDataTable extends StatelessWidget { + final List columnsTitles; + final List Function(T item) cellsWidgets; + final List items; + + const CustomDataTable({ + super.key, + required this.items, + required this.cellsWidgets, + required this.columnsTitles, + }); + + @override + Widget build(BuildContext context) { + return DataTable2( + dividerThickness: 0.5, + columnSpacing: 2, + horizontalMargin: 0, + empty: SvgPicture.asset(Assets.emptyDataTable), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: ColorsManager.textGray, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + columns: columnsTitles.asMap().entries.map((entry) { + final index = entry.key; + final title = entry.value; + + return DataColumn( + label: ColumnTitleWidget( + title: title, + isFirst: index == 0, + isLast: index == columnsTitles.length - 1, + ), + ); + }).toList(), + rows: items.map((item) { + return DataRow(cells: cellsWidgets(item)); + }).toList(), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart new file mode 100644 index 00000000..bbcc5293 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart'; + +class DetailsStepsWidget extends StatelessWidget { + final TextEditingController pointsController; + final BookableSpacemodel? editingBookableSpace; + const DetailsStepsWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), + child: BlocBuilder(builder: (context, state) { + return switch (state) { + StepOneState() => const SpacesStepDetailsWidget(), + StepTwoState() => StepTwoDetailsWidget( + pointsController: pointsController, + editingBookableSpace: editingBookableSpace, + ), + }; + }), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart new file mode 100644 index 00000000..2a014f75 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.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 EditBookableSpaceButtonWidget extends StatelessWidget { + final BookableSpacemodel? space; + const EditBookableSpaceButtonWidget({ + super.key, + required this.space, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 3, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues( + alpha: 0.3, + ), + ) + ], + ), + child: ElevatedButton( + onPressed: () { + final bookableBloc = context.read(); + + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: bookableBloc, + ), + BlocProvider( + create: (context) => UpdateBookableSpacesBloc( + RemoteUpdateBookableSpaceService(HTTPService()), + ), + ), + ], + child: SetupBookableSpacesDialog( + editingBookableSpace: space, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(45, 30), + elevation: 0, + ), + child: SvgPicture.asset( + Assets.settings, + height: 13, + color: ColorsManager.blue1, + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart new file mode 100644 index 00000000..b41fe106 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PaginationButtonsWidget extends StatelessWidget { + const PaginationButtonsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BookableSpacesLoaded) { + final totalPages = state.bookableSpacesList.totalPages; + final currentPage = state.bookableSpacesList.page; + + List paginationItems = []; + + if (currentPage > 2) { + paginationItems.add( + _buildArrowButton( + label: '«', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage - 2), + ), + ); + }, + ), + ); + } + + if (currentPage > 1) { + paginationItems.add( + _buildArrowButton( + label: '<', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage - 1), + ), + ); + }, + ), + ); + } + + for (int i = 1; i <= totalPages; i++) { + paginationItems.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GestureDetector( + onTap: () { + if (i != currentPage) { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: i), + ), + ); + } + }, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + decoration: BoxDecoration( + color: i == currentPage + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ColorsManager.lightGrayBorderColor, + )), + child: Text( + '$i', + style: TextStyle( + color: i == currentPage ? Colors.white : Colors.black, + fontWeight: i == currentPage + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ), + ), + ); + } + + if (currentPage < totalPages) { + paginationItems.add( + _buildArrowButton( + label: '>', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage + 1), + ), + ); + }, + ), + ); + } + + if (currentPage + 1 < totalPages) { + paginationItems.add( + _buildArrowButton( + label: '»', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage + 2), + ), + ); + }, + ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: paginationItems, + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } + + Widget _buildArrowButton( + {required String label, required VoidCallback onTap}) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GestureDetector( + onTap: onTap, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ColorsManager.lightGrayBorderColor, + )), + child: Text( + label, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart new file mode 100644 index 00000000..a6e206fc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart'; + +class TableOfBookableSpacesWidget extends StatelessWidget { + const TableOfBookableSpacesWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BookableSpacesLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is BookableSpacesError) { + return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(state.error), + const SizedBox( + height: 5, + ), + ElevatedButton( + onPressed: () => context + .read() + .add(LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + )), + child: const Text('Try Again')) + ]); + } else if (state is BookableSpacesLoaded) { + return CustomDataTable( + items: state.bookableSpacesList.data, + cellsWidgets: (space) => [ + DataCell( + DataCellWidget( + title: space.spaceName, + ), + ), + DataCell(DataCellWidget( + title: space.spaceVirtualAddress, + )), + DataCell(Container( + padding: const EdgeInsetsGeometry.only(left: 10), + width: 200, + child: Wrap( + spacing: 4, + children: space.spaceConfig!.bookableDays + .map( + (day) => DataCellWidget(title: day), + ) + .toList(), + ), + )), + DataCell( + DataCellWidget( + title: space.spaceConfig!.bookingStartTime!.format(context), + ), + ), + DataCell( + DataCellWidget( + title: space.spaceConfig!.bookingEndTime!.format(context), + ), + ), + DataCell( + DataCellWidget( + title: '${space.spaceConfig!.cost} Points', + ), + ), + DataCell(BookableSpaceSwitchActivationWidget( + bookableSpaces: state.bookableSpacesList, + space: space, + )), + DataCell(EditBookableSpaceButtonWidget( + space: space, + )), + ], + columnsTitles: const [ + 'Space', + 'Space Virtual Address', + 'Bookable Days', + 'Booking Start Time', + 'Booking End Time', + 'Cost', + 'Availability', + 'Settings', + ], + ); + } else { + return const SizedBox(); + } + }, + ); + } +} + +class DataCellWidget extends StatelessWidget { + final String title; + const DataCellWidget({ + super.key, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsGeometry.only(left: 10), + child: Text( + title, + style: const TextStyle(fontSize: 11), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart new file mode 100644 index 00000000..65a647a5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class RowOfButtonsTitleWidget extends StatelessWidget { + const RowOfButtonsTitleWidget({ + super.key, + required this.pageController, + }); + + final PageController pageController; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsGeometry.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 3, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues( + alpha: 0.3, + ), + ) + ], + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(50, 40), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: SvgPicture.asset( + Assets.backButtonIcon, + height: 15, + ), + onPressed: () { + pageController.jumpToPage(1); + }), + ), + const SizedBox( + width: 10, + ), + Text( + 'Manage Bookable Spaces', + style: TextStyle( + fontSize: 18, + color: ColorsManager.vividBlue.withValues( + alpha: 0.7, + ), + fontWeight: FontWeight.w700), + ) + ], + ), + SvgTextButton( + padding: const EdgeInsets.all(10), + svgSize: 15, + fontSize: 10, + fontWeight: FontWeight.bold, + svgAsset: Assets.addButtonIcon, + label: 'Set Up a Bookable Spaces', + onPressed: () { + final bloc = context.read(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: SetupBookableSpacesDialog(), + ), + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart new file mode 100644 index 00000000..12e6bb2e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart'; + +class NextFirstStepButton extends StatelessWidget { + const NextFirstStepButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + SetupBookableSpacesInitial() => ButtonsDividerBottomDialogWidget( + title: 'Next', + onNextPressed: null, + onCancelPressed: () => context.pop(), + ), + AddNonBookableSpaceIntoBookableState(:final bookableSpaces) || + RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) => + ButtonsDividerBottomDialogWidget( + title: 'Next', + onNextPressed: bookableSpaces.isEmpty + ? null + : () { + context.read().goToNextStep(); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + onCancelPressed: () => context.pop(), + ), + _ => const SizedBox(), + }; + }, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart new file mode 100644 index 00000000..3df5343d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PointsPartWidget extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + final TextEditingController pointsController; + + const PointsPartWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + + @override + State createState() => _PointsPartWidgetState(); +} + +class _PointsPartWidgetState extends State { + @override + void initState() { + super.initState(); + + if (widget.editingBookableSpace != null) { + widget.pointsController.text = + widget.editingBookableSpace!.spaceConfig!.cost.toString(); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, switchState) { + final isSwitchOn = switchState is ActivatePointsSwitch; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (isSwitchOn) + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ) + else + const SizedBox(width: 11), + const Text('Points/hrs'), + ], + ), + Transform.scale( + scale: 0.7, + child: Switch( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + trackOutlineColor: + WidgetStateProperty.all(ColorsManager.whiteColors), + activeTrackColor: ColorsManager.dialogBlueTitle, + inactiveTrackColor: ColorsManager.lightGrayBorderColor, + thumbColor: + WidgetStateProperty.all(ColorsManager.whiteColors), + value: isSwitchOn, + onChanged: (value) { + final toggleCubit = + context.read(); + final bloc = context.read(); + + final updatedCost = value ? -1 : 0; + + if (value) { + toggleCubit.activateSwitch(); + } else { + toggleCubit.unActivateSwitch(); + widget.pointsController.clear(); + } + bloc.add(ChangeCostEvent(cost: updatedCost)); + bloc.add(CheckConfigurValidityEvent()); + }, + ), + ), + ], + ), + const SizedBox(height: 5), + if (isSwitchOn) + SearchUnbookableSpacesWidget( + title: 'Ex: 0', + topPadding: 0, + blur: 1, + raduis: 10, + height: 34, + controller: widget.pointsController, + suffix: const SizedBox(), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (_) { + final updatedCost = + int.tryParse(widget.pointsController.text) ?? 0; + context + .read() + .add(ChangeCostEvent(cost: updatedCost)); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ) + else + const SizedBox(), + ], + ); + }, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart new file mode 100644 index 00000000..de35a38a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart'; + +class SaveSecondStepButton extends StatelessWidget { + final TextEditingController pointsController; + final bool isEditingMode; + final List bookableSpaces; + + const SaveSecondStepButton({ + super.key, + required this.pointsController, + required this.isEditingMode, + required this.bookableSpaces, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is SendBookableSpacesSuccess) { + context.read().add(CallInitStateEvent()); + } else if (state is SendBookableSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error)), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + return ButtonsDividerBottomDialogWidget( + title: 'Save', + onNextPressed: state is UnValidSaveButtonState + ? null + : () { + if (bookableSpaces.any((e) => e.isValid)) { + if (isEditingMode) { + callEditLogic(context); + } else { + context.read().add( + SendBookableSpacesToApi( + selectedBookableSpaces: bookableSpaces, + ), + ); + } + } + }, + onCancelPressed: () => context.pop(), + ); + }, + ), + ); + } + + void callEditLogic(BuildContext context) { + print(bookableSpaces.first.spaceConfig!.cost); + if (bookableSpaces.isNotEmpty) { + context.read().add( + UpdateBookableSpace( + onSuccess: () => context + .read() + .add(CallInitStateEvent()), + updatedParam: UpdateBookableSpaceParam.fromBookableModel( + bookableSpaces.first, + ), + ), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart new file mode 100644 index 00000000..ba7cea57 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SearchUnbookableSpacesWidget extends StatelessWidget { + final String title; + final Widget? suffix; + final double? height; + final double? width; + final double? blur; + final double? raduis; + final double? topPadding; + final TextEditingController? controller; + final List? inputFormatters; + final void Function(String)? onChanged; + const SearchUnbookableSpacesWidget({ + required this.title, + this.controller, + this.blur, + this.onChanged, + this.suffix, + this.height, + this.width, + this.topPadding, + this.raduis, + this.inputFormatters, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width ?? 480, + height: height ?? 40, + padding: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(raduis ?? 15), + boxShadow: [ + BoxShadow( + color: + ColorsManager.shadowOfSearchTextfield.withValues(alpha: 0.15), + offset: Offset.zero, + blurRadius: blur ?? 5, + spreadRadius: 0, + ), + ], + ), + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + onChanged: onChanged, + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: topPadding ?? 5, + horizontal: 15, + ), + hintText: title, + hintStyle: const TextStyle(color: ColorsManager.hintTextGrey), + border: InputBorder.none, + suffixIcon: Padding( + padding: const EdgeInsets.all(10), + child: suffix ?? + SvgPicture.asset( + Assets.searchIcon, + height: 12, + ), + ), + ), + style: const TextStyle( + fontSize: 14, + color: ColorsManager.hintTextGrey, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart new file mode 100644 index 00000000..60ba2441 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesStepDetailsWidget extends StatefulWidget { + const SpacesStepDetailsWidget({ + super.key, + }); + + @override + State createState() => + _SpacesStepDetailsWidgetState(); +} + +class _SpacesStepDetailsWidgetState extends State { + ScrollController scrollController = ScrollController(); + int currentPage = 1; + String? currentSearchTerm; + bool isLoadingMore = false; + + @override + void initState() { + super.initState(); + + scrollController.addListener(() { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 100) { + final state = context.read().state; + if (state is NonBookableSpacesLoaded && + state.nonBookableSpaces.hasNext && + !isLoadingMore) { + isLoadingMore = true; + currentPage++; + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + } + } + }); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select Space', + style: TextStyle( + fontWeight: FontWeight.w700, + color: ColorsManager.blackColor, + ), + ), + const SizedBox( + height: 20, + ), + Container( + width: 450, + height: 480, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: ColorsManager.shadowOfDetailsContainer, + offset: Offset.zero, + blurRadius: 5, + ), + ], + ), + child: Column( + children: [ + Container( + width: 520, + height: 70, + padding: + const EdgeInsets.symmetric(vertical: 15, horizontal: 20), + decoration: const BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: SearchUnbookableSpacesWidget( + title: 'Search', + onChanged: (p0) { + currentSearchTerm = p0; + currentPage = 1; + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + }, + ), + ), + Expanded( + child: + BlocConsumer( + listener: (context, state) { + if (state is NonBookableSpacesLoaded) { + isLoadingMore = false; + } + }, + builder: (context, state) { + return switch (state) { + NonBookableSpacesError(error: final error) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(error), + const SizedBox(height: 5), + ElevatedButton( + onPressed: () { + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: + NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + }, + child: const Text('Try Again'), + ), + ], + ), + NonBookableSpacesLoading(lastNonBookableSpaces: null) => + const Center(child: CircularProgressIndicator()), + NonBookableSpacesLoading( + lastNonBookableSpaces: final spaces + ) => + UnbookableListWidget( + scrollController: scrollController, + nonBookableSpaces: spaces!, + ), + NonBookableSpacesLoaded( + nonBookableSpaces: final spaces + ) => + UnbookableListWidget( + scrollController: scrollController, + nonBookableSpaces: spaces, + ), + _ => const SizedBox(), + }; + }, + ), + ) + ], + ), + ) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart new file mode 100644 index 00000000..a1db9694 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart'; + +class StepTwoDetailsWidget extends StatelessWidget { + final TextEditingController pointsController; + final BookableSpacemodel? editingBookableSpace; + const StepTwoDetailsWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + @override + Widget build(BuildContext context) { + return SizedBox( + width: 450, + height: 480, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WeekDaysCheckboxRow( + editingBookableSpace: editingBookableSpace, + ), + const SizedBox( + height: 30, + ), + BookingPeriodWidget( + editingBookableSpace: editingBookableSpace, + ), + const SizedBox( + height: 20, + ), + BlocProvider( + create: editingBookableSpace == null + ? (context) => TogglePointsSwitchCubit()..activateSwitch() + : editingBookableSpace!.spaceConfig!.cost == 0 + ? (context) => TogglePointsSwitchCubit()..unActivateSwitch() + : (context) => TogglePointsSwitchCubit()..activateSwitch(), + child: PointsPartWidget( + pointsController: pointsController, + editingBookableSpace: editingBookableSpace), + ) + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart new file mode 100644 index 00000000..89f9d1c5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class StepperPartWidget extends StatelessWidget { + const StepperPartWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(20), + padding: const EdgeInsetsGeometry.only(left: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is StepOneState) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + const CircleTitleStepperWidget( + title: 'Space', + ), + Container( + padding: const EdgeInsets.only(left: 3), + alignment: Alignment.centerLeft, + height: 40, + child: const VerticalDivider( + width: 8, + )), + const CircleTitleStepperWidget( + title: 'Settings', + titleColor: ColorsManager.softGray, + circleColor: ColorsManager.whiteColors, + borderColor: ColorsManager.textGray, + ) + ], + ); + } else if (state is StepTwoState) { + return Column( + children: [ + const SizedBox( + height: 10, + ), + const CircleTitleStepperWidget( + title: 'Space', + titleColor: ColorsManager.softGray, + cicleIcon: Icon( + Icons.check, + color: ColorsManager.whiteColors, + size: 12, + ), + circleColor: ColorsManager.trueIconGreen, + radius: 15, + borderColor: ColorsManager.trueIconGreen, + ), + Container( + padding: const EdgeInsets.only(left: 3), + alignment: Alignment.centerLeft, + height: 40, + child: const VerticalDivider( + width: 8, + )), + const CircleTitleStepperWidget( + title: 'Settings', + ) + ], + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} + +class CircleTitleStepperWidget extends StatelessWidget { + final double? radius; + final Widget? cicleIcon; + final Color? circleColor; + final Color? borderColor; + final Color? titleColor; + final String title; + const CircleTitleStepperWidget({ + super.key, + required this.title, + this.circleColor, + this.borderColor, + this.cicleIcon, + this.titleColor, + this.radius, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: radius ?? 15, + height: radius ?? 15, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: circleColor ?? ColorsManager.blue1, + border: Border.all(color: borderColor ?? ColorsManager.blue1)), + child: cicleIcon, + ), + const SizedBox( + width: 10, + ), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.w700, + color: titleColor ?? ColorsManager.blackColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart new file mode 100644 index 00000000..c215079e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; + +import 'package:syncrow_web/utils/color_manager.dart'; + +class TimePickerWidget extends StatefulWidget { + final String title; + TimePickerWidget({ + super.key, + required this.onTimePicked, + required this.title, + }); + late final SetupBookableSpacesBloc setupBookableSpacesBloc; + final void Function(TimeOfDay? timePicked) onTimePicked; + @override + State createState() => _TimePickerWidgetState(); +} + +class _TimePickerWidgetState extends State { + TimeOfDay? timePicked; + @override + void initState() { + widget.setupBookableSpacesBloc = context.read(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () async { + final tempTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, + ), + ), + child: child!, + ); + }, + ); + + if (tempTime == null) return; + + widget.onTimePicked(tempTime); + timePicked = tempTime; + + widget.setupBookableSpacesBloc.add(CheckConfigurValidityEvent()); + + setState(() {}); + }, + child: Container( + height: 32, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + timePicked == null ? widget.title : timePicked!.format(context), + style: TextStyle( + color: ColorsManager.blackColor.withValues(alpha: 0.4), + fontSize: 12, + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart new file mode 100644 index 00000000..2aa09b24 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +class UnbookableListWidget extends StatelessWidget { + final PaginatedDataModel nonBookableSpaces; + const UnbookableListWidget({ + super.key, + required this.scrollController, + required this.nonBookableSpaces, + }); + + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return Container( + width: 490, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20), + ), + ), + padding: const EdgeInsets.only(top: 10, left: 20, bottom: 5), + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox( + height: 5, + ), + controller: scrollController, + itemCount: nonBookableSpaces.data.length, + itemBuilder: (context, index) { + if (index < nonBookableSpaces.data.length) { + return CheckBoxSpaceWidget( + nonBookableSpace: nonBookableSpaces.data[index], + + ); + } else { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Center(child: CircularProgressIndicator()), + ); + } + }, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart new file mode 100644 index 00000000..c01dc714 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart'; + +class WeekDaysCheckboxRow extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + const WeekDaysCheckboxRow({ + super.key, + this.editingBookableSpace, + }); + + @override + State createState() => _WeekDaysCheckboxRowState(); +} + +class _WeekDaysCheckboxRowState extends State { + final Map _daysChecked = { + 'Mon': false, + 'Tue': false, + 'Wed': false, + 'Thu': false, + 'Fri': false, + 'Sat': false, + 'Sun': false, + }; + + @override + void initState() { + super.initState(); + + final existingDays = + widget.editingBookableSpace?.spaceConfig?.bookableDays ?? []; + + for (var day in _daysChecked.keys) { + if (existingDays.contains(day)) { + _daysChecked[day] = true; + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + const Text('Days'), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _daysChecked.entries.map((entry) { + return Expanded( + child: Row( + children: [ + CustomCheckboxWidget( + outHeight: 16, + outWidth: 16, + value: entry.value, + onChanged: (newValue) { + setState(() { + _daysChecked[entry.key] = newValue ?? false; + }); + + final selectedDays = _daysChecked.entries + .where((e) => e.value) + .map((e) => e.key) + .toList(); + + context.read().add( + AddBookableDaysEvent(bookableDays: selectedDays), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + entry.key, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index 4e31f23f..a6f8a603 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,7 +2,9 @@ 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/presentation/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart' hide BookingPage; 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'; @@ -71,9 +73,14 @@ class _AccessManagementPageState extends State scaffoldBody: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), - children: const [ - AccessOverviewContent(), - BookingPage(), + children: [ + const AccessOverviewContent(), + BookingPage( + pageController: _pageController, + ), + ManageBookableSpacesPage( + pageController: _pageController, + ), ], ), ), diff --git a/lib/pages/device_managment/gateway/view/gateway_view.dart b/lib/pages/device_managment/gateway/view/gateway_view.dart index d674e4d8..372f190a 100644 --- a/lib/pages/device_managment/gateway/view/gateway_view.dart +++ b/lib/pages/device_managment/gateway/view/gateway_view.dart @@ -100,6 +100,7 @@ class _DeviceItem extends StatelessWidget { @override Widget build(BuildContext context) { + return DeviceControlsContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart index ac35975d..80fcee90 100644 --- a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -40,4 +40,22 @@ class PaginatedDataModel extends Equatable { totalItems, totalPages, ]; + + PaginatedDataModel copyWith({ + List? data, + int? page, + int? size, + bool? hasNext, + int? totalItems, + int? totalPages, + }) { + return PaginatedDataModel( + data: data ?? this.data, + page: page ?? this.page, + size: size ?? this.size, + hasNext: hasNext ?? this.hasNext, + totalItems: totalItems ?? this.totalItems, + totalPages: totalPages ?? this.totalPages, + ); + } } diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 55e47de1..47a67c36 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -9,6 +9,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget { class _SpaceManagementPageState extends State { late final CommunitiesBloc communitiesBloc; + late final HTTPService _httpService; @override void initState() { + _httpService = HTTPService(); communitiesBloc = CommunitiesBloc( communitiesService: DebouncedCommunitiesService( - RemoteCommunitiesService(HTTPService()), + RemoteCommunitiesService(_httpService), ), )..add(const LoadCommunities(LoadCommunitiesParam())); - super.initState(); } @@ -50,13 +53,18 @@ class _SpaceManagementPageState extends State { BlocProvider( create: (context) => SpaceDetailsBloc( UniqueSpaceDetailsSpacesDecoratorService( - RemoteSpaceDetailsService(httpService: HTTPService()), + RemoteSpaceDetailsService(httpService: _httpService), ), ), ), BlocProvider( create: (context) => ProductsBloc( - RemoteProductsService(HTTPService()), + RemoteProductsService(_httpService), + ), + ), + BlocProvider( + create: (context) => ReorderSpacesBloc( + RemoteReorderSpacesService(_httpService), ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 692ffc0a..6614aa88 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -11,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -164,6 +166,16 @@ class _CommunityStructureCanvasState extends State context.read().add( CommunitiesUpdateCommunity(newCommunity), ); + + context.read().add( + ReorderSpacesEvent( + ReorderSpacesParam( + communityUuid: widget.community.uuid, + parentSpaceUuid: data.parent?.uuid ?? '', + spaces: children, + ), + ), + ); } void _onSpaceTapped(SpaceModel? space) { @@ -245,6 +257,13 @@ class _CommunityStructureCanvasState extends State final levelXOffset = {}; _calculateLayout(community.spaces, 0, levelXOffset); + const horizontalCanvasPadding = 100.0; + final originalPositions = Map.of(_positions); + _positions.clear(); + for (final entry in originalPositions.entries) { + _positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0); + } + final selectedSpace = widget.selectedSpace; final highlightedUuids = {}; if (selectedSpace != null) { @@ -262,7 +281,7 @@ class _CommunityStructureCanvasState extends State community: widget.community, ); - final createButtonX = levelXOffset[0] ?? 0.0; + final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding; const createButtonY = 0.0; widgets.add( @@ -294,10 +313,12 @@ class _CommunityStructureCanvasState extends State CommunityModel? community, SpaceModel? parent, }) { + const targetWidth = 40.0; + final padding = (_horizontalSpacing - targetWidth) / 2; if (spaces.isNotEmpty) { final firstChildPos = _positions[spaces.first.uuid]!; final targetPos = Offset( - firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dx - padding - targetWidth, firstChildPos.dy, ); widgets.add(_buildDropTarget(parent, community, 0, targetPos)); @@ -379,7 +400,7 @@ class _CommunityStructureCanvasState extends State ); final targetPos = Offset( - position.dx + cardWidth + (_horizontalSpacing / 4) - 20, + position.dx + cardWidth + padding, position.dy, ); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); @@ -414,24 +435,33 @@ class _CommunityStructureCanvasState extends State child: DragTarget( builder: (context, candidateData, rejectedData) { if (_draggedData == null) { - return const SizedBox(); + return const SizedBox.shrink(); } - final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && - _draggedData?.community == null) || - (_draggedData?.community?.uuid == community?.uuid && - _draggedData?.parent == null); + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid && + _draggedData!.community == null) || + (_draggedData!.community?.uuid == community?.uuid && + _draggedData!.parent == null); - if (!isTargetForDragged) { - return const SizedBox(); + if (!isSameParent) { + return const SizedBox.shrink(); } - return Container( + final oldIndex = + children.indexWhere((s) => s.uuid == _draggedData!.space.uuid); + if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) { + return const SizedBox.shrink(); + } + + return AnimatedContainer( + duration: const Duration(milliseconds: 150), width: 40, + alignment: Alignment.center, height: _cardHeight, decoration: BoxDecoration( color: context.theme.colorScheme.primary.withValues( - alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + alpha: candidateData.isNotEmpty ? 0.9 : 0.3, ), borderRadius: BorderRadius.circular(8), ), @@ -454,6 +484,9 @@ class _CommunityStructureCanvasState extends State final oldIndex = children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == -1) { + return true; + } if (oldIndex == index || oldIndex == index - 1) { return false; } @@ -481,7 +514,7 @@ class _CommunityStructureCanvasState extends State child: SizedBox( width: context.screenWidth * 5, height: context.screenHeight * 5, - child: Stack(children: treeWidgets), + child: Stack(clipBehavior: Clip.none, children: treeWidgets), ), ), ); diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart new file mode 100644 index 00000000..c2494c09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart @@ -0,0 +1,58 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_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'; + +final class RemoteReorderSpacesService implements ReorderSpacesService { + RemoteReorderSpacesService(this._httpClient); + + final HTTPService _httpClient; + + @override + Future reorderSpaces(ReorderSpacesParam param) async { + try { + await _httpClient.post( + path: await _makeUrl(param), + body: param.toJson(), + expectedResponseModel: (json) => json, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + throw APIException(_getErrorMessageFromBody(message)); + } catch (e) { + throw APIException(e.toString()); + } + } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) return 'Failed to delete space'; + final error = body['error'] as Map?; + final errorMessage = error?['message'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl(ReorderSpacesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + final communityUuid = param.communityUuid; + + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + if (param.parentSpaceUuid.isEmpty) { + throw APIException('Parent Space UUID is not set'); + } + + return ApiEndpoints.reorderSpaces + .replaceAll('{projectUuid}', projectUuid) + .replaceAll('{communityUuid}', communityUuid) + .replaceAll('{parentSpaceUuid}', param.parentSpaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart new file mode 100644 index 00000000..05316006 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class ReorderSpacesParam extends Equatable { + const ReorderSpacesParam({ + required this.communityUuid, + required this.parentSpaceUuid, + required this.spaces, + }); + + final String communityUuid; + final String parentSpaceUuid; + final List spaces; + + @override + List get props => [spaces, communityUuid, parentSpaceUuid]; + + Map toJson() => { + 'spacesUuids': spaces.map((space) => space.uuid).toList(), + }; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart new file mode 100644 index 00000000..46811fae --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; + +abstract interface class ReorderSpacesService { + Future reorderSpaces(ReorderSpacesParam param); +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart new file mode 100644 index 00000000..ecd15898 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'reorder_spaces_event.dart'; +part 'reorder_spaces_state.dart'; + +class ReorderSpacesBloc extends Bloc { + ReorderSpacesBloc( + this._reorderSpacesService, + ) : super(const ReorderSpacesInitial()) { + on(_onReorderSpacesEvent); + } + + final ReorderSpacesService _reorderSpacesService; + + Future _onReorderSpacesEvent( + ReorderSpacesEvent event, + Emitter emit, + ) async { + emit(const ReorderSpacesLoading()); + try { + await _reorderSpacesService.reorderSpaces(event.param); + emit(const ReorderSpacesSuccess()); + } on APIException catch (e) { + emit(ReorderSpacesFailure(e.message)); + } catch (e) { + emit(ReorderSpacesFailure(e.toString())); + } finally { + emit(const ReorderSpacesInitial()); + } + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart new file mode 100644 index 00000000..8cccb4f1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart @@ -0,0 +1,10 @@ +part of 'reorder_spaces_bloc.dart'; + +final class ReorderSpacesEvent extends Equatable { + const ReorderSpacesEvent(this.param); + + final ReorderSpacesParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart new file mode 100644 index 00000000..d237d93c --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart @@ -0,0 +1,29 @@ +part of 'reorder_spaces_bloc.dart'; + +sealed class ReorderSpacesState extends Equatable { + const ReorderSpacesState(); + + @override + List get props => []; +} + +final class ReorderSpacesInitial extends ReorderSpacesState { + const ReorderSpacesInitial(); +} + +final class ReorderSpacesLoading extends ReorderSpacesState { + const ReorderSpacesLoading(); +} + +final class ReorderSpacesSuccess extends ReorderSpacesState { + const ReorderSpacesSuccess(); +} + +final class ReorderSpacesFailure extends ReorderSpacesState { + const ReorderSpacesFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index cf65dbb6..4c8fec4f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -103,7 +103,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { ).then((resultSpace) { if (resultSpace != null) { if (context.mounted) { - context.read().add(UpdateSpaceDetails(resultSpace)); + context + .read() + .add(UpdateSpaceDetails(resultSpace)); } } }); @@ -133,6 +135,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { DeviceType.ThreeTouch => Assets.gangSwitch, DeviceType.NCPS => Assets.sensors, DeviceType.PC => Assets.powerClamp, + DeviceType.fourSceen => Assets.fourSceenSwitch, + DeviceType.sixSceen => Assets.sixSceenSwitch, + DeviceType.SOS => Assets.sos, DeviceType.Other => Assets.blackLogo, null => Assets.blackLogo, }; diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index c5ee3259..b963f6b4 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -181,7 +181,7 @@ class VisitorPasswordBloc effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { - // END TIME VALIDATION + if (effectiveTimeTimeStamp != null && selectedTimestamp < effectiveTimeTimeStamp!) { await showDialog( diff --git a/lib/pages/visitor_password/model/device_model.dart b/lib/pages/visitor_password/model/device_model.dart index 75d00350..99f84393 100644 --- a/lib/pages/visitor_password/model/device_model.dart +++ b/lib/pages/visitor_password/model/device_model.dart @@ -84,6 +84,14 @@ class DeviceModel { tempIcon = Assets.curtainIcon; } else if (type == DeviceType.Curtain) { tempIcon = Assets.curtainIcon; + } else if (type == DeviceType.fourSceen) { + tempIcon = Assets.fourSceenSwitch; + } else if (type == DeviceType.sixSceen) { + tempIcon = Assets.sixSceenSwitch; + } else if (type == DeviceType.SOS) { + tempIcon = Assets.sos; + } else if (type == DeviceType.NCPS) { + tempIcon = Assets.presenceSensor; } else { tempIcon = Assets.blackLogo; } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index c1d791c8..75c2d671 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -38,6 +38,7 @@ abstract class ColorsManager { static const Color lightGrayColor = Color(0xB2999999); static const Color grayBorder = Color(0xFFCFCFCF); static const Color textGray = Color(0xffD5D5D5); + static const Color titleGray = Color(0xB2999999); static const Color btnColor = Color(0xFF00008B); static const Color blueColor = Color(0xFF0036E6); static const Color boxColor = Color(0xFFF5F6F7); @@ -64,6 +65,7 @@ abstract class ColorsManager { static const Color circleRolesBackground = Color(0xFFF8F8F8); static const Color activeGreen = Color(0xFF99FF93); static const Color activeGreenText = Color(0xFF008905); + static const Color trueIconGreen = Color(0xFFBBEC6C); static const Color disabledPink = Color(0xFFFF9395); static const Color disabledRedText = Color(0xFF890002); static const Color invitedOrange = Color(0xFFFFE193); @@ -86,4 +88,9 @@ abstract class ColorsManager { static const Color grey50 = Color(0xFF718096); static const Color red100 = Color(0xFFFE0202); static const Color grey800 = Color(0xffF8F8F8); + static const Color shadowOfSearchTextfield = Color(0x26000000); + static const Color hintTextGrey = Colors.grey; + static const Color shadowOfDetailsContainer = Color(0x40000000); + static const Color checkBoxBorderGray = Color(0xffD0D0D0); + static const Color timePickerColor = Color(0xff000000); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index a53582be..cf7ffbe5 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -41,6 +41,8 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{spaceId}'; static const String getSpaceHierarchy = '/projects/{projectId}/communities/{communityId}/spaces'; + static const String reorderSpaces = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{parentSpaceUuid}/spaces/order'; // Community Module static const String createCommunity = '/projects/{projectId}/communities'; @@ -139,6 +141,12 @@ abstract class ApiEndpoints { '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; - static const String getBookableSpaces = '/bookable-spaces'; - static const String getBookings = '/bookings?month={mm}-{yyyy}&space={space}'; + + + ////booking System + static const String bookableSpaces = '/bookable-spaces'; + static const String getCalendarEvents = '/api'; + static const String getBookings = + '/bookings?month={mm}%2F{yyyy}&space={space}'; + } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index b7ad15b5..f4413cda 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -4,6 +4,8 @@ class Assets { static const String webBackground = 'assets/images/web_Background.svg'; static const String webBackgroundPng = 'assets/images/web_Background.png'; static const String blackLogo = 'assets/images/black-logo.png'; + static const String fourSceenSwitch = 'assets/images/4_sceen_switch.svg'; + static const String sixSceenSwitch = 'assets/images/6_sceen_switch.svg'; static const String logo = 'assets/images/Logo.svg'; static const String logoHorizontal = 'assets/images/logo_horizontal.png'; static const String vector = 'assets/images/Vector.png'; @@ -18,6 +20,9 @@ class Assets { 'assets/images/Password_invisible.svg'; static const String visiblePassword = 'assets/images/password_visible.svg'; static const String accessIcon = 'assets/images/access_icon.svg'; + static const String addButtonIcon = 'assets/icons/add_button_Icon.svg'; + static const String backButtonIcon = 'assets/icons/back_button_icon.svg'; + static const String emptyDataTable = 'assets/icons/no_data_table.svg'; static const String spaseManagementIcon = 'assets/images/spase_management_icon.svg'; static const String devicesIcon = 'assets/images/devices_icon.svg'; @@ -205,6 +210,7 @@ class Assets { //assets/icons/ac_lock.svg static const String acLock = 'assets/icons/ac_lock.svg'; + static const String clockIcon = 'assets/icons/clock_icon.svg'; //assets/icons/ac_schedule.svg static const String acSchedule = 'assets/icons/ac_schedule.svg'; diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 947e63aa..85b5d5fa 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -21,6 +21,9 @@ enum DeviceType { NCPS, DoorSensor, PC, + fourSceen, + sixSceen, + SOS, Other, } /* @@ -63,4 +66,7 @@ Map devicesTypesMap = { 'WL': DeviceType.WaterLeak, 'NCPS': DeviceType.NCPS, 'PC': DeviceType.PC, + '4S': DeviceType.fourSceen, + '6S': DeviceType.sixSceen, + 'SOS': DeviceType.SOS, }; diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index c8d8e2af..5df37e20 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -1,6 +1,21 @@ +import 'package:flutter/material.dart'; + class StringUtils { static String capitalizeFirstLetter(String text) { if (text.isEmpty) return text; return text[0].toUpperCase() + text.substring(1); } } + +bool isEndTimeAfterStartTime(TimeOfDay start, TimeOfDay end) { + final startMinutes = start.hour * 60 + start.minute; + final endMinutes = end.hour * 60 + end.minute; + + return endMinutes <= startMinutes; +} + +String formatTimeOfDayTo24HourString(TimeOfDay time) { + final hour = time.hour.toString().padLeft(2, '0'); + final minute = time.minute.toString().padLeft(2, '0'); + return '$hour:$minute'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 67bf5328..6736e1bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,7 +44,7 @@ dependencies: flutter_secure_storage: ^9.2.2 shared_preferences: ^2.3.0 dropdown_button2: ^2.3.9 - data_table_2: ^2.5.15 + data_table_2: ^2.6.0 go_router: intl: ^0.20.2 dropdown_search: ^6.0.2