[FE] Manage Bookable Spaces Tab (#355)

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1693](https://syncrow.atlassian.net/browse/SP-1693)
[SP-1694](https://syncrow.atlassian.net/browse/SP-1694)

## Description

all about unbookable spaces is Ready

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1693]:
https://syncrow.atlassian.net/browse/SP-1693?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1694]:
https://syncrow.atlassian.net/browse/SP-1694?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
Rafeek-khoudare
2025-07-23 09:33:46 +03:00
committed by GitHub
74 changed files with 3455 additions and 11 deletions

View File

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9795_9381)">
<path d="M9.21875 13.5149V10.7805H6.48438C6.05286 10.7805 5.70312 10.4308 5.70312 9.99924C5.70312 9.56787 6.05286 9.21799 6.48438 9.21799H9.21875V6.48361C9.21875 6.05225 9.56848 5.70236 10 5.70236C10.4315 5.70236 10.7812 6.05225 10.7812 6.48361V9.21799H13.5156C13.9471 9.21799 14.2969 9.56787 14.2969 9.99924C14.2969 10.4308 13.9471 10.7805 13.5156 10.7805H10.7812V13.5149C10.7812 13.9464 10.4315 14.2961 10 14.2961C9.56848 14.2961 9.21875 13.9464 9.21875 13.5149ZM17.0711 2.92892C15.1823 1.04019 12.6711 0 10 0C7.32895 0 4.81766 1.04019 2.92892 2.92892C1.04019 4.81766 0 7.32895 0 10C0 12.6711 1.04019 15.1823 2.92892 17.0711C4.81766 18.9598 7.32895 20 10 20C11.8286 20 13.6179 19.5016 15.1743 18.5588C15.5434 18.3353 15.6613 17.8549 15.4378 17.486C15.2142 17.1169 14.7337 16.9989 14.3648 17.2224C13.0525 18.0173 11.5431 18.4375 10 18.4375C5.3476 18.4375 1.5625 14.6524 1.5625 10C1.5625 5.3476 5.3476 1.5625 10 1.5625C14.6524 1.5625 18.4375 5.3476 18.4375 10C18.4375 11.6637 17.9428 13.2829 17.0068 14.6831C16.767 15.0417 16.8634 15.5269 17.2221 15.7668C17.5807 16.0065 18.0659 15.91 18.3058 15.5515C19.4141 13.8936 20 11.9739 20 10C20 7.32895 18.9598 4.81766 17.0711 2.92892Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_9795_9381">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C15.514 20 19.9998 15.514 19.9998 9.99995C19.9998 4.48604 15.514 0 10 0C4.48613 0 0.000183105 4.48604 0.000183105 9.99995C0.000183105 15.514 4.48613 20 10 20ZM10 1.36892C14.7591 1.36892 18.6309 5.24077 18.631 9.99995C18.631 14.7591 14.7592 18.631 10 18.6311C5.24095 18.631 1.36919 14.7591 1.36919 9.99986C1.36919 5.24086 5.24095 1.36892 10 1.36892Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M8.65713 14.2828C8.92444 14.55 9.35784 14.5499 9.62505 14.2828C9.89245 14.0154 9.89245 13.5821 9.62496 13.3147L6.99481 10.6846L14.6112 10.6839C14.9892 10.6838 15.2956 10.3775 15.2956 9.99926C15.2955 9.62126 14.9891 9.31499 14.6111 9.31499L6.99444 9.31572L9.62523 6.68511C9.89254 6.41781 9.89254 5.98432 9.62523 5.7171C9.49154 5.5835 9.3164 5.5166 9.14118 5.5166C8.96605 5.5166 8.79092 5.5835 8.65722 5.71701L4.85811 9.51604C4.7297 9.64435 4.65761 9.81838 4.65761 9.99999C4.6577 10.1816 4.7298 10.3555 4.8582 10.4841L8.65713 14.2828Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.9999 0C4.48595 0 0 4.48586 0 9.99971C0 15.514 4.48595 20 9.9999 20C15.5138 20 19.9996 15.5139 19.9996 9.99971C19.9996 4.48586 15.5138 0 9.9999 0ZM9.9999 18.5665C5.27638 18.5665 1.43349 14.7234 1.43349 9.99971C1.43349 5.27628 5.27638 1.43349 9.9999 1.43349C14.7233 1.43349 18.5661 5.27628 18.5661 9.99971C18.5661 14.7234 14.7233 18.5665 9.9999 18.5665Z" fill="#D5D5D5"/>
<path d="M15.1416 9.83211H10.4423V4.69526C10.4423 4.29943 10.1215 3.97852 9.72553 3.97852C9.3297 3.97852 9.00879 4.29943 9.00879 4.69526V10.5489C9.00879 10.9447 9.3297 11.2656 9.72553 11.2656H15.1416C15.5376 11.2656 15.8584 10.9447 15.8584 10.5489C15.8584 10.153 15.5375 9.83211 15.1416 9.83211Z" fill="#D5D5D5"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.89852 11.08C-0.632759 8.54864 -0.632708 4.42983 1.89852 1.8985C4.42985 -0.632833 8.54861 -0.632833 11.0799 1.8985C13.2274 4.04599 13.5526 7.23986 12.0565 9.73392C12.0565 9.73392 11.949 9.91422 12.0942 10.0593C12.9222 10.8872 15.4065 13.3716 15.4065 13.3716C16.0658 14.0308 16.2227 14.9527 15.638 15.5374L15.5374 15.638C14.9527 16.2227 14.0308 16.0658 13.3716 15.4065C13.3716 15.4065 10.8926 12.9275 10.0662 12.1012C9.91414 11.9491 9.73389 12.0566 9.73389 12.0566C7.23988 13.5526 4.04601 13.2275 1.89852 11.08ZM9.88131 9.88133C11.7517 8.01094 11.7516 4.96763 9.88125 3.09724C8.01086 1.22689 4.96755 1.22684 3.09721 3.09724C1.22681 4.96758 1.22681 8.01094 3.09721 9.88133C4.9676 11.7516 8.01086 11.7516 9.88131 9.88133Z" fill="#999999" fill-opacity="0.7"/>
<path d="M9.46701 6.10386C9.55406 6.10386 9.64256 6.08674 9.72786 6.05072C10.0686 5.9065 10.228 5.51333 10.0838 5.17253C9.17738 3.03045 6.69729 2.0252 4.55526 2.93164C4.21451 3.07586 4.05509 3.46903 4.1993 3.80983C4.34357 4.15063 4.73664 4.30995 5.07755 4.16579C6.539 3.54737 8.23126 4.23326 8.84962 5.69471C8.95781 5.95031 9.20594 6.10386 9.46701 6.10386Z" fill="#999999" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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,

View File

@ -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> 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),

View File

@ -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: () {})
],
)
],
),
),
))
],
);
}
}

View File

@ -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<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params) {
final completer = Completer<PaginatedDataModel<BookableSpacemodel>>();
_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;
}
}

View File

@ -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<PaginatedDataModel<BookableSpacemodel>> 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<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -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<PaginatedDataModel<BookableSpacemodel>> 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<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -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<void> 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<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -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<BookableSpaceConfig> 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<String, dynamic>);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class BookableSpaceConfig {
final String configUuid;
final List<String> 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<String, dynamic> json) =>
BookableSpaceConfig(
configUuid: json['uuid'] as String,
bookableDays: (json['daysAvailable'] as List).cast<String>(),
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<String>? 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,
);
}
}

View File

@ -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<String, dynamic> json) =>
BookableSpacemodel(
spaceUuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
spaceConfig: json['bookableConfig'] == null
? BookableSpaceConfig.zero()
: BookableSpaceConfig.fromJson(
json['bookableConfig'] as Map<String, dynamic>),
spaceVirtualAddress: json['virtualLocation'] as String,
);
static List<BookableSpacemodel> fromJsonList(List<dynamic> jsonList) =>
jsonList
.map(
(e) => BookableSpacemodel.fromJson(e as Map<String, dynamic>),
)
.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,
);
}
}

View File

@ -0,0 +1,10 @@
class BookableSpacesParam {
final int currentPage;
BookableSpacesParam({
required this.currentPage,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -0,0 +1,12 @@
class NonBookableSpacesParams {
final int currentPage;
final String? searchedWords;
NonBookableSpacesParams({
required this.currentPage,
this.searchedWords,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -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<String> spaceUuids;
final List<String> 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<BookableSpacemodel> 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<String, dynamic> toJson() => {
'spaceUuids': spaceUuids,
'daysAvailable': daysAvailable,
'startTime': startTime,
'endTime': endTime,
'points': points
};
}

View File

@ -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<String>? 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<String, dynamic> 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,
};
}

View File

@ -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<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParam param);
}

View File

@ -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<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params);
}

View File

@ -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<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
}

View File

@ -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<BookableSpaceConfig> update(UpdateBookableSpaceParam updateParam);
}

View File

@ -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<BookableSpacesEvent, BookableSpacesState> {
final BookableSpacesService bookableSpacesService;
BookableSpacesBloc(this.bookableSpacesService)
: super(BookableSpacesInitial()) {
on<LoadBookableSpacesEvent>(_onLoadBookableSpaces);
on<InsertUpdatedSpaceEvent>(_onInsertUpdatedSpaceEven);
}
Future<void> _onLoadBookableSpaces(
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> 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<BookableSpacesState> 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));
}
}

View File

@ -0,0 +1,24 @@
part of 'bookable_spaces_bloc.dart';
sealed class BookableSpacesEvent extends Equatable {
const BookableSpacesEvent();
@override
List<Object> get props => [];
}
class LoadBookableSpacesEvent extends BookableSpacesEvent {
final BookableSpacesParam param;
const LoadBookableSpacesEvent(this.param);
}
class InsertUpdatedSpaceEvent extends BookableSpacesEvent {
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
final BookableSpacemodel bookableSpace;
final BookableSpaceConfig updatedBookableSpaceConfig;
const InsertUpdatedSpaceEvent({
required this.bookableSpaces,
required this.bookableSpace,
required this.updatedBookableSpaceConfig,
});
}

View File

@ -0,0 +1,28 @@
part of 'bookable_spaces_bloc.dart';
sealed class BookableSpacesState extends Equatable {
const BookableSpacesState();
@override
List<Object> get props => [];
}
final class BookableSpacesInitial extends BookableSpacesState {}
final class BookableSpacesLoading extends BookableSpacesState {}
final class BookableSpacesLoaded extends BookableSpacesState {
final PaginatedDataModel<BookableSpacemodel> bookableSpacesList;
const BookableSpacesLoaded({
required this.bookableSpacesList,
});
}
final class BookableSpacesError extends BookableSpacesState {
final String error;
const BookableSpacesError({
required this.error,
});
}
class InsertingUpdatedSpaceState extends BookableSpacesState {}

View File

@ -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<NonBookableSpacesEvent, NonBookableSpacesState> {
NonBookableSpacesService nonBookableSpacesService;
NonBookableSpacesBloc(this.nonBookableSpacesService)
: super(NonBookableSpacesInitial()) {
on<CallInitStateEvent>(_onCallInitStateEvent);
on<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
}
void _onCallInitStateEvent(
CallInitStateEvent event, Emitter<NonBookableSpacesState> emit) {
emit(NonBookableSpacesInitial());
}
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
Emitter<NonBookableSpacesState> 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()),
);
}
}
}
}

View File

@ -0,0 +1,17 @@
part of 'non_bookaable_spaces_bloc.dart';
sealed class NonBookableSpacesEvent extends Equatable {
const NonBookableSpacesEvent();
@override
List<Object> get props => [];
}
class CallInitStateEvent extends NonBookableSpacesEvent {}
class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent {
final NonBookableSpacesParams nonBookableSpacesParams;
const LoadUnBookableSpacesEvent({
required this.nonBookableSpacesParams,
});
}

View File

@ -0,0 +1,32 @@
part of 'non_bookaable_spaces_bloc.dart';
sealed class NonBookableSpacesState extends Equatable {
const NonBookableSpacesState();
@override
List<Object> get props => [];
}
final class NonBookableSpacesInitial extends NonBookableSpacesState {}
class NonBookableSpacesLoading extends NonBookableSpacesState {
final PaginatedDataModel<BookableSpacemodel>? lastNonBookableSpaces;
const NonBookableSpacesLoading({
this.lastNonBookableSpaces,
});
}
class NonBookableSpacesLoaded extends NonBookableSpacesState {
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
final List<BookableSpacemodel> selectedBookableSpaces;
const NonBookableSpacesLoaded({
required this.nonBookableSpaces,
this.selectedBookableSpaces = const [],
});
}
class NonBookableSpacesError extends NonBookableSpacesState {
final String error;
const NonBookableSpacesError(this.error);
}

View File

@ -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<SendBookableSpacesEvent, SendBookableSpacesState> {
SendBookableSpacesService sendBookableSpacesService;
SendBookableSpacesBloc(
this.sendBookableSpacesService,
) : super(SendBookableSpacesInitial()) {
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
}
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
Emitter<SendBookableSpacesState> emit) async {
emit(SendBookableSpacesLoading());
try {
await sendBookableSpacesService.sendBookableSpacesToApi(
SendBookableSpacesToApiParams.fromBookableSpacesModel(
event.selectedBookableSpaces),
);
emit(SendBookableSpacesSuccess());
} catch (e) {
emit(
SendBookableSpacesError(e.toString()),
);
}
}
}

View File

@ -0,0 +1,13 @@
part of 'send_bookable_spaces_bloc.dart';
sealed class SendBookableSpacesEvent extends Equatable {
const SendBookableSpacesEvent();
@override
List<Object> get props => [];
}
class SendBookableSpacesToApi extends SendBookableSpacesEvent {
final List<BookableSpacemodel> selectedBookableSpaces;
const SendBookableSpacesToApi({required this.selectedBookableSpaces});
}

View File

@ -0,0 +1,19 @@
part of 'send_bookable_spaces_bloc.dart';
sealed class SendBookableSpacesState extends Equatable {
const SendBookableSpacesState();
@override
List<Object> 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);
}

View File

@ -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<SetupBookableSpacesEvent, SetupBookableSpacesState> {
NonBookableSpacesService nonBookableSpacesService;
SetupBookableSpacesBloc(this.nonBookableSpacesService)
: super(const SetupBookableSpacesInitial(bookableSpaces: [])) {
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
on<AddBookableDaysEvent>(_onAddBookableDays);
on<ChangeStartTimeEvent>(_onChangeStartTimeEvent);
on<ChangeEndTimeEvent>(_onChangeEndTimeEvent);
on<ChangeCostEvent>(_onChangeCostEvent);
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
on<EditModeSelected>(_onEditModeSelected);
}
List<BookableSpacemodel> 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<SetupBookableSpacesState> emit,
) {
emit(InProgressState(bookableSpaces: state.bookableSpaces));
final updatedSpaces = List<BookableSpacemodel>.from(state.bookableSpaces);
updatedSpaces.add(event.nonBookableSpace);
emit(AddNonBookableSpaceIntoBookableState(bookableSpaces: updatedSpaces));
}
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
Emitter<SetupBookableSpacesState> emit) {
emit(InProgressState(bookableSpaces: state.bookableSpaces));
state.bookableSpaces.remove(event.bookableSpace);
emit(RemoveBookableSpaceIntoNonBookableState(
bookableSpaces: state.bookableSpaces));
}
void _onAddBookableDays(
AddBookableDaysEvent event, Emitter<SetupBookableSpacesState> 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<SetupBookableSpacesState> 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<SetupBookableSpacesState> 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<SetupBookableSpacesState> 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<SetupBookableSpacesState> emit) {
if (state.bookableSpaces.first.spaceConfig!.isValid) {
emit(ValidSaveButtonState(
bookableSpaces: state.bookableSpaces,
));
} else {
emit(UnValidSaveButtonState(
bookableSpaces: state.bookableSpaces,
));
}
}
void _onEditModeSelected(
EditModeSelected event,
Emitter<SetupBookableSpacesState> emit,
) {
final updatedList = [event.editingBookableSpace];
emit(SetupBookableSpacesInitial(bookableSpaces: updatedList));
}
}

View File

@ -0,0 +1,59 @@
part of 'setup_bookable_spaces_bloc.dart';
sealed class SetupBookableSpacesEvent extends Equatable {
const SetupBookableSpacesEvent();
@override
List<Object> 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<String> 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,
});
}

View File

@ -0,0 +1,37 @@
part of 'setup_bookable_spaces_bloc.dart';
sealed class SetupBookableSpacesState extends Equatable {
final List<BookableSpacemodel> bookableSpaces;
const SetupBookableSpacesState({required this.bookableSpaces});
TimeOfDay? get startTime =>
bookableSpaces.first.spaceConfig!.bookingStartTime;
TimeOfDay? get endTime => bookableSpaces.first.spaceConfig!.bookingEndTime;
@override
List<Object> 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});
}

View File

@ -0,0 +1,22 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'steps_state.dart';
class StepsCubit extends Cubit<StepsState> {
StepsCubit() : super(StepOneState());
void initDialogValue() {
emit(StepOneState());
}
void editValueInit() {
emit(StepTwoState());
}
void goToNextStep() {
if (state is StepOneState) {
emit(StepTwoState());
}
}
}

View File

@ -0,0 +1,12 @@
part of 'steps_cubit.dart';
sealed class StepsState extends Equatable {
const StepsState();
@override
List<Object> get props => [];
}
final class StepOneState extends StepsState {}
final class StepTwoState extends StepsState {}

View File

@ -0,0 +1,16 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'toggle_points_switch_state.dart';
class TogglePointsSwitchCubit extends Cubit<TogglePointsSwitchState> {
TogglePointsSwitchCubit() : super(UnActivatePointsSwitch());
void activateSwitch() {
emit(ActivatePointsSwitch());
}
void unActivateSwitch() {
emit(UnActivatePointsSwitch());
}
}

View File

@ -0,0 +1,13 @@
part of 'toggle_points_switch_cubit.dart';
sealed class TogglePointsSwitchState extends Equatable {
const TogglePointsSwitchState();
@override
List<Object> get props => [];
}
class ActivatePointsSwitch extends TogglePointsSwitchState {}
class UnActivatePointsSwitch extends TogglePointsSwitchState {}

View File

@ -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<UpdateBookableSpaceEvent, UpdateBookableSpacesState> {
final UpdateBookableSpaceService updateBookableSpaceService;
UpdateBookableSpacesBloc(this.updateBookableSpaceService)
: super(UpdateBookableSpacesInitial()) {
on<UpdateBookableSpace>(_onUpdateBookableSpace);
}
Future<void> _onUpdateBookableSpace(UpdateBookableSpace event,
Emitter<UpdateBookableSpacesState> 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()),
);
}
}
}

View File

@ -0,0 +1,17 @@
part of 'update_bookable_spaces_bloc.dart';
sealed class UpdateBookableSpaceEvent extends Equatable {
const UpdateBookableSpaceEvent();
@override
List<Object> get props => [];
}
class UpdateBookableSpace extends UpdateBookableSpaceEvent {
final void Function()? onSuccess;
final UpdateBookableSpaceParam updatedParam;
const UpdateBookableSpace({
required this.updatedParam,
this.onSuccess,
});
}

View File

@ -0,0 +1,30 @@
part of 'update_bookable_spaces_bloc.dart';
sealed class UpdateBookableSpacesState extends Equatable {
const UpdateBookableSpacesState();
@override
List<Object> 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,
});
}

View File

@ -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<ManageBookableSpacesPage> createState() =>
_ManageBookableSpacesPageState();
}
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
@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(),
),
],
),
);
}
}

View File

@ -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<SetupBookableSpacesDialog> createState() =>
_SetupBookableSpacesDialogState();
}
class _SetupBookableSpacesDialogState extends State<SetupBookableSpacesDialog> {
final TextEditingController pointsController = TextEditingController();
@override
void dispose() {
pointsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<StepsCubit>(
create: widget.editingBookableSpace == null
? (context) => StepsCubit()..initDialogValue()
: (context) => StepsCubit()..editValueInit(),
),
BlocProvider<NonBookableSpacesBloc>(
create: (context) => NonBookableSpacesBloc(
NonBookableSpacesDebouncerDecoratorService(
RemoteNonBookableSpaces(HTTPService()),
),
)..add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(currentPage: 1),
),
),
),
BlocProvider<SetupBookableSpacesBloc>(
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<StepsCubit>().state;
final bookableSpaces =
context.watch<SetupBookableSpacesBloc>().state.bookableSpaces;
return stepsState is StepOneState
? const NextFirstStepButton()
: SaveSecondStepButton(
pointsController: pointsController,
isEditingMode: widget.editingBookableSpace != null,
bookableSpaces: bookableSpaces,
);
}),
],
),
),
);
}
}

View File

@ -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<BookableSpacemodel> 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<UpdateBookableSpacesBloc, UpdateBookableSpacesState>(
listener: (context, updateState) {
if (updateState is UpdateBookableSpaceSuccess) {
context.read<BookableSpacesBloc>().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<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
value: space.spaceConfig!.availability,
activeTrackColor: ColorsManager.dialogBlueTitle,
inactiveTrackColor: ColorsManager.grayBorder,
thumbColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
onChanged: (value) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
updatedParam: UpdateBookableSpaceParam(
spaceUuid: space.spaceUuid,
availability: value,
)),
);
},
);
},
),
),
);
}
}

View File

@ -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<SetupBookableSpacesBloc>().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<SetupBookableSpacesBloc>().add(
ChangeStartTimeEvent(startTime: pickedStartTime),
);
context.read<SetupBookableSpacesBloc>().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<SetupBookableSpacesBloc>().add(
ChangeEndTimeEvent(endTime: pickedEndTime),
);
context.read<SetupBookableSpacesBloc>().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,
),
);
}
}

View File

@ -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<NonBookableSpacesBloc, NonBookableSpacesState>(
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<BookableSpacesBloc>().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,
),
);
},
),
)
],
)
],
);
}
}

View File

@ -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<SetupBookableSpacesBloc, SetupBookableSpacesState>(
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<SetupBookableSpacesBloc>();
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,
),
),
],
)),
],
);
}
}

View File

@ -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,
),
));
}
}

View File

@ -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<bool?> 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,
),
);
}
}

View File

@ -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<T> extends StatelessWidget {
final List<String> columnsTitles;
final List<DataCell> Function(T item) cellsWidgets;
final List<T> 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(),
);
}
}

View File

@ -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<StepsCubit, StepsState>(builder: (context, state) {
return switch (state) {
StepOneState() => const SpacesStepDetailsWidget(),
StepTwoState() => StepTwoDetailsWidget(
pointsController: pointsController,
editingBookableSpace: editingBookableSpace,
),
};
}),
);
}
}

View File

@ -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<BookableSpacesBloc>();
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,
),
),
),
);
}
}

View File

@ -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<BookableSpacesBloc, BookableSpacesState>(
builder: (context, state) {
if (state is BookableSpacesLoaded) {
final totalPages = state.bookableSpacesList.totalPages;
final currentPage = state.bookableSpacesList.page;
List<Widget> paginationItems = [];
if (currentPage > 2) {
paginationItems.add(
_buildArrowButton(
label: '«',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParam(currentPage: currentPage - 2),
),
);
},
),
);
}
if (currentPage > 1) {
paginationItems.add(
_buildArrowButton(
label: '<',
onTap: () {
context.read<BookableSpacesBloc>().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<BookableSpacesBloc>().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<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParam(currentPage: currentPage + 1),
),
);
},
),
);
}
if (currentPage + 1 < totalPages) {
paginationItems.add(
_buildArrowButton(
label: '»',
onTap: () {
context.read<BookableSpacesBloc>().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,
),
),
),
),
);
}
}

View File

@ -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<BookableSpacesBloc, BookableSpacesState>(
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<BookableSpacesBloc>()
.add(LoadBookableSpacesEvent(
BookableSpacesParam(currentPage: 1),
)),
child: const Text('Try Again'))
]);
} else if (state is BookableSpacesLoaded) {
return CustomDataTable<BookableSpacemodel>(
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),
),
);
}
}

View File

@ -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<BookableSpacesBloc>();
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: bloc,
child: SetupBookableSpacesDialog(),
),
);
},
)
],
),
);
}
}

View File

@ -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<SetupBookableSpacesBloc, SetupBookableSpacesState>(
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<StepsCubit>().goToNextStep();
context.read<SetupBookableSpacesBloc>().add(
CheckConfigurValidityEvent(),
);
},
onCancelPressed: () => context.pop(),
),
_ => const SizedBox(),
};
},
);
}
}

View File

@ -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<PointsPartWidget> createState() => _PointsPartWidgetState();
}
class _PointsPartWidgetState extends State<PointsPartWidget> {
@override
void initState() {
super.initState();
if (widget.editingBookableSpace != null) {
widget.pointsController.text =
widget.editingBookableSpace!.spaceConfig!.cost.toString();
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TogglePointsSwitchCubit, TogglePointsSwitchState>(
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<TogglePointsSwitchCubit>();
final bloc = context.read<SetupBookableSpacesBloc>();
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<SetupBookableSpacesBloc>()
.add(ChangeCostEvent(cost: updatedCost));
context.read<SetupBookableSpacesBloc>().add(
CheckConfigurValidityEvent(),
);
},
)
else
const SizedBox(),
],
);
},
);
}
}

View File

@ -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<BookableSpacemodel> bookableSpaces;
const SaveSecondStepButton({
super.key,
required this.pointsController,
required this.isEditingMode,
required this.bookableSpaces,
});
@override
Widget build(BuildContext context) {
return BlocListener<SendBookableSpacesBloc, SendBookableSpacesState>(
listener: (context, state) {
if (state is SendBookableSpacesSuccess) {
context.read<NonBookableSpacesBloc>().add(CallInitStateEvent());
} else if (state is SendBookableSpacesError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error)),
);
}
},
child: BlocBuilder<SetupBookableSpacesBloc, SetupBookableSpacesState>(
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<SendBookableSpacesBloc>().add(
SendBookableSpacesToApi(
selectedBookableSpaces: bookableSpaces,
),
);
}
}
},
onCancelPressed: () => context.pop(),
);
},
),
);
}
void callEditLogic(BuildContext context) {
print(bookableSpaces.first.spaceConfig!.cost);
if (bookableSpaces.isNotEmpty) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
onSuccess: () => context
.read<NonBookableSpacesBloc>()
.add(CallInitStateEvent()),
updatedParam: UpdateBookableSpaceParam.fromBookableModel(
bookableSpaces.first,
),
),
);
}
}
}

View File

@ -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<TextInputFormatter>? 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,
),
),
);
}
}

View File

@ -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<SpacesStepDetailsWidget> createState() =>
_SpacesStepDetailsWidgetState();
}
class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
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<NonBookableSpacesBloc>().state;
if (state is NonBookableSpacesLoaded &&
state.nonBookableSpaces.hasNext &&
!isLoadingMore) {
isLoadingMore = true;
currentPage++;
context.read<NonBookableSpacesBloc>().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<NonBookableSpacesBloc>().add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams: NonBookableSpacesParams(
currentPage: currentPage,
searchedWords: currentSearchTerm,
),
),
);
},
),
),
Expanded(
child:
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
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<NonBookableSpacesBloc>().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(),
};
},
),
)
],
),
)
],
);
}
}

View File

@ -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),
)
],
),
);
}
}

View File

@ -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<StepsCubit, StepsState>(
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,
),
),
],
);
}
}

View File

@ -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<TimePickerWidget> createState() => _TimePickerWidgetState();
}
class _TimePickerWidgetState extends State<TimePickerWidget> {
TimeOfDay? timePicked;
@override
void initState() {
widget.setupBookableSpacesBloc = context.read<SetupBookableSpacesBloc>();
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,
),
),
),
);
}
}

View File

@ -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<BookableSpacemodel> 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()),
);
}
},
),
);
}
}

View File

@ -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<WeekDaysCheckboxRow> createState() => _WeekDaysCheckboxRowState();
}
class _WeekDaysCheckboxRowState extends State<WeekDaysCheckboxRow> {
final Map<String, bool> _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<SetupBookableSpacesBloc>().add(
AddBookableDaysEvent(bookableDays: selectedDays),
);
context.read<SetupBookableSpacesBloc>().add(
CheckConfigurValidityEvent(),
);
},
),
const SizedBox(width: 8),
Expanded(
child: Text(
entry.key,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w400,
),
),
),
],
),
);
}).toList(),
),
],
);
}
}

View File

@ -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<AccessManagementPage>
scaffoldBody: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
AccessOverviewContent(),
BookingPage(),
children: [
const AccessOverviewContent(),
BookingPage(
pageController: _pageController,
),
ManageBookableSpacesPage(
pageController: _pageController,
),
],
),
),

View File

@ -40,4 +40,22 @@ class PaginatedDataModel<T> extends Equatable {
totalItems,
totalPages,
];
PaginatedDataModel<T> copyWith({
List<T>? data,
int? page,
int? size,
bool? hasNext,
int? totalItems,
int? totalPages,
}) {
return PaginatedDataModel<T>(
data: data ?? this.data,
page: page ?? this.page,
size: size ?? this.size,
hasNext: hasNext ?? this.hasNext,
totalItems: totalItems ?? this.totalItems,
totalPages: totalPages ?? this.totalPages,
);
}
}

View File

@ -181,7 +181,7 @@ class VisitorPasswordBloc
effectiveTimeTimeStamp = selectedTimestamp;
startTimeAccess = selectedDateTime.toString().split('.').first;
} else {
// END TIME VALIDATION
if (effectiveTimeTimeStamp != null &&
selectedTimestamp < effectiveTimeTimeStamp!) {
await showDialog<void>(

View File

@ -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);
}

View File

@ -141,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}';
}

View File

@ -18,6 +18,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 +208,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';

View File

@ -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';
}

View File

@ -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