diff --git a/assets/icons/add_button_Icon.svg b/assets/icons/add_button_Icon.svg new file mode 100644 index 00000000..f9b8eae7 --- /dev/null +++ b/assets/icons/add_button_Icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/back_button_icon.svg b/assets/icons/back_button_icon.svg new file mode 100644 index 00000000..5cc7b637 --- /dev/null +++ b/assets/icons/back_button_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/clock_icon.svg b/assets/icons/clock_icon.svg new file mode 100644 index 00000000..296aa862 --- /dev/null +++ b/assets/icons/clock_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/no_data_table.svg b/assets/icons/no_data_table.svg new file mode 100644 index 00000000..c97946a2 --- /dev/null +++ b/assets/icons/no_data_table.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/search_icon.svg b/assets/icons/search_icon.svg new file mode 100644 index 00000000..009efd91 --- /dev/null +++ b/assets/icons/search_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/x_delete.svg b/assets/icons/x_delete.svg new file mode 100644 index 00000000..637f2e72 --- /dev/null +++ b/assets/icons/x_delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/4_sceen_switch.svg b/assets/images/4_sceen_switch.svg new file mode 100644 index 00000000..3765e137 --- /dev/null +++ b/assets/images/4_sceen_switch.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/6_sceen_switch.svg b/assets/images/6_sceen_switch.svg new file mode 100644 index 00000000..fef2291b --- /dev/null +++ b/assets/images/6_sceen_switch.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index a50d2615..071c7433 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,9 @@ import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:go_router/go_router.dart'; import 'package:syncrow_web/firebase_options.dart'; -import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_event.dart'; -import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; -import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; -import 'package:syncrow_web/utils/app_routes.dart'; -import 'package:syncrow_web/utils/constants/routes_const.dart'; -import 'package:syncrow_web/utils/navigation_service.dart'; -import 'package:syncrow_web/utils/theme/theme.dart'; +import 'package:syncrow_web/syncrow_app.dart'; Future main() async { try { @@ -33,59 +20,5 @@ Future main() async { ); initialSetup(); } catch (_) {} - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - MyApp({super.key}); - - final GoRouter _router = GoRouter( - initialLocation: RoutesConst.auth, - routes: AppRoutes.getRoutes(), - redirect: (context, state) async { - final checkToken = await AuthBloc.getTokenAndValidate(); - final loggedIn = checkToken == 'Success'; - final goingToLogin = state.uri.toString() == RoutesConst.auth; - - if (!loggedIn && !goingToLogin) return RoutesConst.auth; - if (loggedIn && goingToLogin) return RoutesConst.home; - - return null; - }, - ); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => CreateRoutineBloc(), - ), - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), - BlocProvider( - create: (context) => VisitorPasswordBloc(), - ), - BlocProvider( - create: (context) => RoutineBloc(), - ), - BlocProvider( - create: (context) => SpaceTreeBloc(), - ), - ], - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - scrollBehavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.unknown, - }, - ), - key: NavigationService.navigatorKey, - // scaffoldMessengerKey: NavigationService.snackbarKey, - theme: myTheme, - routerConfig: _router, - )); - } + runApp(const SyncrowApp()); } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 284e2f30..49df196f 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -1,22 +1,9 @@ import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:go_router/go_router.dart'; import 'package:syncrow_web/firebase_options.dart'; -import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_event.dart'; -import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; -import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; -import 'package:syncrow_web/utils/app_routes.dart'; -import 'package:syncrow_web/utils/constants/routes_const.dart'; -import 'package:syncrow_web/utils/navigation_service.dart'; -import 'package:syncrow_web/utils/theme/theme.dart'; +import 'package:syncrow_web/syncrow_app.dart'; Future main() async { try { @@ -33,59 +20,5 @@ Future main() async { ); initialSetup(); } catch (_) {} - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - MyApp({super.key}); - - final GoRouter _router = GoRouter( - initialLocation: RoutesConst.auth, - routes: AppRoutes.getRoutes(), - redirect: (context, state) async { - final checkToken = await AuthBloc.getTokenAndValidate(); - final loggedIn = checkToken == 'Success'; - final goingToLogin = state.uri.toString() == RoutesConst.auth; - - if (!loggedIn && !goingToLogin) return RoutesConst.auth; - if (loggedIn && goingToLogin) return RoutesConst.home; - - return null; - }, - ); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => CreateRoutineBloc(), - ), - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), - BlocProvider( - create: (context) => VisitorPasswordBloc(), - ), - BlocProvider( - create: (context) => RoutineBloc(), - ), - BlocProvider( - create: (context) => SpaceTreeBloc(), - ), - ], - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - scrollBehavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.unknown, - }, - ), - key: NavigationService.navigatorKey, - // scaffoldMessengerKey: NavigationService.snackbarKey, - theme: myTheme, - routerConfig: _router, - )); - } + runApp(const SyncrowApp()); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 6389c53a..6e0de3e1 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,26 +1,16 @@ import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:go_router/go_router.dart'; import 'package:syncrow_web/firebase_options.dart'; -import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; -import 'package:syncrow_web/pages/home/bloc/home_event.dart'; -import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; -import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/services/locator.dart'; -import 'package:syncrow_web/utils/app_routes.dart'; -import 'package:syncrow_web/utils/constants/routes_const.dart'; -import 'package:syncrow_web/utils/navigation_service.dart'; -import 'package:syncrow_web/utils/theme/theme.dart'; +import 'package:syncrow_web/syncrow_app.dart'; Future main() async { try { - const environment = String.fromEnvironment('FLAVOR', defaultValue: 'staging'); + const environment = String.fromEnvironment( + 'FLAVOR', + defaultValue: 'staging', + ); await dotenv.load(fileName: '.env.$environment'); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp( @@ -30,59 +20,5 @@ Future main() async { ); initialSetup(); } catch (_) {} - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - MyApp({super.key}); - - final GoRouter _router = GoRouter( - initialLocation: RoutesConst.auth, - routes: AppRoutes.getRoutes(), - redirect: (context, state) async { - final checkToken = await AuthBloc.getTokenAndValidate(); - final loggedIn = checkToken == 'Success'; - final goingToLogin = state.uri.toString() == RoutesConst.auth; - - if (!loggedIn && !goingToLogin) return RoutesConst.auth; - if (loggedIn && goingToLogin) return RoutesConst.home; - - return null; - }, - ); - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => CreateRoutineBloc(), - ), - BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())), - BlocProvider( - create: (context) => VisitorPasswordBloc(), - ), - BlocProvider( - create: (context) => RoutineBloc(), - ), - BlocProvider( - create: (context) => SpaceTreeBloc(), - ), - ], - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - scrollBehavior: const MaterialScrollBehavior().copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.stylus, - PointerDeviceKind.unknown, - }, - ), - key: NavigationService.navigatorKey, - // scaffoldMessengerKey: NavigationService.snackbarKey, - theme: myTheme, - routerConfig: _router, - )); - } + runApp(const SyncrowApp()); } diff --git a/lib/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart b/lib/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart new file mode 100644 index 00000000..034480ec --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart @@ -0,0 +1,63 @@ +import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; + +class MemoryCalendarService implements CalendarSystemService { + final Map _eventsCache = {}; + + @override + Future getCalendarEvents({ + required LoadEventsParam params, + }) async { + final key = params.generateKey(); + + return _eventsCache[key]!; + } + + void setEvents( + LoadEventsParam param, + CalendarEventsResponse events, + ) { + final key = param.generateKey(); + _eventsCache[key] = events; + } + + void addEvent(LoadEventsParam param, CalendarEventsResponse event) { + final key = param.generateKey(); + + _eventsCache[key] = event; + } + + void clear() { + _eventsCache.clear(); + } +} + +class MemoryCalendarServiceWithRemoteFallback implements CalendarSystemService { + final MemoryCalendarService memoryService; + final RemoteCalendarService remoteService; + + MemoryCalendarServiceWithRemoteFallback({ + required this.memoryService, + required this.remoteService, + }); + + @override + Future getCalendarEvents({ + required LoadEventsParam params, + }) async { + final key = params.generateKey(); + final doesExistInMemory = memoryService._eventsCache.containsKey(key); + + if (doesExistInMemory) { + return memoryService.getCalendarEvents(params: params); + } else { + final remoteResult = + await remoteService.getCalendarEvents(params: params); + memoryService.setEvents(params, remoteResult); + + return remoteResult; + } + } +} diff --git a/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart index 3c2610db..5ed71116 100644 --- a/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart +++ b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart @@ -18,7 +18,7 @@ class RemoteBookableSpacesService implements BookableSystemService { }) async { try { final response = await _httpService.get( - path: ApiEndpoints.getBookableSpaces, + path: ApiEndpoints.bookableSpaces, queryParameters: { 'page': param.page, 'size': param.size, diff --git a/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart index aa3307d3..55a5b0b8 100644 --- a/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart +++ b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -13,147 +14,21 @@ class RemoteCalendarService implements CalendarSystemService { @override Future getCalendarEvents({ - required String spaceId, + required LoadEventsParam params, }) async { + final month = params.startDate.month.toString().padLeft(2, '0'); + final year = params.startDate.year.toString(); + try { - final response = await _httpService.get( - path: ApiEndpoints.getCalendarEvents, - queryParameters: { - 'spaceId': spaceId, - }, + return await _httpService.get( + path: ApiEndpoints.getBookings + .replaceAll('{mm}', month) + .replaceAll('{yyyy}', year) + .replaceAll('{space}', params.id), expectedResponseModel: (json) { - return CalendarEventsResponse.fromJson( - json as Map, - ); + return CalendarEventsResponse.fromJson(json as Map); }, ); - - return CalendarEventsResponse.fromJson(response as Map); - } on DioException catch (e) { - final responseData = e.response?.data; - if (responseData is Map) { - final errorMessage = responseData['error']?['message'] as String? ?? - responseData['message'] as String? ?? - _defaultErrorMessage; - throw APIException(errorMessage); - } - throw APIException(_defaultErrorMessage); - } catch (e) { - throw APIException('$_defaultErrorMessage: ${e.toString()}'); - } - } -} - -class FakeRemoteCalendarService implements CalendarSystemService { - const FakeRemoteCalendarService(this._httpService, {this.useDummy = false}); - - final HTTPService _httpService; - final bool useDummy; - static const _defaultErrorMessage = 'Failed to load Calendar'; - - @override - Future getCalendarEvents({ - required String spaceId, - }) async { - if (useDummy) { - final dummyJson = { - 'statusCode': 200, - 'message': 'Successfully fetched all bookings', - 'data': [ - { - 'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80', - 'date': '2025-07-11T10:22:00.626Z', - 'startTime': '09:00:00', - 'endTime': '12:00:00', - 'cost': 10, - 'user': { - 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', - 'firstName': 'salsabeel', - 'lastName': 'abuzaid', - 'email': 'test@test.com', - 'companyName': null - }, - 'space': { - 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', - 'spaceName': '2(1)' - } - }, - { - 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', - 'date': '2025-07-11T10:22:00.626Z', - 'startTime': '12:00:00', - 'endTime': '13:00:00', - 'cost': 10, - 'user': { - 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', - 'firstName': 'salsabeel', - 'lastName': 'abuzaid', - 'email': 'test@test.com', - 'companyName': null - }, - 'space': { - 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', - 'spaceName': '2(1)' - } - }, - { - 'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561', - 'date': '2025-07-13T10:22:00.626Z', - 'startTime': '15:30:00', - 'endTime': '19:00:00', - 'cost': 20, - 'user': { - 'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e', - 'firstName': 'salsabeel', - 'lastName': 'abuzaid', - 'email': 'test@test.com', - 'companyName': null - }, - 'space': { - 'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e', - 'spaceName': '2(1)' - } - } - ], - 'success': true - }; - final response = CalendarEventsResponse.fromJson(dummyJson); - - // Filter events by spaceId - final filteredData = response.data.where((event) { - return event.space.uuid == spaceId; - }).toList(); - print('Filtering events for spaceId: $spaceId'); - print('Found ${filteredData.length} matching events'); - return filteredData.isNotEmpty - ? CalendarEventsResponse( - statusCode: response.statusCode, - message: response.message, - data: filteredData, - success: response.success, - ) - : CalendarEventsResponse( - statusCode: 404, - message: 'No events found for spaceId: $spaceId', - data: [], - success: false, - ); - } - - try { - final response = await _httpService.get( - path: ApiEndpoints.getCalendarEvents, - queryParameters: { - 'spaceId': spaceId, - }, - expectedResponseModel: (json) { - return CalendarEventsResponse.fromJson( - json as Map, - ); - }, - ); - - return CalendarEventsResponse.fromJson(response as Map); } on DioException catch (e) { final responseData = e.response?.data; if (responseData is Map) { diff --git a/lib/pages/access_management/booking_system/domain/LoadEventsParam.dart b/lib/pages/access_management/booking_system/domain/LoadEventsParam.dart new file mode 100644 index 00000000..542dd5dc --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/LoadEventsParam.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +class LoadEventsParam extends Equatable { + final DateTime startDate; + final DateTime endDate; + final String id; + + const LoadEventsParam({ + required this.startDate, + required this.endDate, + required this.id, + }); + + @override + List get props => [startDate, endDate, id]; + + LoadEventsParam copyWith({ + DateTime? startDate, + DateTime? endDate, + String? id, + }) { + return LoadEventsParam( + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + id: id ?? this.id, + ); + } +} + +extension KeyGenerator on LoadEventsParam { + String generateKey() { + return '$id-${startDate.year}-${startDate.month.toString().padLeft(2, '0')}'; + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart index 9e178040..3522054c 100644 --- a/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart +++ b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart @@ -1,7 +1,8 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; abstract class CalendarSystemService { Future getCalendarEvents({ - required String spaceId, + required LoadEventsParam params, }); } diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart index da782d74..59a1aa28 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart @@ -2,37 +2,48 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; - +import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart'; part 'events_event.dart'; part 'events_state.dart'; class CalendarEventsBloc extends Bloc { - final EventController eventController = EventController(); + EventController eventController = EventController(); final CalendarSystemService calendarService; - CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) { + CalendarEventsBloc({ + required this.calendarService, + }) : super(EventsInitial()) { on(_onLoadEvents); on(_onAddEvent); - on(_onStartTimer); on(_onDisposeResources); on(_onGoToWeek); + on(_onResetEvents); } Future _onLoadEvents( LoadEvents event, Emitter emit, ) async { + final param = event.param; + final month = param.endDate.month; + final year = param.endDate.year; + final spaceId = param.id; + emit(EventsLoading()); try { - final response = await calendarService.getCalendarEvents( - spaceId: event.spaceId, - ); - final events = - response.data.map(_toCalendarEventData).toList(); + final response = await calendarService.getCalendarEvents(params: param); + + final events = response.data.map(_toCalendarEventData).toList(); eventController.addAll(events); - emit(EventsLoaded(events: events)); + emit(EventsLoaded( + events: events, + spaceId: spaceId, + month: month, + year: year, + )); } catch (e) { emit(EventsError('Failed to load events')); } @@ -40,16 +51,19 @@ class CalendarEventsBloc extends Bloc { void _onAddEvent(AddEvent event, Emitter emit) { eventController.add(event.event); + if (state is EventsLoaded) { final loaded = state as EventsLoaded; + emit(EventsLoaded( events: [...eventController.events], + spaceId: loaded.spaceId, + month: loaded.month, + year: loaded.year, )); } } - void _onStartTimer(StartTimer event, Emitter emit) {} - void _onDisposeResources( DisposeResources event, Emitter emit) { eventController.dispose(); @@ -61,6 +75,9 @@ class CalendarEventsBloc extends Bloc { final newWeekDays = _getWeekDays(event.weekDate); emit(EventsLoaded( events: loaded.events, + spaceId: loaded.spaceId, + month: loaded.month, + year: loaded.year, )); } } @@ -90,14 +107,13 @@ class CalendarEventsBloc extends Bloc { ); return CalendarEventData( - date: startTime, + date: startTime, startTime: startTime, endTime: endTime, - title: - '${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}', + title: '${booking.user.firstName} ${booking.user.lastName}', description: 'Cost: ${booking.cost}', color: Colors.blue, - event: booking, + event: booking, ); } @@ -112,4 +128,18 @@ class CalendarEventsBloc extends Bloc { eventController.dispose(); return super.close(); } + + void _onResetEvents( + ResetEvents event, + Emitter emit, + ) { + if (calendarService is MemoryCalendarServiceWithRemoteFallback) { + (calendarService as MemoryCalendarServiceWithRemoteFallback) + .memoryService + .clear(); + } + eventController.dispose(); + eventController = EventController(); + emit(EventsInitial()); + } } diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart index 4f4cafcf..ecd7f975 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart @@ -6,17 +6,11 @@ abstract class CalendarEventsEvent { } class LoadEvents extends CalendarEventsEvent { - final String spaceId; - final DateTime weekStart; - final DateTime weekEnd; - - const LoadEvents({ - required this.spaceId, - required this.weekStart, - required this.weekEnd, - }); + final LoadEventsParam param; + const LoadEvents(this.param); } + class AddEvent extends CalendarEventsEvent { final CalendarEventData event; const AddEvent(this.event); @@ -35,3 +29,10 @@ class CheckWeekHasEvents extends CalendarEventsEvent { final DateTime weekStart; const CheckWeekHasEvents(this.weekStart); } + +class ResetEvents extends CalendarEventsEvent { + const ResetEvents(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart index bc0c2e31..b98fd2fb 100644 --- a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart @@ -7,11 +7,17 @@ class EventsInitial extends CalendarEventState {} class EventsLoading extends CalendarEventState {} -class EventsLoaded extends CalendarEventState { +final class EventsLoaded extends CalendarEventState { final List events; + final String spaceId; + final int month; + final int year; EventsLoaded({ required this.events, + required this.spaceId, + required this.month, + required this.year, }); } diff --git a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart index 0ff9aaf6..68934ddc 100644 --- a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -1,12 +1,15 @@ +// booking_page.dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; @@ -24,7 +27,33 @@ class BookingPage extends StatefulWidget { } class _BookingPageState extends State { - late final EventController _eventController; + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => SelectedBookableSpaceBloc()), + BlocProvider(create: (_) => DateSelectionBloc()), + BlocProvider( + create: (_) => CalendarEventsBloc( + calendarService: MemoryCalendarServiceWithRemoteFallback( + remoteService: RemoteCalendarService(HTTPService()), + memoryService: MemoryCalendarService(), + ), + ), + ), + ], + child: _BookingPageContent(), + ); + } +} + +class _BookingPageContent extends StatefulWidget { + @override + State<_BookingPageContent> createState() => _BookingPageContentState(); +} + +class _BookingPageContentState extends State<_BookingPageContent> { + late EventController _eventController; @override void initState() { @@ -38,7 +67,7 @@ class _BookingPageState extends State { super.dispose(); } - void _dispatchLoadEvents(BuildContext context) { + void _loadEvents(BuildContext context) { final selectedRoom = context.read().state.selectedBookableSpace; final dateState = context.read().state; @@ -46,9 +75,11 @@ class _BookingPageState extends State { if (selectedRoom != null) { context.read().add( LoadEvents( - spaceId: selectedRoom.uuid, - weekStart: dateState.weekStart, - weekEnd: dateState.weekStart.add(const Duration(days: 6)), + LoadEventsParam( + startDate: dateState.weekStart, + endDate: dateState.weekStart.add(const Duration(days: 6)), + id: selectedRoom.uuid, + ), ), ); } @@ -56,182 +87,170 @@ class _BookingPageState extends State { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => SelectedBookableSpaceBloc()), - BlocProvider(create: (_) => DateSelectionBloc()), - BlocProvider( - create: (_) => CalendarEventsBloc( - calendarService: - FakeRemoteCalendarService(HTTPService(), useDummy: true), - ), - ), - ], - child: Builder( - builder: (context) => - BlocListener( - listenWhen: (prev, curr) => curr is EventsLoaded, + return BlocListener( + listener: (context, state) { + if (state.selectedBookableSpace != null) { + // Reset events and clear cache when room changes + context.read().add(ResetEvents()); + _loadEvents(context); + } + }, + child: BlocListener( + listener: (context, state) { + _loadEvents(context); + }, + child: BlocListener( listener: (context, state) { if (state is EventsLoaded) { _eventController.removeWhere((_) => true); _eventController.addAll(state.events); } }, - child: BlocListener( - listener: (context, state) => _dispatchLoadEvents(context), - child: BlocListener( - listener: (context, state) => _dispatchLoadEvents(context), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(3, 0), - blurRadius: 6, - spreadRadius: 0, - ), - ], + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, ), - child: Column( - children: [ - Expanded( - flex: 2, - child: BlocBuilder( - builder: (context, state) { - return BookingSidebar( - onRoomSelected: (selectedRoom) { - context - .read() - .add(SelectBookableSpace(selectedRoom)); - }, - ); - }, - ), - ), - Expanded( - child: BlocBuilder( - builder: (context, dateState) { - return CustomCalendarPage( - selectedDate: dateState.selectedDate, - onDateChanged: (day, month, year) { - final newDate = DateTime(year, month, day); - context - .read() - .add(SelectDate(newDate)); - context.read().add( - SelectDateFromSidebarCalendar(newDate)); - }, - ); - }, - ), - ), - ], - ), - ), + ], ), - Expanded( - flex: 4, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (selectedRoom) { + context + .read() + .add(SelectBookableSpace(selectedRoom)); + }, + ); + }, + ), + ), + Expanded( + child: + BlocBuilder( + builder: (context, dateState) { + return CustomCalendarPage( + selectedDate: dateState.selectedDate, + onDateChanged: (day, month, year) { + final newDate = DateTime(year, month, day); + context + .read() + .add(SelectDate(newDate)); + context.read().add( + SelectDateFromSidebarCalendar(newDate)); + }, + ); + }, + ), + ), + ], + ), + ), + ), + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - SvgTextButton( - svgAsset: Assets.homeIcon, - label: 'Manage Bookable Spaces', - onPressed: () {}, - ), - const SizedBox(width: 20), - SvgTextButton( - svgAsset: Assets.groupIcon, - label: 'Manage Users', - onPressed: () {}, - ), - ], + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () {}, ), - BlocBuilder( - builder: (context, state) { - final weekStart = state.weekStart; - final weekEnd = - weekStart.add(const Duration(days: 6)); - return WeekNavigation( - weekStart: weekStart, - weekEnd: weekEnd, - onPreviousWeek: () { - context - .read() - .add(PreviousWeek()); - }, - onNextWeek: () { - context - .read() - .add(NextWeek()); - }, - ); - }, + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}, ), ], ), - Expanded( - child: BlocBuilder( - builder: (context, roomState) { - final selectedRoom = - roomState.selectedBookableSpace; - return BlocBuilder( - builder: (context, dateState) { - return BlocListener( - listenWhen: (prev, curr) => - curr is EventsLoaded, - listener: (context, state) { - if (state is EventsLoaded) { - _eventController - .removeWhere((_) => true); - _eventController.addAll(state.events); - } - }, - child: WeeklyCalendarPage( - startTime: selectedRoom - ?.bookableConfig.startTime, - endTime: selectedRoom - ?.bookableConfig.endTime, - weekStart: dateState.weekStart, - selectedDate: dateState.selectedDate, - eventController: _eventController, - selectedDateFromSideBarCalender: context - .watch() - .state - .selectedDateFromSideBarCalender, - ), + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return WeekNavigation( + weekStart: weekStart, + weekEnd: weekEnd, + onPreviousWeek: () { + context + .read() + .add(PreviousWeek()); + }, + onNextWeek: () { + context + .read() + .add(NextWeek()); + }, + ); + }, + ), + ], + ), + const SizedBox(height: 20), + Expanded( + flex: 5, + child: BlocBuilder( + builder: (context, roomState) { + final selectedRoom = + roomState.selectedBookableSpace; + return BlocBuilder( + builder: (context, dateState) { + return BlocBuilder( + builder: (context, eventState) { + return WeeklyCalendarPage( + key: ValueKey( + selectedRoom?.uuid ?? 'no-room'), + startTime: selectedRoom + ?.bookableConfig.startTime, + endTime: + selectedRoom?.bookableConfig.endTime, + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + selectedDateFromSideBarCalender: context + .watch() + .state + .selectedDateFromSideBarCalender, + // isLoading: eventState is EventsLoading, ); }, ); }, - ), - ), - ], + ); + }, + ), ), - ), + ], ), - ], + ), ), - ), + ], ), ), ), diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart index e3d84924..bd051a94 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -72,29 +72,33 @@ class __SidebarContentState extends State<_SidebarContent> { @override Widget build(BuildContext context) { return BlocConsumer( - listener: (context, state) { - if (state.currentPage == 1 && searchController.text.isNotEmpty) { - searchController.clear(); - } - }, + listener: (context, state) {}, builder: (context, state) { return Column( children: [ - const _SidebarHeader(title: 'Spaces'), + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + child: _SidebarHeader(title: 'Spaces')), + ), Container( decoration: BoxDecoration( color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(8.0), boxShadow: [ BoxShadow( color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, -2), - blurRadius: 4, - spreadRadius: 0, - ), - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.1), - offset: const Offset(0, 2), + offset: const Offset(0, 4), blurRadius: 4, spreadRadius: 0, ), @@ -147,6 +151,7 @@ class __SidebarContentState extends State<_SidebarContent> { IconButton( icon: const Icon(Icons.close), onPressed: () { + searchController.clear(); context.read().add(ResetSearch()); }, ), @@ -223,7 +228,7 @@ class _SidebarHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart index eb758311..21dfe646 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart @@ -66,18 +66,28 @@ class _CustomCalendarPageState extends State { weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], ); - return CalendarDatePicker2( - config: config, - value: [_selectedDate], - onValueChanged: (dates) { - final picked = dates.first; - if (picked != null) { - setState(() { - _selectedDate = picked; - }); - widget.onDateChanged(picked.day, picked.month, picked.year); - } - }, + return Container( + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: ColorsManager.textGray, + width: 1.0, + ), + ), + ), + child: CalendarDatePicker2( + config: config, + value: [_selectedDate], + onValueChanged: (dates) { + final picked = dates.first; + if (picked != null) { + setState(() { + _selectedDate = picked; + }); + widget.onDateChanged(picked.day, picked.month, picked.year); + } + }, + ), ); } } diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart index 6c0f9cb2..87c10a81 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart @@ -1,16 +1,15 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class EventTileWidget extends StatelessWidget { final List> events; - const EventTileWidget({ super.key, required this.events, }); - @override Widget build(BuildContext context) { return Container( @@ -18,39 +17,88 @@ class EventTileWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: events.map((event) { - final bool isEventEnded = + final booking = event.event is CalendarEventBooking + ? event.event! as CalendarEventBooking + : null; + + final companyName = booking?.user.companyName ?? 'Unknown Company'; + final startTime = DateFormat('hh:mm a').format(event.startTime!); + final endTime = DateFormat('hh:mm a').format(event.endTime!); + final isEventEnded = event.endTime != null && event.endTime!.isBefore(DateTime.now()); + + final duration = event.endTime!.difference(event.startTime!); + final bool isLongEnough = duration.inMinutes >= 31; return Expanded( child: Container( width: double.infinity, - padding: const EdgeInsets.all(6), + padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: isEventEnded - ? ColorsManager.lightGrayBorderColor - : ColorsManager.blue1.withOpacity(0.25), + ? ColorsManager.grayColor.withOpacity(0.1) + : ColorsManager.blue1.withOpacity(0.1), borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('h:mm a').format(event.startTime!), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.black87, - ), + border: Border( + left: BorderSide( + color: isEventEnded + ? ColorsManager.grayColor + : ColorsManager.secondaryColor, + width: 4, ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - ], + ), ), + child: isLongEnough + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$startTime - $endTime', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: isEventEnded + ? ColorsManager.grayColor.withOpacity(0.9) + : ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: isEventEnded + ? ColorsManager.grayColor + : ColorsManager.blackColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + companyName, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: isEventEnded + ? ColorsManager.grayColor.withOpacity(0.9) + : ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), + ), + ], + ) + : Text( + event.title, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: isEventEnded + ? ColorsManager.grayColor + : ColorsManager.blackColor, + fontWeight: FontWeight.bold, + ), + ), ), ); }).toList(), diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart index c7c660c1..9844e3d8 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; class SvgTextButton extends StatelessWidget { final String svgAsset; + final EdgeInsets? padding; final String label; final VoidCallback onPressed; final Color backgroundColor; @@ -12,16 +13,20 @@ class SvgTextButton extends StatelessWidget { final double borderRadius; final List boxShadow; final double svgSize; - + final double? fontSize; + final FontWeight? fontWeight; const SvgTextButton({ super.key, required this.svgAsset, + this.fontSize, + this.fontWeight, required this.label, required this.onPressed, this.backgroundColor = ColorsManager.circleRolesBackground, this.svgColor = const Color(0xFF496EFF), this.labelColor = Colors.black, this.borderRadius = 10.0, + this.padding, this.boxShadow = const [ BoxShadow( color: ColorsManager.lightGrayColor, @@ -40,7 +45,8 @@ class SvgTextButton extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadius), onTap: onPressed, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: padding ?? + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(borderRadius), diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart index 4a4b608d..57a14002 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart @@ -21,20 +21,24 @@ class RoomListItem extends StatelessWidget { groupValue: isSelected ? room.uuid : null, visualDensity: const VisualDensity(vertical: -4), onChanged: (value) => onTap(), - activeColor: ColorsManager.primaryColor, + activeColor: ColorsManager.secondaryColor, title: Text( room.spaceName, + maxLines: 2, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: ColorsManager.lightGrayColor, fontWeight: FontWeight.w700, + overflow: TextOverflow.ellipsis, fontSize: 12), ), subtitle: Text( room.virtualLocation, + maxLines: 2, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontSize: 10, fontWeight: FontWeight.w400, color: ColorsManager.textGray, + overflow: TextOverflow.ellipsis, ), ), ); diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart index 57e35c6d..325ce582 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart @@ -14,26 +14,33 @@ class WeekDayHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - Text( - DateFormat('EEE').format(date).toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: isSelectedDay ? Colors.blue : Colors.black, + return ColoredBox( + color: isSelectedDay + ? ColorsManager.secondaryColor.withOpacity(0.1) + : Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 10, ), - ), - Text( - DateFormat('d').format(date), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20, - color: - isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor, + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: ColorsManager.blackColor, + ), ), - ), - ], + Text( + DateFormat('d').format(date), + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 30, + color: ColorsManager.blackColor, + ), + ), + ], + ), ); } } diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 0dd343a7..931746b7 100644 --- a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -23,6 +22,12 @@ class WeeklyCalendarPage extends StatelessWidget { this.endTime, this.selectedDateFromSideBarCalender, }); + static const double timeLineWidth = 65; + static const int totalDays = 7; + static const double dayColumnWidth = 220; + + final double calendarContentWidth = + timeLineWidth + (totalDays * dayColumnWidth); @override Widget build(BuildContext context) { @@ -52,154 +57,152 @@ class WeeklyCalendarPage extends StatelessWidget { ); } - final weekDays = _getWeekDays(weekStart); + const double timeLineWidth = 90; - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); - final selectedSidebarIndex = selectedDateFromSideBarCalender == null - ? -1 - : weekDays - .indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!)); - - const double timeLineWidth = 80; - const int totalDays = 7; - final DateTime highlightStart = DateTime(2025, 7, 10); - final DateTime highlightEnd = DateTime(2025, 7, 19); return LayoutBuilder( builder: (context, constraints) { - final double calendarWidth = constraints.maxWidth; - final double dayColumnWidth = - (calendarWidth - timeLineWidth) / totalDays - 0.1; bool isInRange(DateTime date, DateTime start, DateTime end) { - return !date.isBefore(start) && !date.isAfter(end); + !date.isBefore(start) && !date.isAfter(end); + // remove this line and Check if the date is within the range + return false; } - return Padding( - padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), - child: Stack( - children: [ - WeekView( - weekDetectorBuilder: ({ - required date, - required height, - required heightPerMinute, - required minuteSlotSize, - required width, - }) { - return isInRange(date, highlightStart, highlightEnd) - ? HatchedColumnBackground( - backgroundColor: ColorsManager.grey800, - lineColor: ColorsManager.textGray, - opacity: 0.3, - stripeSpacing: 12, - borderRadius: BorderRadius.circular(8), - ) - : const SizedBox(); - }, - pageViewPhysics: const NeverScrollableScrollPhysics(), - key: ValueKey(weekStart), - controller: eventController, - initialDay: weekStart, - startHour: startHour - 1, - endHour: endHour, - heightPerMinute: 1.1, - showLiveTimeLineInAllDays: false, - showVerticalLines: true, - emulateVerticalOffsetBy: -80, - startDay: WeekDays.monday, - liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( - showBullet: false, - height: 0, - ), - weekDayBuilder: (date) { - return WeekDayHeader( - date: date, - isSelectedDay: isSameDay(date, selectedDate), - ); - }, - timeLineBuilder: (date) { - return TimeLineWidget(date: date); - }, - timeLineWidth: timeLineWidth, - weekPageHeaderBuilder: (start, end) => Container(), - weekTitleHeight: 60, - weekNumberBuilder: (firstDayOfWeek) => Padding( - padding: const EdgeInsets.only(right: 15, bottom: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - firstDayOfWeek.timeZoneName.replaceAll(':00', ''), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 12, - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - ), + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: calendarContentWidth, + child: Padding( + padding: + const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), + child: Stack( + children: [ + Container( + child: WeekView( + minuteSlotSize: MinuteSlotSize.minutes15, + weekDetectorBuilder: ({ + required date, + required height, + required heightPerMinute, + required minuteSlotSize, + required width, + }) { + final isSelected = isSameDay(date, selectedDate); + final isSidebarSelected = + selectedDateFromSideBarCalender != null && + isSameDay( + date, selectedDateFromSideBarCalender!); + if (isSidebarSelected && !isSelected) { + return Container( + height: height, + width: width, + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.13), + ), + ); + } else if (isSelected) { + return Container( + height: height, + width: width, + decoration: BoxDecoration( + color: + ColorsManager.spaceColor.withOpacity(0.07), + ), + ); + } + return const SizedBox.shrink(); + }, + + // weekDetectorBuilder: ({ + // required date, + // required height, + // required heightPerMinute, + // required minuteSlotSize, + // required width, + // }) { + // return isInRange(date, highlightStart, highlightEnd) + // ? HatchedColumnBackground( + // backgroundColor: ColorsManager.grey800, + // lineColor: ColorsManager.textGray, + // opacity: 0.3, + // stripeSpacing: 12, + // borderRadius: BorderRadius.circular(8), + // ) + // : const SizedBox(); + // }, + pageViewPhysics: const NeverScrollableScrollPhysics(), + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: startHour - 1, + endHour: endHour, + heightPerMinute: 1.7, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -95, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: + const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, + ), + weekDayBuilder: (date) { + return WeekDayHeader( + date: date, + isSelectedDay: isSameDay(date, selectedDate), + ); + }, + timeLineBuilder: (date) { + return TimeLineWidget(date: date); + }, + timeLineWidth: timeLineWidth, + weekPageHeaderBuilder: (start, end) => Container(), + weekTitleHeight: 90, + weekNumberBuilder: (firstDayOfWeek) => Padding( + padding: const EdgeInsets.only(right: 15, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + firstDayOfWeek.timeZoneName + .replaceAll(':00', ''), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontSize: 12, + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + eventTileBuilder: (date, events, boundary, start, end) { + return EventTileWidget( + events: events, + ); + }, ), - ], - ), - ), - eventTileBuilder: (date, events, boundary, start, end) { - return EventTileWidget( - events: events, - ); - }, - ), - if (selectedDayIndex >= 0) - Positioned( - left: (timeLineWidth + 3) + - (dayColumnWidth - 8) * (selectedDayIndex - 0.01), - top: 0, - bottom: 0, - width: dayColumnWidth, - child: IgnorePointer( - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 0, horizontal: 4), - color: ColorsManager.spaceColor.withOpacity(0.07), ), - ), - ), - if (selectedSidebarIndex >= 0 && - selectedSidebarIndex != selectedDayIndex) - Positioned( - left: (timeLineWidth + 3) + - (dayColumnWidth - 8) * (selectedSidebarIndex - 0.01), - top: 0, - bottom: 0, - width: dayColumnWidth, - child: IgnorePointer( - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 0, horizontal: 4), - color: Colors.orange.withOpacity(0.14), + Positioned( + right: 0, + top: 50, + bottom: 0, + child: IgnorePointer( + child: Container( + width: 1, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), ), - ), - ), - Positioned( - right: 0, - top: 50, - bottom: 0, - child: IgnorePointer( - child: Container( - width: 1, - color: Theme.of(context).scaffoldBackgroundColor, - ), + ], ), ), - ], - ), - ); + )); }, ); } - - List _getWeekDays(DateTime date) { - final int weekday = date.weekday; - final DateTime monday = date.subtract(Duration(days: weekday - 1)); - return List.generate(7, (i) => monday.add(Duration(days: i))); - } } bool isSameDay(DateTime d1, DateTime d2) { diff --git a/lib/pages/access_management/booking_system/view/booking_page.dart b/lib/pages/access_management/booking_system/view/booking_page.dart new file mode 100644 index 00000000..49d10b50 --- /dev/null +++ b/lib/pages/access_management/booking_system/view/booking_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; + +import 'package:syncrow_web/utils/constants/assets.dart'; + +class BookingPage extends StatelessWidget { + final PageController pageController; + const BookingPage({ + super.key, + required this.pageController, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Container( + color: Colors.blueGrey[100], + child: const Center( + child: Text( + 'Side bar', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + )), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: SizedBox( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () { + pageController.jumpToPage(2); + }), + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}) + ], + ) + ], + ), + ), + )) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart b/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart new file mode 100644 index 00000000..af24439f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +class NonBookableSpacesDebouncerDecoratorService + implements NonBookableSpacesService { + final NonBookableSpacesService _delegate; + Timer? _debounce; + + NonBookableSpacesDebouncerDecoratorService(this._delegate); + + @override + Future> load( + NonBookableSpacesParams params) { + final completer = Completer>(); + + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () async { + try { + final result = await _delegate.load(params); + completer.complete(result); + } catch (e) { + completer.completeError(e); + } + }); + + return completer.future; + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart new file mode 100644 index 00000000..ceebf262 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteBookableSpacesService implements BookableSpacesService { + final HTTPService _httpService; + RemoteBookableSpacesService(this._httpService); + static const _defaultErrorMessage = 'Failed to load Bookable Spaces'; + @override + Future> load( + BookableSpacesParam param) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.bookableSpaces, + queryParameters: { + 'configured': true, + 'page': param.currentPage, + }, + expectedResponseModel: (json) { + final result = json as Map; + return PaginatedDataModel.fromJson( + result, + BookableSpacemodel.fromJsonList, + ); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart new file mode 100644 index 00000000..1db35a8e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteNonBookableSpaces implements NonBookableSpacesService { + final HTTPService _httpService; + + RemoteNonBookableSpaces(this._httpService); + + static const _defaultErrorMessage = 'Failed to load Spaces'; + + @override + Future> load( + NonBookableSpacesParams params) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.bookableSpaces, + queryParameters: { + 'configured': false, + 'page': params.currentPage, + 'search': params.searchedWords, + }, + expectedResponseModel: (json) { + final result = json as Map; + return PaginatedDataModel.fromJson( + result, + BookableSpacemodel.fromJsonList, + ); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart new file mode 100644 index 00000000..5d50fd55 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart @@ -0,0 +1,35 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteSendBookableSpaces implements SendBookableSpacesService { + final HTTPService _httpService; + RemoteSendBookableSpaces(this._httpService); + static const _defaultErrorMessage = 'Failed to load Spaces'; + @override + Future sendBookableSpacesToApi( + SendBookableSpacesToApiParams params) async { + try { + await _httpService.post( + path: ApiEndpoints.bookableSpaces, + body: params.toJson(), + expectedResponseModel: (p0) {}, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart b/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart new file mode 100644 index 00000000..1c0b1c8e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart @@ -0,0 +1,40 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService { + final HTTPService _httpService; + RemoteUpdateBookableSpaceService(this._httpService); + static const _defaultErrorMessage = 'Failed to load Bookable Spaces'; + @override + Future update( + UpdateBookableSpaceParam updateParam) async { + try { + final response = await _httpService.put( + path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}', + body: updateParam.toJson(), + expectedResponseModel: (json) { + return BookableSpaceConfig.fromJson( + json['data'] as Map); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart new file mode 100644 index 00000000..1e28b686 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class BookableSpaceConfig { + final String configUuid; + final List bookableDays; + final TimeOfDay? bookingStartTime; + final TimeOfDay? bookingEndTime; + final int cost; + final bool availability; + BookableSpaceConfig({ + required this.configUuid, + required this.availability, + required this.bookableDays, + this.bookingEndTime, + this.bookingStartTime, + required this.cost, + }); + factory BookableSpaceConfig.zero() => BookableSpaceConfig( + configUuid: '', + bookableDays: [], + availability: false, + cost: -1, + ); + factory BookableSpaceConfig.fromJson(Map json) => + BookableSpaceConfig( + configUuid: json['uuid'] as String, + bookableDays: (json['daysAvailable'] as List).cast(), + availability: (json['active'] as bool?) ?? false, + bookingStartTime: parseTimeOfDay(json['startTime'] as String), + bookingEndTime: parseTimeOfDay(json['endTime'] as String), + cost: json['points'] as int, + ); + + static TimeOfDay parseTimeOfDay(String timeString) { + final parts = timeString.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + return TimeOfDay(hour: hour, minute: minute); + } + + bool get isValid => + bookableDays.isNotEmpty && + cost >= 0 && + bookingStartTime != null && + bookingEndTime != null; + + BookableSpaceConfig copyWith({ + List? bookableDays, + TimeOfDay? bookingStartTime, + TimeOfDay? bookingEndTime, + int? cost, + bool? availability, + }) { + return BookableSpaceConfig( + configUuid: configUuid, + availability: availability ?? this.availability, + bookableDays: bookableDays ?? this.bookableDays, + cost: cost ?? this.cost, + bookingEndTime: bookingEndTime ?? this.bookingEndTime, + bookingStartTime: bookingStartTime ?? this.bookingStartTime, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart new file mode 100644 index 00000000..70e700be --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart @@ -0,0 +1,58 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; + +class BookableSpacemodel { + final String spaceUuid; + final String spaceName; + final BookableSpaceConfig? spaceConfig; + final String spaceVirtualAddress; + + BookableSpacemodel({ + required this.spaceUuid, + required this.spaceName, + this.spaceConfig, + required this.spaceVirtualAddress, + }); + factory BookableSpacemodel.zero() => BookableSpacemodel( + spaceUuid: '', + spaceName: '', + spaceVirtualAddress: '', + ); + factory BookableSpacemodel.fromJson(Map json) => + BookableSpacemodel( + spaceUuid: json['uuid'] as String, + spaceName: json['spaceName'] as String, + spaceConfig: json['bookableConfig'] == null + ? BookableSpaceConfig.zero() + : BookableSpaceConfig.fromJson( + json['bookableConfig'] as Map), + spaceVirtualAddress: json['virtualLocation'] as String, + ); + + static List fromJsonList(List jsonList) => + jsonList + .map( + (e) => BookableSpacemodel.fromJson(e as Map), + ) + .toList(); + + bool get isValid => + spaceUuid.isNotEmpty && + spaceName.isNotEmpty && + spaceVirtualAddress.isNotEmpty && + spaceConfig != null && + spaceConfig!.isValid; + + BookableSpacemodel copyWith({ + String? spaceUuid, + String? spaceName, + BookableSpaceConfig? spaceConfig, + String? spaceVirtualAddress, + }) { + return BookableSpacemodel( + spaceUuid: spaceUuid ?? this.spaceUuid, + spaceName: spaceName ?? this.spaceName, + spaceConfig: spaceConfig ?? this.spaceConfig, + spaceVirtualAddress: spaceVirtualAddress ?? this.spaceVirtualAddress, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart new file mode 100644 index 00000000..6007424b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart @@ -0,0 +1,10 @@ +class BookableSpacesParam { + final int currentPage; + BookableSpacesParam({ + required this.currentPage, + }); + + Map toJson() => { + 'page': currentPage, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart new file mode 100644 index 00000000..a081ce04 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart @@ -0,0 +1,12 @@ +class NonBookableSpacesParams { +final int currentPage; +final String? searchedWords; + NonBookableSpacesParams({ + required this.currentPage, + this.searchedWords, + }); + + Map toJson() => { + 'page': currentPage, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart new file mode 100644 index 00000000..3e1251dd --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart @@ -0,0 +1,41 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class SendBookableSpacesToApiParams { + final List spaceUuids; + final List daysAvailable; + final String startTime; + final String endTime; + final int points; + SendBookableSpacesToApiParams({ + required this.spaceUuids, + required this.daysAvailable, + required this.startTime, + required this.endTime, + required this.points, + }); + + static SendBookableSpacesToApiParams fromBookableSpacesModel( + List bookableSpaces) { + return SendBookableSpacesToApiParams( + spaceUuids: bookableSpaces.map((space) => space.spaceUuid).toList(), + daysAvailable: bookableSpaces + .expand((space) => space.spaceConfig!.bookableDays) + .toSet() + .toList(), + startTime: formatTimeOfDayTo24HourString( + bookableSpaces.first.spaceConfig!.bookingStartTime!), + endTime: formatTimeOfDayTo24HourString( + bookableSpaces.first.spaceConfig!.bookingEndTime!), + points: bookableSpaces.first.spaceConfig!.cost, + ); + } + + Map toJson() => { + 'spaceUuids': spaceUuids, + 'daysAvailable': daysAvailable, + 'startTime': startTime, + 'endTime': endTime, + 'points': points + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart b/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart new file mode 100644 index 00000000..237f3442 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart @@ -0,0 +1,40 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class UpdateBookableSpaceParam { + final String spaceUuid; + final List? bookableDays; + final String? bookingStartTime; + final String? bookingEndTime; + final int? cost; + final bool? availability; + UpdateBookableSpaceParam({ + required this.spaceUuid, + this.bookingStartTime, + this.bookingEndTime, + this.bookableDays, + this.availability, + this.cost, + }); + factory UpdateBookableSpaceParam.fromBookableModel( + BookableSpacemodel bookableSpace) { + return UpdateBookableSpaceParam( + spaceUuid: bookableSpace.spaceUuid, + availability: bookableSpace.spaceConfig!.availability, + bookableDays: bookableSpace.spaceConfig!.bookableDays, + cost: bookableSpace.spaceConfig!.cost, + bookingStartTime: formatTimeOfDayTo24HourString( + bookableSpace.spaceConfig!.bookingStartTime!), + bookingEndTime: formatTimeOfDayTo24HourString( + bookableSpace.spaceConfig!.bookingEndTime!), + ); + } + + Map toJson() => { + if (bookableDays != null) 'daysAvailable': bookableDays, + if (bookingStartTime != null) 'startTime': bookingStartTime, + if (bookingEndTime != null) 'endTime': bookingEndTime, + if (cost != null) 'points': cost, + if (availability != null) 'active': availability, + }; +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart new file mode 100644 index 00000000..208589ad --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +abstract class BookableSpacesService { + Future> load( + BookableSpacesParam param); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart new file mode 100644 index 00000000..a5034bbf --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +abstract class NonBookableSpacesService { + Future> load( + NonBookableSpacesParams params); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart new file mode 100644 index 00000000..6b3f40d5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; + +abstract class SendBookableSpacesService{ + Future sendBookableSpacesToApi(SendBookableSpacesToApiParams params); +} \ No newline at end of file diff --git a/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart b/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart new file mode 100644 index 00000000..509c69eb --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; + +abstract class UpdateBookableSpaceService { + Future update(UpdateBookableSpaceParam updateParam); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart new file mode 100644 index 00000000..c947cd69 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart @@ -0,0 +1,66 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'bookable_spaces_event.dart'; +part 'bookable_spaces_state.dart'; + +class BookableSpacesBloc + extends Bloc { + final BookableSpacesService bookableSpacesService; + BookableSpacesBloc(this.bookableSpacesService) + : super(BookableSpacesInitial()) { + on(_onLoadBookableSpaces); + on(_onInsertUpdatedSpaceEven); + } + + Future _onLoadBookableSpaces( + LoadBookableSpacesEvent event, Emitter emit) async { + emit(BookableSpacesLoading()); + try { + final bookableSpaces = await bookableSpacesService.load(event.param); + emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces)); + } on APIException catch (e) { + emit(BookableSpacesError(error: e.message)); + } catch (e) { + emit( + BookableSpacesError(error: e.toString()), + ); + } + } + + void _onInsertUpdatedSpaceEven( + InsertUpdatedSpaceEvent event, Emitter emit) { + emit(InsertingUpdatedSpaceState()); + + if (event.bookableSpace.spaceConfig!.configUuid == + event.updatedBookableSpaceConfig.configUuid) { + final index = event.bookableSpaces.data.indexWhere( + (element) => element.spaceUuid == event.bookableSpace.spaceUuid, + ); + + if (index != -1) { + final original = event.bookableSpaces.data[index]; + + final updatedConfig = original.spaceConfig!.copyWith( + availability: event.updatedBookableSpaceConfig.availability, + bookableDays: event.updatedBookableSpaceConfig.bookableDays, + bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime, + bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime, + cost: event.updatedBookableSpaceConfig.cost, + ); + + final updatedSpace = original.copyWith(spaceConfig: updatedConfig); + + event.bookableSpaces.data[index] = updatedSpace; + } + } + + emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces)); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart new file mode 100644 index 00000000..c73f08dc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_event.dart @@ -0,0 +1,24 @@ +part of 'bookable_spaces_bloc.dart'; + +sealed class BookableSpacesEvent extends Equatable { + const BookableSpacesEvent(); + + @override + List get props => []; +} + +class LoadBookableSpacesEvent extends BookableSpacesEvent { + final BookableSpacesParam param; + const LoadBookableSpacesEvent(this.param); +} + +class InsertUpdatedSpaceEvent extends BookableSpacesEvent { + final PaginatedDataModel bookableSpaces; + final BookableSpacemodel bookableSpace; + final BookableSpaceConfig updatedBookableSpaceConfig; + const InsertUpdatedSpaceEvent({ + required this.bookableSpaces, + required this.bookableSpace, + required this.updatedBookableSpaceConfig, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart new file mode 100644 index 00000000..d22f585a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_state.dart @@ -0,0 +1,28 @@ +part of 'bookable_spaces_bloc.dart'; + +sealed class BookableSpacesState extends Equatable { + const BookableSpacesState(); + + @override + List get props => []; +} + +final class BookableSpacesInitial extends BookableSpacesState {} + +final class BookableSpacesLoading extends BookableSpacesState {} + +final class BookableSpacesLoaded extends BookableSpacesState { + final PaginatedDataModel bookableSpacesList; + const BookableSpacesLoaded({ + required this.bookableSpacesList, + }); +} + +final class BookableSpacesError extends BookableSpacesState { + final String error; + const BookableSpacesError({ + required this.error, + }); +} + +class InsertingUpdatedSpaceState extends BookableSpacesState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart new file mode 100644 index 00000000..608a44ff --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart @@ -0,0 +1,65 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +part 'non_bookaable_spaces_event.dart'; +part 'non_bookaable_spaces_state.dart'; + +class NonBookableSpacesBloc + extends Bloc { + NonBookableSpacesService nonBookableSpacesService; + + NonBookableSpacesBloc(this.nonBookableSpacesService) + : super(NonBookableSpacesInitial()) { + on(_onCallInitStateEvent); + on(_onLoadUnBookableSpacesEvent); + } + + void _onCallInitStateEvent( + CallInitStateEvent event, Emitter emit) { + emit(NonBookableSpacesInitial()); + } + + Future _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event, + Emitter emit) async { + if (state is NonBookableSpacesLoaded) { + final currState = state as NonBookableSpacesLoaded; + try { + emit(NonBookableSpacesLoading( + lastNonBookableSpaces: currState.nonBookableSpaces)); + + final nonBookableSpacesList = await nonBookableSpacesService.load( + event.nonBookableSpacesParams, + ); + nonBookableSpacesList.data.addAll(currState.nonBookableSpaces.data); + + emit( + NonBookableSpacesLoaded( + nonBookableSpaces: nonBookableSpacesList, + ), + ); + } catch (e) { + emit( + NonBookableSpacesError(e.toString()), + ); + } + } else { + try { + emit(const NonBookableSpacesLoading()); + final nonBookableSpacesList = await nonBookableSpacesService.load( + event.nonBookableSpacesParams, + ); + emit( + NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList), + ); + } catch (e) { + emit( + NonBookableSpacesError(e.toString()), + ); + } + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart new file mode 100644 index 00000000..e185fae6 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_event.dart @@ -0,0 +1,17 @@ +part of 'non_bookaable_spaces_bloc.dart'; + +sealed class NonBookableSpacesEvent extends Equatable { + const NonBookableSpacesEvent(); + + @override + List get props => []; +} + +class CallInitStateEvent extends NonBookableSpacesEvent {} + +class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent { + final NonBookableSpacesParams nonBookableSpacesParams; + const LoadUnBookableSpacesEvent({ + required this.nonBookableSpacesParams, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart new file mode 100644 index 00000000..a5804f9d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_state.dart @@ -0,0 +1,32 @@ +part of 'non_bookaable_spaces_bloc.dart'; + +sealed class NonBookableSpacesState extends Equatable { + const NonBookableSpacesState(); + + @override + List get props => []; +} + +final class NonBookableSpacesInitial extends NonBookableSpacesState {} + +class NonBookableSpacesLoading extends NonBookableSpacesState { + final PaginatedDataModel? lastNonBookableSpaces; + const NonBookableSpacesLoading({ + this.lastNonBookableSpaces, + }); +} + +class NonBookableSpacesLoaded extends NonBookableSpacesState { + final PaginatedDataModel nonBookableSpaces; + final List selectedBookableSpaces; + const NonBookableSpacesLoaded({ + required this.nonBookableSpaces, + this.selectedBookableSpaces = const [], + }); +} + +class NonBookableSpacesError extends NonBookableSpacesState { + final String error; + const NonBookableSpacesError(this.error); +} + diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart new file mode 100644 index 00000000..a2993db9 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart @@ -0,0 +1,33 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart'; + +part 'send_bookable_spaces_event.dart'; +part 'send_bookable_spaces_state.dart'; + +class SendBookableSpacesBloc + extends Bloc { + SendBookableSpacesService sendBookableSpacesService; + SendBookableSpacesBloc( + this.sendBookableSpacesService, + ) : super(SendBookableSpacesInitial()) { + on(_onSendBookableSpacesToApi); + } + Future _onSendBookableSpacesToApi(SendBookableSpacesToApi event, + Emitter emit) async { + emit(SendBookableSpacesLoading()); + try { + await sendBookableSpacesService.sendBookableSpacesToApi( + SendBookableSpacesToApiParams.fromBookableSpacesModel( + event.selectedBookableSpaces), + ); + emit(SendBookableSpacesSuccess()); + } catch (e) { + emit( + SendBookableSpacesError(e.toString()), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart new file mode 100644 index 00000000..3e82ea0b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_event.dart @@ -0,0 +1,13 @@ +part of 'send_bookable_spaces_bloc.dart'; + +sealed class SendBookableSpacesEvent extends Equatable { + const SendBookableSpacesEvent(); + + @override + List get props => []; +} + +class SendBookableSpacesToApi extends SendBookableSpacesEvent { + final List selectedBookableSpaces; + const SendBookableSpacesToApi({required this.selectedBookableSpaces}); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart new file mode 100644 index 00000000..2fce5476 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_state.dart @@ -0,0 +1,19 @@ +part of 'send_bookable_spaces_bloc.dart'; + +sealed class SendBookableSpacesState extends Equatable { + const SendBookableSpacesState(); + + @override + List get props => []; +} + +final class SendBookableSpacesInitial extends SendBookableSpacesState {} + +class SendBookableSpacesLoading extends SendBookableSpacesState {} + +class SendBookableSpacesSuccess extends SendBookableSpacesState {} + +class SendBookableSpacesError extends SendBookableSpacesState { + final String error; + const SendBookableSpacesError(this.error); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart new file mode 100644 index 00000000..7ba4eb0a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart @@ -0,0 +1,132 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart'; + +part 'setup_bookable_spaces_event.dart'; +part 'setup_bookable_spaces_state.dart'; + +class SetupBookableSpacesBloc + extends Bloc { + NonBookableSpacesService nonBookableSpacesService; + + SetupBookableSpacesBloc(this.nonBookableSpacesService) + : super(const SetupBookableSpacesInitial(bookableSpaces: [])) { + on(_onAddToBookableSpaceEvent); + on(_onRemoveFromBookableSpaceEvent); + on(_onAddBookableDays); + on(_onChangeStartTimeEvent); + on(_onChangeEndTimeEvent); + on(_onChangeCostEvent); + + on(_onCheckConfigurValidityEvent); + on(_onEditModeSelected); + } + List get currentBookableSpaces { + return switch (state) { + AddNonBookableSpaceIntoBookableState(:final bookableSpaces) => + bookableSpaces, + RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) => + bookableSpaces, + SetupBookableSpacesInitial(:final bookableSpaces) => bookableSpaces, + InProgressState(:final bookableSpaces) => bookableSpaces, + ValidSaveButtonState(:final bookableSpaces) => bookableSpaces, + UnValidSaveButtonState(:final bookableSpaces) => bookableSpaces, + }; + } + + void _onAddToBookableSpaceEvent( + AddToBookableSpaceEvent event, + Emitter emit, + ) { + emit(InProgressState(bookableSpaces: state.bookableSpaces)); + final updatedSpaces = List.from(state.bookableSpaces); + + updatedSpaces.add(event.nonBookableSpace); + + emit(AddNonBookableSpaceIntoBookableState(bookableSpaces: updatedSpaces)); + } + + void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event, + Emitter emit) { + emit(InProgressState(bookableSpaces: state.bookableSpaces)); + state.bookableSpaces.remove(event.bookableSpace); + emit(RemoveBookableSpaceIntoNonBookableState( + bookableSpaces: state.bookableSpaces)); + } + + void _onAddBookableDays( + AddBookableDaysEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookableDays: event.bookableDays, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeStartTimeEvent( + ChangeStartTimeEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookingStartTime: event.startTime, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeEndTimeEvent( + ChangeEndTimeEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + bookingEndTime: event.endTime, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onChangeCostEvent( + ChangeCostEvent event, Emitter emit) { + final updatedSpaces = state.bookableSpaces.map((space) { + final updatedConfig = space.spaceConfig?.copyWith( + cost: event.cost, + ); + + return space.copyWith(spaceConfig: updatedConfig); + }).toList(); + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces)); + } + + void _onCheckConfigurValidityEvent(CheckConfigurValidityEvent event, + Emitter emit) { + if (state.bookableSpaces.first.spaceConfig!.isValid) { + emit(ValidSaveButtonState( + bookableSpaces: state.bookableSpaces, + )); + } else { + emit(UnValidSaveButtonState( + bookableSpaces: state.bookableSpaces, + )); + } + } + + void _onEditModeSelected( + EditModeSelected event, + Emitter emit, + ) { + final updatedList = [event.editingBookableSpace]; + + emit(SetupBookableSpacesInitial(bookableSpaces: updatedList)); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart new file mode 100644 index 00000000..8bc66f95 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_event.dart @@ -0,0 +1,59 @@ +part of 'setup_bookable_spaces_bloc.dart'; + +sealed class SetupBookableSpacesEvent extends Equatable { + const SetupBookableSpacesEvent(); + + @override + List get props => []; +} + +class AddToBookableSpaceEvent extends SetupBookableSpacesEvent { + final BookableSpacemodel nonBookableSpace; + const AddToBookableSpaceEvent({ + required this.nonBookableSpace, + }); +} + +class RemoveFromBookableSpaceEvent extends SetupBookableSpacesEvent { + final BookableSpacemodel bookableSpace; + const RemoveFromBookableSpaceEvent({ + required this.bookableSpace, + }); +} + +class AddBookableDaysEvent extends SetupBookableSpacesEvent { + final List bookableDays; + const AddBookableDaysEvent({ + required this.bookableDays, + }); +} + +class ChangeCostEvent extends SetupBookableSpacesEvent { + final int cost; + const ChangeCostEvent({ + required this.cost, + }); +} + +class ChangeStartTimeEvent extends SetupBookableSpacesEvent { + final TimeOfDay startTime; + const ChangeStartTimeEvent({ + required this.startTime, + }); +} + +class ChangeEndTimeEvent extends SetupBookableSpacesEvent { + final TimeOfDay endTime; + const ChangeEndTimeEvent({ + required this.endTime, + }); +} + +class CheckConfigurValidityEvent extends SetupBookableSpacesEvent {} + +class EditModeSelected extends SetupBookableSpacesEvent { + final BookableSpacemodel editingBookableSpace; + const EditModeSelected({ + required this.editingBookableSpace, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart new file mode 100644 index 00000000..5b04f075 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_state.dart @@ -0,0 +1,37 @@ +part of 'setup_bookable_spaces_bloc.dart'; + +sealed class SetupBookableSpacesState extends Equatable { + final List bookableSpaces; + const SetupBookableSpacesState({required this.bookableSpaces}); + TimeOfDay? get startTime => + bookableSpaces.first.spaceConfig!.bookingStartTime; + TimeOfDay? get endTime => bookableSpaces.first.spaceConfig!.bookingEndTime; + @override + List get props => []; +} + +final class SetupBookableSpacesInitial extends SetupBookableSpacesState { + const SetupBookableSpacesInitial({required super.bookableSpaces}); +} + +class AddNonBookableSpaceIntoBookableState extends SetupBookableSpacesState { + const AddNonBookableSpaceIntoBookableState({required super.bookableSpaces}); +} + +class InProgressState extends SetupBookableSpacesState { + const InProgressState({required super.bookableSpaces}); +} + +class RemoveBookableSpaceIntoNonBookableState extends SetupBookableSpacesState { + const RemoveBookableSpaceIntoNonBookableState({ + required super.bookableSpaces, + }); +} + +class ValidSaveButtonState extends SetupBookableSpacesState { + const ValidSaveButtonState({required super.bookableSpaces}); +} + +class UnValidSaveButtonState extends SetupBookableSpacesState { + const UnValidSaveButtonState({required super.bookableSpaces}); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart new file mode 100644 index 00000000..4258f5aa --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart @@ -0,0 +1,22 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'steps_state.dart'; + +class StepsCubit extends Cubit { + StepsCubit() : super(StepOneState()); + + void initDialogValue() { + emit(StepOneState()); + } + + void editValueInit() { + emit(StepTwoState()); + } + + void goToNextStep() { + if (state is StepOneState) { + emit(StepTwoState()); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart new file mode 100644 index 00000000..183fdb88 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_state.dart @@ -0,0 +1,12 @@ +part of 'steps_cubit.dart'; + +sealed class StepsState extends Equatable { + const StepsState(); + + @override + List get props => []; +} + +final class StepOneState extends StepsState {} + +final class StepTwoState extends StepsState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart new file mode 100644 index 00000000..09a11e5e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart @@ -0,0 +1,16 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'toggle_points_switch_state.dart'; + +class TogglePointsSwitchCubit extends Cubit { + TogglePointsSwitchCubit() : super(UnActivatePointsSwitch()); + + void activateSwitch() { + emit(ActivatePointsSwitch()); + } + + void unActivateSwitch() { + emit(UnActivatePointsSwitch()); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart new file mode 100644 index 00000000..be078660 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_state.dart @@ -0,0 +1,13 @@ +part of 'toggle_points_switch_cubit.dart'; + +sealed class TogglePointsSwitchState extends Equatable { + const TogglePointsSwitchState(); + + @override + List get props => []; +} + + +class ActivatePointsSwitch extends TogglePointsSwitchState {} + +class UnActivatePointsSwitch extends TogglePointsSwitchState {} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart new file mode 100644 index 00000000..39e42b5b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart @@ -0,0 +1,36 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'update_bookable_spaces_event.dart'; +part 'update_bookable_spaces_state.dart'; + +class UpdateBookableSpacesBloc + extends Bloc { + final UpdateBookableSpaceService updateBookableSpaceService; + UpdateBookableSpacesBloc(this.updateBookableSpaceService) + : super(UpdateBookableSpacesInitial()) { + on(_onUpdateBookableSpace); + } + + Future _onUpdateBookableSpace(UpdateBookableSpace event, + Emitter emit) async { + emit(UpdateBookableSpaceLoading(event.updatedParam.spaceUuid)); + try { + final updatedSpace = + await updateBookableSpaceService.update(event.updatedParam); + + emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace)); + event.onSuccess?.call(); + } on APIException catch (e) { + emit(UpdateBookableSpaceFailure(error: e.message)); + } catch (e) { + emit( + UpdateBookableSpaceFailure(error: e.toString()), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart new file mode 100644 index 00000000..1abcea0b --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_event.dart @@ -0,0 +1,17 @@ +part of 'update_bookable_spaces_bloc.dart'; + +sealed class UpdateBookableSpaceEvent extends Equatable { + const UpdateBookableSpaceEvent(); + + @override + List get props => []; +} + +class UpdateBookableSpace extends UpdateBookableSpaceEvent { + final void Function()? onSuccess; + final UpdateBookableSpaceParam updatedParam; + const UpdateBookableSpace({ + required this.updatedParam, + this.onSuccess, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart new file mode 100644 index 00000000..e173dd21 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_state.dart @@ -0,0 +1,30 @@ +part of 'update_bookable_spaces_bloc.dart'; + +sealed class UpdateBookableSpacesState extends Equatable { + const UpdateBookableSpacesState(); + + @override + List get props => []; +} + +final class UpdateBookableSpacesInitial extends UpdateBookableSpacesState {} + +final class UpdateBookableSpaceLoading extends UpdateBookableSpacesState { + final String updatingSpaceUuid; + + const UpdateBookableSpaceLoading(this.updatingSpaceUuid); +} + +final class UpdateBookableSpaceSuccess extends UpdateBookableSpacesState { + final BookableSpaceConfig bookableSpaceConfig; + const UpdateBookableSpaceSuccess({ + required this.bookableSpaceConfig, + }); +} + +final class UpdateBookableSpaceFailure extends UpdateBookableSpacesState { + final String error; + const UpdateBookableSpaceFailure({ + required this.error, + }); +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart new file mode 100644 index 00000000..155049e4 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart'; + +import 'package:syncrow_web/services/api/http_service.dart'; + +class ManageBookableSpacesPage extends StatefulWidget { + final PageController pageController; + const ManageBookableSpacesPage({ + super.key, + required this.pageController, + }); + + @override + State createState() => + _ManageBookableSpacesPageState(); +} + +class _ManageBookableSpacesPageState extends State { + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => BookableSpacesBloc( + RemoteBookableSpacesService(HTTPService()), + )..add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + ), + ), + ), + BlocProvider( + create: (context) => UpdateBookableSpacesBloc( + RemoteUpdateBookableSpaceService(HTTPService()), + ), + ) + ], + child: ManageBookableSpacesWidget( + pageController: widget.pageController, + ), + ); + } +} + +class ManageBookableSpacesWidget extends StatelessWidget { + final PageController pageController; + + const ManageBookableSpacesWidget({ + super.key, + required this.pageController, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 35, + ), + child: Column( + children: [ + Expanded( + flex: 10, + child: RowOfButtonsTitleWidget(pageController: pageController)), + const SizedBox( + height: 10, + ), + const Expanded( + flex: 85, + child: TableOfBookableSpacesWidget(), + ), + const SizedBox( + height: 15, + ), + const Expanded( + flex: 5, + child: PaginationButtonsWidget(), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart new file mode 100644 index 00000000..07be929f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SetupBookableSpacesDialog extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + SetupBookableSpacesDialog({ + super.key, + this.editingBookableSpace, + }); + + @override + State createState() => + _SetupBookableSpacesDialogState(); +} + +class _SetupBookableSpacesDialogState extends State { + final TextEditingController pointsController = TextEditingController(); + @override + void dispose() { + pointsController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: widget.editingBookableSpace == null + ? (context) => StepsCubit()..initDialogValue() + : (context) => StepsCubit()..editValueInit(), + ), + BlocProvider( + create: (context) => NonBookableSpacesBloc( + NonBookableSpacesDebouncerDecoratorService( + RemoteNonBookableSpaces(HTTPService()), + ), + )..add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: + NonBookableSpacesParams(currentPage: 1), + ), + ), + ), + BlocProvider( + create: widget.editingBookableSpace == null + ? (context) => SetupBookableSpacesBloc( + RemoteNonBookableSpaces(HTTPService())) + : (context) => SetupBookableSpacesBloc( + RemoteNonBookableSpaces(HTTPService())) + ..add( + EditModeSelected( + editingBookableSpace: widget.editingBookableSpace!), + ), + ), + BlocProvider( + create: (context) => SendBookableSpacesBloc( + RemoteSendBookableSpaces(HTTPService()), + ), + ) + ], + child: AlertDialog( + backgroundColor: ColorsManager.whiteColors, + contentPadding: EdgeInsets.zero, + title: Center( + child: Text( + 'Set Up a Bookable Spaces', + style: TextStyle( + fontWeight: FontWeight.w700, + color: ColorsManager.dialogBlueTitle, + fontSize: 15, + ), + ), + ), + content: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Divider(), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Expanded( + flex: 3, + child: StepperPartWidget(), + ), + const SizedBox( + height: 588, + child: VerticalDivider( + thickness: 0.5, + width: 1, + ), + ), + Expanded( + flex: 7, + child: DetailsStepsWidget( + pointsController: pointsController, + editingBookableSpace: widget.editingBookableSpace, + ), + ) + ], + ), + Builder(builder: (context) { + final stepsState = context.watch().state; + final bookableSpaces = + context.watch().state.bookableSpaces; + return stepsState is StepOneState + ? const NextFirstStepButton() + : SaveSecondStepButton( + pointsController: pointsController, + isEditingMode: widget.editingBookableSpace != null, + bookableSpaces: bookableSpaces, + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart new file mode 100644 index 00000000..738a2ecc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class BookableSpaceSwitchActivationWidget extends StatelessWidget { + final PaginatedDataModel bookableSpaces; + final BookableSpacemodel space; + const BookableSpaceSwitchActivationWidget({ + super.key, + required this.bookableSpaces, + required this.space, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Transform.scale( + scale: 0.7, + child: + BlocConsumer( + listener: (context, updateState) { + if (updateState is UpdateBookableSpaceSuccess) { + context.read().add( + InsertUpdatedSpaceEvent( + bookableSpaces: bookableSpaces, + bookableSpace: space, + updatedBookableSpaceConfig: + updateState.bookableSpaceConfig, + ), + ); + } + }, + builder: (context, updateState) { + final isLoading = updateState is UpdateBookableSpaceLoading && + updateState.updatingSpaceUuid == space.spaceUuid; + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + return Switch( + trackOutlineColor: WidgetStateProperty.resolveWith( + (Set states) { + return ColorsManager.whiteColors; + }), + value: space.spaceConfig!.availability, + activeTrackColor: ColorsManager.dialogBlueTitle, + inactiveTrackColor: ColorsManager.grayBorder, + thumbColor: WidgetStateProperty.resolveWith( + (Set states) { + return ColorsManager.whiteColors; + }), + onChanged: (value) { + context.read().add( + UpdateBookableSpace( + updatedParam: UpdateBookableSpaceParam( + spaceUuid: space.spaceUuid, + availability: value, + )), + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart new file mode 100644 index 00000000..c434fc3d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/string_utils.dart'; + +class BookingPeriodWidget extends StatelessWidget { + final BookableSpacemodel? editingBookableSpace; + const BookingPeriodWidget({ + super.key, + this.editingBookableSpace, + }); + + @override + Widget build(BuildContext context) { + final state = context.watch().state; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + const Text('Booking Period'), + ], + ), + const SizedBox(height: 5), + Container( + width: 230, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: ColorsManager.circleRolesBackground, + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 4, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues(alpha: 0.15), + ) + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TimePickerWidget( + title: editingBookableSpace?.spaceConfig?.bookingStartTime + ?.format(context) ?? + 'Start Time', + onTimePicked: (pickedStartTime) { + if (pickedStartTime == null) return; + + if (state.endTime != null && + isEndTimeAfterStartTime( + pickedStartTime, state.endTime!)) { + _showInvalidSnackBar( + context, "You can't choose Start Time after End Time"); + return; + } + + context.read().add( + ChangeStartTimeEvent(startTime: pickedStartTime), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 10), + const Icon( + Icons.arrow_right_alt, + color: ColorsManager.grayColor, + size: 13, + ), + TimePickerWidget( + title: editingBookableSpace?.spaceConfig?.bookingEndTime + ?.format(context) ?? + 'End Time', + onTimePicked: (pickedEndTime) { + if (pickedEndTime == null) return; + + if (state.startTime != null && + isEndTimeAfterStartTime( + state.startTime!, pickedEndTime)) { + _showInvalidSnackBar( + context, "You can't choose End Time before Start Time"); + return; + } + + context.read().add( + ChangeEndTimeEvent(endTime: pickedEndTime), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 15), + Container( + width: 30, + height: 32, + alignment: Alignment.center, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + child: SvgPicture.asset( + Assets.clockIcon, + height: 18, + color: ColorsManager.blackColor.withValues(alpha: 0.4), + ), + ), + ], + ), + ), + ], + ); + } + + void _showInvalidSnackBar(BuildContext context, String message) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 2), + backgroundColor: ColorsManager.red, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart new file mode 100644 index 00000000..0e13370f --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonsDividerBottomDialogWidget extends StatelessWidget { + final String title; + final void Function()? onNextPressed; + final void Function() onCancelPressed; + const ButtonsDividerBottomDialogWidget({ + super.key, + required this.title, + required this.onNextPressed, + required this.onCancelPressed, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider( + thickness: 0.5, + height: 1, + ), + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + onTap: onCancelPressed, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.blackColor), + ), + ), + ), + ), + Expanded( + child: + BlocConsumer( + listener: (context, nonBookableState) { + if (nonBookableState is NonBookableSpacesInitial) { + context.pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Operation Done Successfully', + style: TextStyle(color: ColorsManager.activeGreen), + ), + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + ), + ); + } else if (nonBookableState is NonBookableSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + nonBookableState.error, + style: const TextStyle(color: ColorsManager.red), + ), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + builder: (context, nonBookableState) { + return TextButton( + onPressed: onNextPressed, + child: Text( + title, + ), + ); + }, + ), + ) + ], + ) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart new file mode 100644 index 00000000..862b11fa --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CheckBoxSpaceWidget extends StatelessWidget { + final BookableSpacemodel nonBookableSpace; + + const CheckBoxSpaceWidget({ + super.key, + required this.nonBookableSpace, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + BlocBuilder( + builder: (context, state) { + final isChecked = switch (state) { + AddNonBookableSpaceIntoBookableState( + bookableSpaces: final spaces + ) => + spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid), + RemoveBookableSpaceIntoNonBookableState( + bookableSpaces: final spaces + ) => + spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid), + _ => false, + }; + + return CustomCheckboxWidget( + value: isChecked, + onChanged: (value) { + final bloc = context.read(); + + if (value ?? false) { + bloc.add(AddToBookableSpaceEvent( + nonBookableSpace: nonBookableSpace, + )); + } else { + bloc.add(RemoveFromBookableSpaceEvent( + bookableSpace: nonBookableSpace, + )); + } + }, + ); + }, + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + nonBookableSpace.spaceName, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 12, + color: ColorsManager.titleGray, + ), + ), + Text( + nonBookableSpace.spaceVirtualAddress, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 10, + color: ColorsManager.titleGray, + ), + ), + ], + )), + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart new file mode 100644 index 00000000..5525d6a4 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ColumnTitleWidget extends StatelessWidget { + final bool isFirst; + final bool isLast; + final String title; + const ColumnTitleWidget({ + super.key, + required this.title, + required this.isFirst, + required this.isLast, + }); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 10), + decoration: BoxDecoration( + color: ColorsManager.graysColor, + borderRadius: isFirst + ? const BorderRadius.only( + topLeft: Radius.circular(12), + ) + : isLast + ? const BorderRadius.only( + topRight: Radius.circular(12), + ) + : null, + ), + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 12, + ), + )); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart new file mode 100644 index 00000000..35e600e8 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CustomCheckboxWidget extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + final double? outHeight; + final double? outWidth; + final double? iconSize; + const CustomCheckboxWidget({ + super.key, + required this.value, + required this.onChanged, + this.outWidth, + this.outHeight, + this.iconSize, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), + child: Container( + width: outWidth ?? 17, + height: outHeight ?? 17, + decoration: BoxDecoration( + color: value ? Colors.white : ColorsManager.checkBoxFillColor, + border: value + ? Border.all(color: ColorsManager.secondaryColor, width: 1) + : Border.all(color: ColorsManager.checkBoxBorderGray, width: 1), + borderRadius: BorderRadius.circular(4), + ), + child: value + ? Center( + child: Container( + width: outWidth != null ? outWidth! - 4 : 13, + height: outHeight != null ? outHeight! - 4 : 13, + decoration: BoxDecoration( + color: ColorsManager.secondaryColor, + borderRadius: BorderRadius.circular(2), + ), + child: const Icon( + Icons.check, + size: 12, + color: Colors.white, + ), + ), + ) + : null, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart new file mode 100644 index 00000000..a72104fe --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart @@ -0,0 +1,55 @@ +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CustomDataTable extends StatelessWidget { + final List columnsTitles; + final List Function(T item) cellsWidgets; + final List items; + + const CustomDataTable({ + super.key, + required this.items, + required this.cellsWidgets, + required this.columnsTitles, + }); + + @override + Widget build(BuildContext context) { + return DataTable2( + dividerThickness: 0.5, + columnSpacing: 2, + horizontalMargin: 0, + empty: SvgPicture.asset(Assets.emptyDataTable), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: ColorsManager.textGray, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + columns: columnsTitles.asMap().entries.map((entry) { + final index = entry.key; + final title = entry.value; + + return DataColumn( + label: ColumnTitleWidget( + title: title, + isFirst: index == 0, + isLast: index == columnsTitles.length - 1, + ), + ); + }).toList(), + rows: items.map((item) { + return DataRow(cells: cellsWidgets(item)); + }).toList(), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart new file mode 100644 index 00000000..bbcc5293 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart'; + +class DetailsStepsWidget extends StatelessWidget { + final TextEditingController pointsController; + final BookableSpacemodel? editingBookableSpace; + const DetailsStepsWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), + child: BlocBuilder(builder: (context, state) { + return switch (state) { + StepOneState() => const SpacesStepDetailsWidget(), + StepTwoState() => StepTwoDetailsWidget( + pointsController: pointsController, + editingBookableSpace: editingBookableSpace, + ), + }; + }), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart new file mode 100644 index 00000000..2a014f75 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class EditBookableSpaceButtonWidget extends StatelessWidget { + final BookableSpacemodel? space; + const EditBookableSpaceButtonWidget({ + super.key, + required this.space, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 3, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues( + alpha: 0.3, + ), + ) + ], + ), + child: ElevatedButton( + onPressed: () { + final bookableBloc = context.read(); + + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: bookableBloc, + ), + BlocProvider( + create: (context) => UpdateBookableSpacesBloc( + RemoteUpdateBookableSpaceService(HTTPService()), + ), + ), + ], + child: SetupBookableSpacesDialog( + editingBookableSpace: space, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(45, 30), + elevation: 0, + ), + child: SvgPicture.asset( + Assets.settings, + height: 13, + color: ColorsManager.blue1, + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart new file mode 100644 index 00000000..b41fe106 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PaginationButtonsWidget extends StatelessWidget { + const PaginationButtonsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BookableSpacesLoaded) { + final totalPages = state.bookableSpacesList.totalPages; + final currentPage = state.bookableSpacesList.page; + + List paginationItems = []; + + if (currentPage > 2) { + paginationItems.add( + _buildArrowButton( + label: '«', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage - 2), + ), + ); + }, + ), + ); + } + + if (currentPage > 1) { + paginationItems.add( + _buildArrowButton( + label: '<', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage - 1), + ), + ); + }, + ), + ); + } + + for (int i = 1; i <= totalPages; i++) { + paginationItems.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GestureDetector( + onTap: () { + if (i != currentPage) { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: i), + ), + ); + } + }, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + decoration: BoxDecoration( + color: i == currentPage + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ColorsManager.lightGrayBorderColor, + )), + child: Text( + '$i', + style: TextStyle( + color: i == currentPage ? Colors.white : Colors.black, + fontWeight: i == currentPage + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ), + ), + ); + } + + if (currentPage < totalPages) { + paginationItems.add( + _buildArrowButton( + label: '>', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage + 1), + ), + ); + }, + ), + ); + } + + if (currentPage + 1 < totalPages) { + paginationItems.add( + _buildArrowButton( + label: '»', + onTap: () { + context.read().add( + LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: currentPage + 2), + ), + ); + }, + ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: paginationItems, + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } + + Widget _buildArrowButton( + {required String label, required VoidCallback onTap}) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GestureDetector( + onTap: onTap, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: ColorsManager.lightGrayBorderColor, + )), + child: Text( + label, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart new file mode 100644 index 00000000..a6e206fc --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart'; + +class TableOfBookableSpacesWidget extends StatelessWidget { + const TableOfBookableSpacesWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BookableSpacesLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is BookableSpacesError) { + return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text(state.error), + const SizedBox( + height: 5, + ), + ElevatedButton( + onPressed: () => context + .read() + .add(LoadBookableSpacesEvent( + BookableSpacesParam(currentPage: 1), + )), + child: const Text('Try Again')) + ]); + } else if (state is BookableSpacesLoaded) { + return CustomDataTable( + items: state.bookableSpacesList.data, + cellsWidgets: (space) => [ + DataCell( + DataCellWidget( + title: space.spaceName, + ), + ), + DataCell(DataCellWidget( + title: space.spaceVirtualAddress, + )), + DataCell(Container( + padding: const EdgeInsetsGeometry.only(left: 10), + width: 200, + child: Wrap( + spacing: 4, + children: space.spaceConfig!.bookableDays + .map( + (day) => DataCellWidget(title: day), + ) + .toList(), + ), + )), + DataCell( + DataCellWidget( + title: space.spaceConfig!.bookingStartTime!.format(context), + ), + ), + DataCell( + DataCellWidget( + title: space.spaceConfig!.bookingEndTime!.format(context), + ), + ), + DataCell( + DataCellWidget( + title: '${space.spaceConfig!.cost} Points', + ), + ), + DataCell(BookableSpaceSwitchActivationWidget( + bookableSpaces: state.bookableSpacesList, + space: space, + )), + DataCell(EditBookableSpaceButtonWidget( + space: space, + )), + ], + columnsTitles: const [ + 'Space', + 'Space Virtual Address', + 'Bookable Days', + 'Booking Start Time', + 'Booking End Time', + 'Cost', + 'Availability', + 'Settings', + ], + ); + } else { + return const SizedBox(); + } + }, + ); + } +} + +class DataCellWidget extends StatelessWidget { + final String title; + const DataCellWidget({ + super.key, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsGeometry.only(left: 10), + child: Text( + title, + style: const TextStyle(fontSize: 11), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart new file mode 100644 index 00000000..65a647a5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class RowOfButtonsTitleWidget extends StatelessWidget { + const RowOfButtonsTitleWidget({ + super.key, + required this.pageController, + }); + + final PageController pageController; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsetsGeometry.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + offset: Offset.zero, + blurRadius: 3, + spreadRadius: 0, + color: ColorsManager.timePickerColor.withValues( + alpha: 0.3, + ), + ) + ], + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(50, 40), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: SvgPicture.asset( + Assets.backButtonIcon, + height: 15, + ), + onPressed: () { + pageController.jumpToPage(1); + }), + ), + const SizedBox( + width: 10, + ), + Text( + 'Manage Bookable Spaces', + style: TextStyle( + fontSize: 18, + color: ColorsManager.vividBlue.withValues( + alpha: 0.7, + ), + fontWeight: FontWeight.w700), + ) + ], + ), + SvgTextButton( + padding: const EdgeInsets.all(10), + svgSize: 15, + fontSize: 10, + fontWeight: FontWeight.bold, + svgAsset: Assets.addButtonIcon, + label: 'Set Up a Bookable Spaces', + onPressed: () { + final bloc = context.read(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: SetupBookableSpacesDialog(), + ), + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart new file mode 100644 index 00000000..12e6bb2e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart'; + +class NextFirstStepButton extends StatelessWidget { + const NextFirstStepButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + SetupBookableSpacesInitial() => ButtonsDividerBottomDialogWidget( + title: 'Next', + onNextPressed: null, + onCancelPressed: () => context.pop(), + ), + AddNonBookableSpaceIntoBookableState(:final bookableSpaces) || + RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) => + ButtonsDividerBottomDialogWidget( + title: 'Next', + onNextPressed: bookableSpaces.isEmpty + ? null + : () { + context.read().goToNextStep(); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + onCancelPressed: () => context.pop(), + ), + _ => const SizedBox(), + }; + }, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart new file mode 100644 index 00000000..3df5343d --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PointsPartWidget extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + final TextEditingController pointsController; + + const PointsPartWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + + @override + State createState() => _PointsPartWidgetState(); +} + +class _PointsPartWidgetState extends State { + @override + void initState() { + super.initState(); + + if (widget.editingBookableSpace != null) { + widget.pointsController.text = + widget.editingBookableSpace!.spaceConfig!.cost.toString(); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, switchState) { + final isSwitchOn = switchState is ActivatePointsSwitch; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (isSwitchOn) + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ) + else + const SizedBox(width: 11), + const Text('Points/hrs'), + ], + ), + Transform.scale( + scale: 0.7, + child: Switch( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + trackOutlineColor: + WidgetStateProperty.all(ColorsManager.whiteColors), + activeTrackColor: ColorsManager.dialogBlueTitle, + inactiveTrackColor: ColorsManager.lightGrayBorderColor, + thumbColor: + WidgetStateProperty.all(ColorsManager.whiteColors), + value: isSwitchOn, + onChanged: (value) { + final toggleCubit = + context.read(); + final bloc = context.read(); + + final updatedCost = value ? -1 : 0; + + if (value) { + toggleCubit.activateSwitch(); + } else { + toggleCubit.unActivateSwitch(); + widget.pointsController.clear(); + } + bloc.add(ChangeCostEvent(cost: updatedCost)); + bloc.add(CheckConfigurValidityEvent()); + }, + ), + ), + ], + ), + const SizedBox(height: 5), + if (isSwitchOn) + SearchUnbookableSpacesWidget( + title: 'Ex: 0', + topPadding: 0, + blur: 1, + raduis: 10, + height: 34, + controller: widget.pointsController, + suffix: const SizedBox(), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (_) { + final updatedCost = + int.tryParse(widget.pointsController.text) ?? 0; + context + .read() + .add(ChangeCostEvent(cost: updatedCost)); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ) + else + const SizedBox(), + ], + ); + }, + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart new file mode 100644 index 00000000..de35a38a --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart'; + +class SaveSecondStepButton extends StatelessWidget { + final TextEditingController pointsController; + final bool isEditingMode; + final List bookableSpaces; + + const SaveSecondStepButton({ + super.key, + required this.pointsController, + required this.isEditingMode, + required this.bookableSpaces, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is SendBookableSpacesSuccess) { + context.read().add(CallInitStateEvent()); + } else if (state is SendBookableSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error)), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + return ButtonsDividerBottomDialogWidget( + title: 'Save', + onNextPressed: state is UnValidSaveButtonState + ? null + : () { + if (bookableSpaces.any((e) => e.isValid)) { + if (isEditingMode) { + callEditLogic(context); + } else { + context.read().add( + SendBookableSpacesToApi( + selectedBookableSpaces: bookableSpaces, + ), + ); + } + } + }, + onCancelPressed: () => context.pop(), + ); + }, + ), + ); + } + + void callEditLogic(BuildContext context) { + print(bookableSpaces.first.spaceConfig!.cost); + if (bookableSpaces.isNotEmpty) { + context.read().add( + UpdateBookableSpace( + onSuccess: () => context + .read() + .add(CallInitStateEvent()), + updatedParam: UpdateBookableSpaceParam.fromBookableModel( + bookableSpaces.first, + ), + ), + ); + } + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart new file mode 100644 index 00000000..ba7cea57 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SearchUnbookableSpacesWidget extends StatelessWidget { + final String title; + final Widget? suffix; + final double? height; + final double? width; + final double? blur; + final double? raduis; + final double? topPadding; + final TextEditingController? controller; + final List? inputFormatters; + final void Function(String)? onChanged; + const SearchUnbookableSpacesWidget({ + required this.title, + this.controller, + this.blur, + this.onChanged, + this.suffix, + this.height, + this.width, + this.topPadding, + this.raduis, + this.inputFormatters, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width ?? 480, + height: height ?? 40, + padding: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(raduis ?? 15), + boxShadow: [ + BoxShadow( + color: + ColorsManager.shadowOfSearchTextfield.withValues(alpha: 0.15), + offset: Offset.zero, + blurRadius: blur ?? 5, + spreadRadius: 0, + ), + ], + ), + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + onChanged: onChanged, + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: topPadding ?? 5, + horizontal: 15, + ), + hintText: title, + hintStyle: const TextStyle(color: ColorsManager.hintTextGrey), + border: InputBorder.none, + suffixIcon: Padding( + padding: const EdgeInsets.all(10), + child: suffix ?? + SvgPicture.asset( + Assets.searchIcon, + height: 12, + ), + ), + ), + style: const TextStyle( + fontSize: 14, + color: ColorsManager.hintTextGrey, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart new file mode 100644 index 00000000..60ba2441 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesStepDetailsWidget extends StatefulWidget { + const SpacesStepDetailsWidget({ + super.key, + }); + + @override + State createState() => + _SpacesStepDetailsWidgetState(); +} + +class _SpacesStepDetailsWidgetState extends State { + ScrollController scrollController = ScrollController(); + int currentPage = 1; + String? currentSearchTerm; + bool isLoadingMore = false; + + @override + void initState() { + super.initState(); + + scrollController.addListener(() { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 100) { + final state = context.read().state; + if (state is NonBookableSpacesLoaded && + state.nonBookableSpaces.hasNext && + !isLoadingMore) { + isLoadingMore = true; + currentPage++; + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + } + } + }); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select Space', + style: TextStyle( + fontWeight: FontWeight.w700, + color: ColorsManager.blackColor, + ), + ), + const SizedBox( + height: 20, + ), + Container( + width: 450, + height: 480, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: ColorsManager.shadowOfDetailsContainer, + offset: Offset.zero, + blurRadius: 5, + ), + ], + ), + child: Column( + children: [ + Container( + width: 520, + height: 70, + padding: + const EdgeInsets.symmetric(vertical: 15, horizontal: 20), + decoration: const BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: SearchUnbookableSpacesWidget( + title: 'Search', + onChanged: (p0) { + currentSearchTerm = p0; + currentPage = 1; + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + }, + ), + ), + Expanded( + child: + BlocConsumer( + listener: (context, state) { + if (state is NonBookableSpacesLoaded) { + isLoadingMore = false; + } + }, + builder: (context, state) { + return switch (state) { + NonBookableSpacesError(error: final error) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(error), + const SizedBox(height: 5), + ElevatedButton( + onPressed: () { + context.read().add( + LoadUnBookableSpacesEvent( + nonBookableSpacesParams: + NonBookableSpacesParams( + currentPage: currentPage, + searchedWords: currentSearchTerm, + ), + ), + ); + }, + child: const Text('Try Again'), + ), + ], + ), + NonBookableSpacesLoading(lastNonBookableSpaces: null) => + const Center(child: CircularProgressIndicator()), + NonBookableSpacesLoading( + lastNonBookableSpaces: final spaces + ) => + UnbookableListWidget( + scrollController: scrollController, + nonBookableSpaces: spaces!, + ), + NonBookableSpacesLoaded( + nonBookableSpaces: final spaces + ) => + UnbookableListWidget( + scrollController: scrollController, + nonBookableSpaces: spaces, + ), + _ => const SizedBox(), + }; + }, + ), + ) + ], + ), + ) + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart new file mode 100644 index 00000000..a1db9694 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart'; + +class StepTwoDetailsWidget extends StatelessWidget { + final TextEditingController pointsController; + final BookableSpacemodel? editingBookableSpace; + const StepTwoDetailsWidget({ + super.key, + required this.pointsController, + this.editingBookableSpace, + }); + @override + Widget build(BuildContext context) { + return SizedBox( + width: 450, + height: 480, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + WeekDaysCheckboxRow( + editingBookableSpace: editingBookableSpace, + ), + const SizedBox( + height: 30, + ), + BookingPeriodWidget( + editingBookableSpace: editingBookableSpace, + ), + const SizedBox( + height: 20, + ), + BlocProvider( + create: editingBookableSpace == null + ? (context) => TogglePointsSwitchCubit()..activateSwitch() + : editingBookableSpace!.spaceConfig!.cost == 0 + ? (context) => TogglePointsSwitchCubit()..unActivateSwitch() + : (context) => TogglePointsSwitchCubit()..activateSwitch(), + child: PointsPartWidget( + pointsController: pointsController, + editingBookableSpace: editingBookableSpace), + ) + ], + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart new file mode 100644 index 00000000..89f9d1c5 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class StepperPartWidget extends StatelessWidget { + const StepperPartWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(20), + padding: const EdgeInsetsGeometry.only(left: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is StepOneState) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + const CircleTitleStepperWidget( + title: 'Space', + ), + Container( + padding: const EdgeInsets.only(left: 3), + alignment: Alignment.centerLeft, + height: 40, + child: const VerticalDivider( + width: 8, + )), + const CircleTitleStepperWidget( + title: 'Settings', + titleColor: ColorsManager.softGray, + circleColor: ColorsManager.whiteColors, + borderColor: ColorsManager.textGray, + ) + ], + ); + } else if (state is StepTwoState) { + return Column( + children: [ + const SizedBox( + height: 10, + ), + const CircleTitleStepperWidget( + title: 'Space', + titleColor: ColorsManager.softGray, + cicleIcon: Icon( + Icons.check, + color: ColorsManager.whiteColors, + size: 12, + ), + circleColor: ColorsManager.trueIconGreen, + radius: 15, + borderColor: ColorsManager.trueIconGreen, + ), + Container( + padding: const EdgeInsets.only(left: 3), + alignment: Alignment.centerLeft, + height: 40, + child: const VerticalDivider( + width: 8, + )), + const CircleTitleStepperWidget( + title: 'Settings', + ) + ], + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} + +class CircleTitleStepperWidget extends StatelessWidget { + final double? radius; + final Widget? cicleIcon; + final Color? circleColor; + final Color? borderColor; + final Color? titleColor; + final String title; + const CircleTitleStepperWidget({ + super.key, + required this.title, + this.circleColor, + this.borderColor, + this.cicleIcon, + this.titleColor, + this.radius, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: radius ?? 15, + height: radius ?? 15, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: circleColor ?? ColorsManager.blue1, + border: Border.all(color: borderColor ?? ColorsManager.blue1)), + child: cicleIcon, + ), + const SizedBox( + width: 10, + ), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.w700, + color: titleColor ?? ColorsManager.blackColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart new file mode 100644 index 00000000..c215079e --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; + +import 'package:syncrow_web/utils/color_manager.dart'; + +class TimePickerWidget extends StatefulWidget { + final String title; + TimePickerWidget({ + super.key, + required this.onTimePicked, + required this.title, + }); + late final SetupBookableSpacesBloc setupBookableSpacesBloc; + final void Function(TimeOfDay? timePicked) onTimePicked; + @override + State createState() => _TimePickerWidgetState(); +} + +class _TimePickerWidgetState extends State { + TimeOfDay? timePicked; + @override + void initState() { + widget.setupBookableSpacesBloc = context.read(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () async { + final tempTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, + ), + ), + child: child!, + ); + }, + ); + + if (tempTime == null) return; + + widget.onTimePicked(tempTime); + timePicked = tempTime; + + widget.setupBookableSpacesBloc.add(CheckConfigurValidityEvent()); + + setState(() {}); + }, + child: Container( + height: 32, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + timePicked == null ? widget.title : timePicked!.format(context), + style: TextStyle( + color: ColorsManager.blackColor.withValues(alpha: 0.4), + fontSize: 12, + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart new file mode 100644 index 00000000..2aa09b24 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; + +class UnbookableListWidget extends StatelessWidget { + final PaginatedDataModel nonBookableSpaces; + const UnbookableListWidget({ + super.key, + required this.scrollController, + required this.nonBookableSpaces, + }); + + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return Container( + width: 490, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20), + ), + ), + padding: const EdgeInsets.only(top: 10, left: 20, bottom: 5), + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox( + height: 5, + ), + controller: scrollController, + itemCount: nonBookableSpaces.data.length, + itemBuilder: (context, index) { + if (index < nonBookableSpaces.data.length) { + return CheckBoxSpaceWidget( + nonBookableSpace: nonBookableSpaces.data[index], + + ); + } else { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Center(child: CircularProgressIndicator()), + ); + } + }, + ), + ); + } +} diff --git a/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart new file mode 100644 index 00000000..c01dc714 --- /dev/null +++ b/lib/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart'; + +class WeekDaysCheckboxRow extends StatefulWidget { + final BookableSpacemodel? editingBookableSpace; + const WeekDaysCheckboxRow({ + super.key, + this.editingBookableSpace, + }); + + @override + State createState() => _WeekDaysCheckboxRowState(); +} + +class _WeekDaysCheckboxRowState extends State { + final Map _daysChecked = { + 'Mon': false, + 'Tue': false, + 'Wed': false, + 'Thu': false, + 'Fri': false, + 'Sat': false, + 'Sun': false, + }; + + @override + void initState() { + super.initState(); + + final existingDays = + widget.editingBookableSpace?.spaceConfig?.bookableDays ?? []; + + for (var day in _daysChecked.keys) { + if (existingDays.contains(day)) { + _daysChecked[day] = true; + } + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + const Text('Days'), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _daysChecked.entries.map((entry) { + return Expanded( + child: Row( + children: [ + CustomCheckboxWidget( + outHeight: 16, + outWidth: 16, + value: entry.value, + onChanged: (newValue) { + setState(() { + _daysChecked[entry.key] = newValue ?? false; + }); + + final selectedDays = _daysChecked.entries + .where((e) => e.value) + .map((e) => e.key) + .toList(); + + context.read().add( + AddBookableDaysEvent(bookableDays: selectedDays), + ); + context.read().add( + CheckConfigurValidityEvent(), + ); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + entry.key, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index 4e31f23f..a6f8a603 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; -import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; +import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart' hide BookingPage; import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -71,9 +73,14 @@ class _AccessManagementPageState extends State scaffoldBody: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), - children: const [ - AccessOverviewContent(), - BookingPage(), + children: [ + const AccessOverviewContent(), + BookingPage( + pageController: _pageController, + ), + ManageBookableSpacesPage( + pageController: _pageController, + ), ], ), ), diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 93f8998e..6c02e889 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -132,6 +132,8 @@ class _DynamicTableState extends State { child: SingleChildScrollView( controller: _horizontalScrollController, scrollDirection: Axis.horizontal, + physics: + widget.isEmpty ? const NeverScrollableScrollPhysics() : null, child: SizedBox( width: _totalTableWidth, child: Column( @@ -164,7 +166,6 @@ class _DynamicTableState extends State { ], ), ), - Expanded( child: widget.isEmpty ? _buildEmptyState() @@ -265,7 +266,7 @@ class _DynamicTableState extends State { ), ], ), - SizedBox(height: widget.size.height * 0.5), + SizedBox(height: widget.size.height * 0.2), ], ), ); diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index d8cd04df..f79528f8 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -53,8 +53,9 @@ class DeviceManagementBloc for (var community in spaceBloc.state.selectedCommunities) { final spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; - devices.addAll(await DevicesManagementApi() - .fetchDevices(projectUuid, spacesId: spacesList)); + devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid, + spacesId: spacesList, + communities: spaceBloc.state.selectedCommunities)); } } @@ -158,7 +159,8 @@ class DeviceManagementBloc add(FilterDevices(_getFilterFromIndex(_selectedIndex))); } - void _onSelectDevice(SelectDevice event, Emitter emit) { + void _onSelectDevice( + SelectDevice event, Emitter emit) { final selectedUuid = event.selectedDevice.uuid; if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { @@ -254,7 +256,8 @@ class DeviceManagementBloc _onlineCount = _devices.where((device) => device.online == true).length; _offlineCount = _devices.where((device) => device.online == false).length; _lowBatteryCount = _devices - .where((device) => device.batteryLevel != null && device.batteryLevel! < 20) + .where((device) => + device.batteryLevel != null && device.batteryLevel! < 20) .length; } @@ -271,7 +274,8 @@ class DeviceManagementBloc } } - void _onSearchDevices(SearchDevices event, Emitter emit) { + void _onSearchDevices( + SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) && (event.deviceNameOrProductName == null || @@ -435,8 +439,8 @@ class DeviceManagementBloc final selectedDevices = loaded.selectedDevice?.map((device) { if (device.uuid == event.deviceId) { return device.copyWith( - subspace: - device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + subspace: device.subspace + ?.copyWith(subspaceName: event.newSubSpaceName)); } return device; }).toList(); diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index 8210fb2f..17650f36 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout { } class _DeviceManagementPageState extends State { - -@override + @override void initState() { context.read().add(InitialEvent()); super.initState(); } + @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State { const TriggerSwitchTabsEvent(isRoutineTab: true)); }, child: Text( - 'Routines', + 'Workflow Automation', style: context.textTheme.titleMedium?.copyWith( color: state.routineTab ? ColorsManager.whiteColors diff --git a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart index 30d9bf5d..86ca1317 100644 --- a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart +++ b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart @@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_ import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; @@ -94,11 +95,18 @@ class GarageDoorControlView extends StatelessWidget FetchGarageDoorSchedulesEvent( deviceId: deviceId, category: 'doorcontact_state'), ); - showDialog( + + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), - child: BuildGarageDoorScheduleView(status: status), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'Timer', + code: 'doorcontact_state', + countdownCode: 'Timer', + deviceType: 'GD', + ), )); }, name: 'Scheduling', diff --git a/lib/pages/device_managment/gateway/view/gateway_view.dart b/lib/pages/device_managment/gateway/view/gateway_view.dart index d674e4d8..372f190a 100644 --- a/lib/pages/device_managment/gateway/view/gateway_view.dart +++ b/lib/pages/device_managment/gateway/view/gateway_view.dart @@ -100,6 +100,7 @@ class _DeviceItem extends StatelessWidget { @override Widget build(BuildContext context) { + return DeviceControlsContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 62bef920..11b2ebbb 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -287,7 +287,8 @@ class ScheduleBloc extends Bloc { try { if (state is ScheduleLoaded) { Status status = Status(code: '', value: ''); - if (event.deviceType == 'CUR_2') { + if (event.deviceType == 'CUR_2' || + event.deviceType == 'GD' ) { status = status.copyWith( code: 'control', value: event.functionOn == true ? 'open' : 'close'); diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index 8fa1e290..752f2243 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -69,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget { countDownCode: countDownCode), ); }, - backgroundColor: ColorsManager.primaryColorWithOpacity, + backgroundColor: ColorsManager.secondaryColor, child: const Text('Save'), ), ), diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index e8dc5e79..c8fe1357 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -63,7 +63,7 @@ class InchingModeButtons extends StatelessWidget { ), ); }, - backgroundColor: ColorsManager.primaryColor, + backgroundColor: ColorsManager.secondaryColor, child: const Text('Save'), ), ), diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index d5194f35..5103e9d0 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -31,11 +31,12 @@ class BuildScheduleView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ScheduleBloc(deviceId: deviceUuid,) + create: (_) => ScheduleBloc( + deviceId: deviceUuid, + ) ..add(ScheduleGetEvent(category: category)) ..add(ScheduleFetchStatusEvent( - deviceId: deviceUuid, - countdownCode: countdownCode ?? '')), + deviceId: deviceUuid, countdownCode: countdownCode ?? '')), child: Dialog( backgroundColor: Colors.white, insetPadding: const EdgeInsets.all(20), @@ -56,7 +57,7 @@ class BuildScheduleView extends StatelessWidget { children: [ const ScheduleHeader(), const SizedBox(height: 20), - if (deviceType == 'CUR_2') + if (deviceType == 'CUR_2' || deviceType == 'GD') const SizedBox() else ScheduleModeSelector( @@ -76,8 +77,7 @@ class BuildScheduleView extends StatelessWidget { category: category, time: '', function: Status( - code: code.toString(), - value: true), + code: code.toString(), value: true), days: [], ), isEdit: false, @@ -96,7 +96,7 @@ class BuildScheduleView extends StatelessWidget { } }, ), - if (deviceType != 'CUR_2') + if (deviceType != 'CUR_2'|| deviceType != 'GD') if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) CountdownInchingView( diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 39899fe5..924524f0 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -24,12 +24,13 @@ class ScheduleManagementUI extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 170, + width: 177, height: 40, child: DefaultButton( - borderColor: ColorsManager.grayColor.withOpacity(0.5), - padding: 2, - backgroundColor: ColorsManager.graysColor, + borderWidth: 4, + borderColor: ColorsManager.neutralGray, + padding: 8, + backgroundColor: ColorsManager.textFieldGreyColor, borderRadius: 15, onPressed: onAddSchedule, child: Row( diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart index f1df1f20..76accf60 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart @@ -39,7 +39,7 @@ class ScheduleModeButtons extends StatelessWidget { borderRadius: 8, height: 40, onPressed: onSave, - backgroundColor: ColorsManager.primaryColorWithOpacity, + backgroundColor: ColorsManager.secondaryColor, child: const Text('Save'), ), ), diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index c1771c1b..8dc4d211 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -194,7 +194,7 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - if (deviceType == 'CUR_2') + if (deviceType == 'CUR_2' || deviceType == 'GD') Center( child: Text(schedule.function.value == true ? 'open' : 'close')) else diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index e5695b9e..6b5ff033 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -23,7 +23,7 @@ class ScheduleDialogHelper { required String deviceType, }) { bool temp; - if (deviceType == 'CUR_2') { + if (deviceType == 'CUR_2' || deviceType == 'GD') { temp = schedule!.function.value == 'open' ? true : false; } else { temp = schedule!.function.value; @@ -116,7 +116,7 @@ class ScheduleDialogHelper { ScheduleModeButtons( onSave: () { dynamic temp; - if (deviceType == 'CUR_2') { + if (deviceType == 'CUR_2' || deviceType == 'GD') { temp = functionOn! ? 'open' : 'close'; } else { temp = functionOn; @@ -202,18 +202,23 @@ class ScheduleDialogHelper { ), const SizedBox(width: 10), Radio( + activeColor: ColorsManager.secondaryColor, + focusColor: ColorsManager.secondaryColor, value: true, groupValue: isOn, onChanged: (val) => onChanged(true), ), - Text(categor == 'CUR_2' ? 'open' : 'On'), + Text(categor == 'CUR_2' || categor == 'GD' ? 'open' : 'On'), const SizedBox(width: 10), Radio( + activeColor: ColorsManager.secondaryColor, + focusColor: ColorsManager.secondaryColor, + value: false, groupValue: isOn, onChanged: (val) => onChanged(false), ), - Text(categor == 'CUR_2' ? 'close' : 'Off'), + Text(categor == 'CUR_2' || categor == 'GD' ? 'close' : 'Off'), ], ); } diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 971f4f8c..a26a2715 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -937,13 +937,17 @@ class RoutineBloc extends Bloc { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - devices.addAll(await DevicesManagementApi() - .fetchDevices(projectUuid, spacesId: spacesList)); + devices.addAll(await DevicesManagementApi().fetchDevices( + projectUuid, + spacesId: spacesList, + communities: spaceBloc.state.selectedCommunities, + )); } } else { devices.addAll(await DevicesManagementApi().fetchDevices( projectUuid, spacesId: [createRoutineBloc.selectedSpaceId], + communities: spaceBloc.state.selectedCommunities, )); } diff --git a/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart b/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart new file mode 100644 index 00000000..de5ce34f --- /dev/null +++ b/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart @@ -0,0 +1,71 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +abstract final class SpacesRecursiveHelper { + const SpacesRecursiveHelper._(); + + static List recusrivelyUpdate( + List spaces, + SpaceDetailsModel updatedSpace, + ) { + return spaces.map((space) { + final isUpdatedSpace = space.uuid == updatedSpace.uuid; + if (isUpdatedSpace) { + return space.copyWith( + spaceName: updatedSpace.spaceName, + icon: updatedSpace.icon, + ); + } + final hasChildren = space.children.isNotEmpty; + if (hasChildren) { + return space.copyWith( + children: recusrivelyUpdate(space.children, updatedSpace), + ); + } + return space; + }).toList(); + } + + static List recusrivelyDelete( + List spaces, + String spaceUuid, + ) { + final updatedSpaces = spaces.map((space) { + if (space.uuid == spaceUuid) return null; + if (space.children.isNotEmpty) { + return space.copyWith( + children: recusrivelyDelete(space.children, spaceUuid), + ); + } + return space; + }).toList(); + final nonNullSpaces = updatedSpaces.whereType().toList(); + return nonNullSpaces; + } + + static List recursivelyInsert({ + required List spaces, + required String parentUuid, + required SpaceModel newSpace, + }) { + return spaces.map((space) { + final isParentSpace = space.uuid == parentUuid; + if (isParentSpace) { + return space.copyWith( + children: [...space.children, newSpace], + ); + } + final hasChildren = space.children.isNotEmpty; + if (hasChildren) { + return space.copyWith( + children: recursivelyInsert( + spaces: space.children, + parentUuid: parentUuid, + newSpace: newSpace, + ), + ); + } + return space; + }).toList(); + } +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index e9fa0a15..ed797c74 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart'; class SpacesConnectionsArrowPainter extends CustomPainter { final List connections; final Map positions; - final double cardWidth = 150.0; + final Map cardWidths; final double cardHeight = 90.0; final Set highlightedUuids; SpacesConnectionsArrowPainter({ required this.connections, required this.positions, + required this.cardWidths, required this.highlightedUuids, }); @@ -29,19 +30,30 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final from = positions[connection.from]; final to = positions[connection.to]; + final fromWidth = cardWidths[connection.from] ?? 150.0; + final toWidth = cardWidths[connection.to] ?? 150.0; if (from != null && to != null) { final startPoint = - Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); - final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + Offset(from.dx + fromWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + toWidth / 2, to.dy); final path = Path()..moveTo(startPoint.dx, startPoint.dy); - final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); - final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); - - path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, - controlPoint2.dy, endPoint.dx, endPoint.dy); + if ((startPoint.dx - endPoint.dx).abs() < 1.0) { + path.lineTo(endPoint.dx, endPoint.dy); + } else { + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100); + path.cubicTo( + controlPoint1.dx, + controlPoint1.dy, + controlPoint2.dx, + controlPoint2.dy, + endPoint.dx, + endPoint.dy, + ); + } canvas.drawPath(path, paint); @@ -51,7 +63,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { : ColorsManager.blackColor.withValues(alpha: 0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.srcIn; - canvas.drawCircle(endPoint, 4, circlePaint); + canvas.drawCircle(endPoint, 6, circlePaint); } } } diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart index ac35975d..80fcee90 100644 --- a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -40,4 +40,22 @@ class PaginatedDataModel extends Equatable { totalItems, totalPages, ]; + + PaginatedDataModel copyWith({ + List? data, + int? page, + int? size, + bool? hasNext, + int? totalItems, + int? totalPages, + }) { + return PaginatedDataModel( + data: data ?? this.data, + page: page ?? this.page, + size: size ?? this.size, + hasNext: hasNext ?? this.hasNext, + totalItems: totalItems ?? this.totalItems, + totalPages: totalPages ?? this.totalPages, + ); + } } diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 40a37891..47a67c36 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -9,8 +9,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; @@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget { class _SpaceManagementPageState extends State { late final CommunitiesBloc communitiesBloc; + late final HTTPService _httpService; @override void initState() { + _httpService = HTTPService(); communitiesBloc = CommunitiesBloc( communitiesService: DebouncedCommunitiesService( - RemoteCommunitiesService(HTTPService()), + RemoteCommunitiesService(_httpService), ), )..add(const LoadCommunities(LoadCommunitiesParam())); - super.initState(); } @@ -49,14 +52,19 @@ class _SpaceManagementPageState extends State { ), BlocProvider( create: (context) => SpaceDetailsBloc( - UniqueSubspacesDecorator( - RemoteSpaceDetailsService(httpService: HTTPService()), + UniqueSpaceDetailsSpacesDecoratorService( + RemoteSpaceDetailsService(httpService: _httpService), ), ), ), BlocProvider( create: (context) => ProductsBloc( - RemoteProductsService(HTTPService()), + RemoteProductsService(_httpService), + ), + ), + BlocProvider( + create: (context) => ReorderSpacesBloc( + RemoteReorderSpacesService(_httpService), ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 3cf761ad..6614aa88 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; @@ -10,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -30,10 +33,11 @@ class CommunityStructureCanvas extends StatefulWidget { class _CommunityStructureCanvasState extends State with SingleTickerProviderStateMixin { final Map _positions = {}; - final double _cardWidth = 150.0; + final Map _cardWidths = {}; final double _cardHeight = 90.0; final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; + static const double _minCardWidth = 150.0; late final TransformationController _transformationController; late final AnimationController _animationController; @@ -52,6 +56,7 @@ class _CommunityStructureCanvasState extends State @override void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { super.didUpdateWidget(oldWidget); + if (widget.selectedSpace == null) return; if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -68,6 +73,34 @@ class _CommunityStructureCanvasState extends State super.dispose(); } + double _calculateCardWidth(String text) { + final style = context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ); + final textPainter = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + + const iconWidth = 40.0; + const horizontalPadding = 10.0; + const contentPadding = 10.0; + final calculatedWidth = + iconWidth + horizontalPadding + textPainter.width + contentPadding; + + return calculatedWidth.clamp(_minCardWidth, double.infinity); + } + + void _calculateAllCardWidths(List spaces) { + for (final space in spaces) { + _cardWidths[space.uuid] = _calculateCardWidth(space.spaceName); + if (space.children.isNotEmpty) { + _calculateAllCardWidths(space.children); + } + } + } + Set _getAllDescendantUuids(SpaceModel space) { final uuids = {}; for (final child in space.children) { @@ -102,11 +135,12 @@ class _CommunityStructureCanvasState extends State final position = _positions[space.uuid]; if (position == null) return; + final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth; const scale = 1; final viewSize = context.size; if (viewSize == null) return; - final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final x = -position.dx * scale + (viewSize.width / 2) - (cardWidth * scale / 2); final y = -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); @@ -132,6 +166,16 @@ class _CommunityStructureCanvasState extends State context.read().add( CommunitiesUpdateCommunity(newCommunity), ); + + context.read().add( + ReorderSpacesEvent( + ReorderSpacesParam( + communityUuid: widget.community.uuid, + parentSpaceUuid: data.parent?.uuid ?? '', + spaces: children, + ), + ), + ); } void _onSpaceTapped(SpaceModel? space) { @@ -155,13 +199,16 @@ class _CommunityStructureCanvasState extends State Map levelXOffset, ) { for (final space in spaces) { + final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth; double childSubtreeWidth = 0; if (space.children.isNotEmpty) { _calculateLayout(space.children, depth + 1, levelXOffset); final firstChildPos = _positions[space.children.first.uuid]; final lastChildPos = _positions[space.children.last.uuid]; if (firstChildPos != null && lastChildPos != null) { - childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + final lastChildWidth = + _cardWidths[space.children.last.uuid] ?? _minCardWidth; + childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx; } } @@ -170,7 +217,7 @@ class _CommunityStructureCanvasState extends State if (space.children.isNotEmpty) { final firstChildPos = _positions[space.children.first.uuid]!; - x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + x = firstChildPos.dx + (childSubtreeWidth - cardWidth) / 2; } else { x = currentX; } @@ -187,7 +234,7 @@ class _CommunityStructureCanvasState extends State final y = depth * (_verticalSpacing + _cardHeight); _positions[space.uuid] = Offset(x, y); - levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + levelXOffset[depth] = x + cardWidth + _horizontalSpacing; } } @@ -202,11 +249,21 @@ class _CommunityStructureCanvasState extends State List _buildTreeWidgets() { _positions.clear(); + _cardWidths.clear(); final community = widget.community; + _calculateAllCardWidths(community.spaces); + final levelXOffset = {}; _calculateLayout(community.spaces, 0, levelXOffset); + const horizontalCanvasPadding = 100.0; + final originalPositions = Map.of(_positions); + _positions.clear(); + for (final entry in originalPositions.entries) { + _positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0); + } + final selectedSpace = widget.selectedSpace; final highlightedUuids = {}; if (selectedSpace != null) { @@ -224,14 +281,14 @@ class _CommunityStructureCanvasState extends State community: widget.community, ); - final createButtonX = levelXOffset[0] ?? 0.0; + final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding; const createButtonY = 0.0; widgets.add( Positioned( left: createButtonX, top: createButtonY, - child: CreateSpaceButton(communityUuid: widget.community.uuid), + child: CreateSpaceButton(community: widget.community), ), ); @@ -240,6 +297,7 @@ class _CommunityStructureCanvasState extends State painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, + cardWidths: _cardWidths, highlightedUuids: highlightedUuids, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), @@ -255,10 +313,12 @@ class _CommunityStructureCanvasState extends State CommunityModel? community, SpaceModel? parent, }) { + const targetWidth = 40.0; + final padding = (_horizontalSpacing - targetWidth) / 2; if (spaces.isNotEmpty) { final firstChildPos = _positions[spaces.first.uuid]!; final targetPos = Offset( - firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dx - padding - targetWidth, firstChildPos.dy, ); widgets.add(_buildDropTarget(parent, community, 0, targetPos)); @@ -271,6 +331,7 @@ class _CommunityStructureCanvasState extends State continue; } + final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth; final isHighlighted = highlightedUuids.contains(space.uuid); final hasNoSelectedSpace = widget.selectedSpace == null; @@ -278,20 +339,29 @@ class _CommunityStructureCanvasState extends State buildSpaceContainer: () { return Opacity( opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: Tooltip( - message: space.spaceName, - preferBelow: false, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, - ), + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, ), ); }, onTap: () => SpaceDetailsDialogHelper.showCreate( context, communityUuid: widget.community.uuid, + parentUuid: space.uuid, + onSuccess: (updatedSpaceModel) { + final updatedSpaces = SpacesRecursiveHelper.recursivelyInsert( + spaces: widget.community.spaces, + parentUuid: space.uuid, + newSpace: updatedSpaceModel, + ); + context.read().add( + CommunitiesUpdateCommunity( + widget.community.copyWith(spaces: updatedSpaces), + ), + ); + }, ), ); @@ -305,7 +375,7 @@ class _CommunityStructureCanvasState extends State Positioned( left: position.dx, top: position.dy, - width: _cardWidth, + width: cardWidth, height: _cardHeight, child: Draggable( data: reorderData, @@ -314,7 +384,7 @@ class _CommunityStructureCanvasState extends State child: Opacity( opacity: 0.2, child: SizedBox( - width: _cardWidth, + width: cardWidth, height: _cardHeight, child: spaceCard, ), @@ -330,7 +400,7 @@ class _CommunityStructureCanvasState extends State ); final targetPos = Offset( - position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, + position.dx + cardWidth + padding, position.dy, ); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); @@ -365,24 +435,33 @@ class _CommunityStructureCanvasState extends State child: DragTarget( builder: (context, candidateData, rejectedData) { if (_draggedData == null) { - return const SizedBox(); + return const SizedBox.shrink(); } - final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && - _draggedData?.community == null) || - (_draggedData?.community?.uuid == community?.uuid && - _draggedData?.parent == null); + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid && + _draggedData!.community == null) || + (_draggedData!.community?.uuid == community?.uuid && + _draggedData!.parent == null); - if (!isTargetForDragged) { - return const SizedBox(); + if (!isSameParent) { + return const SizedBox.shrink(); } - return Container( + final oldIndex = + children.indexWhere((s) => s.uuid == _draggedData!.space.uuid); + if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) { + return const SizedBox.shrink(); + } + + return AnimatedContainer( + duration: const Duration(milliseconds: 150), width: 40, + alignment: Alignment.center, height: _cardHeight, decoration: BoxDecoration( color: context.theme.colorScheme.primary.withValues( - alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + alpha: candidateData.isNotEmpty ? 0.9 : 0.3, ), borderRadius: BorderRadius.circular(8), ), @@ -405,6 +484,9 @@ class _CommunityStructureCanvasState extends State final oldIndex = children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == -1) { + return true; + } if (oldIndex == index || oldIndex == index - 1) { return false; } @@ -418,21 +500,21 @@ class _CommunityStructureCanvasState extends State @override Widget build(BuildContext context) { final treeWidgets = _buildTreeWidgets(); - return InteractiveViewer( - transformationController: _transformationController, - boundaryMargin: EdgeInsets.symmetric( - horizontal: context.screenWidth * 0.3, - vertical: context.screenHeight * 0.3, - ), - minScale: 0.5, - maxScale: 3.0, - constrained: false, - child: GestureDetector( - onTap: _resetSelectionAndZoom, + return GestureDetector( + onTap: _resetSelectionAndZoom, + child: InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: context.screenWidth * 0.3, + vertical: context.screenHeight * 0.3, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, child: SizedBox( width: context.screenWidth * 5, height: context.screenHeight * 5, - child: Stack(children: treeWidgets), + child: Stack(clipBehavior: Clip.none, children: treeWidgets), ), ), ); diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart index cb6271d1..2e1a350e 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart @@ -2,41 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CommunityStructureHeader extends StatelessWidget { const CommunityStructureHeader({super.key}); - List _updateRecursive( - List spaces, - SpaceDetailsModel updatedSpace, - ) { - return spaces.map((space) { - if (space.uuid == updatedSpace.uuid) { - return space.copyWith( - spaceName: updatedSpace.spaceName, - icon: updatedSpace.icon, - ); - } - if (space.children.isNotEmpty) { - return space.copyWith( - children: _updateRecursive(space.children, updatedSpace), - ); - } - return space; - }).toList(); - } - @override Widget build(BuildContext context) { - final theme = Theme.of(context); final screenWidth = MediaQuery.of(context).size.width; return Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), @@ -44,9 +20,9 @@ class CommunityStructureHeader extends StatelessWidget { color: ColorsManager.whiteColors, boxShadow: [ BoxShadow( - color: ColorsManager.shadowBlackColor, - blurRadius: 8, - offset: const Offset(0, 4), + color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, 1), ), ], ), @@ -57,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: _buildCommunityInfo(context, theme, screenWidth), + child: _buildCommunityInfo(context, screenWidth), ), const SizedBox(width: 16), ], @@ -67,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget { ); } - Widget _buildCommunityInfo( - BuildContext context, ThemeData theme, double screenWidth) { + Widget _buildCommunityInfo(BuildContext context, double screenWidth) { final selectedCommunity = context.watch().state.selectedCommunity; final selectedSpace = @@ -78,7 +53,7 @@ class CommunityStructureHeader extends StatelessWidget { children: [ Text( 'Community Structure', - style: theme.textTheme.headlineLarge?.copyWith( + style: context.textTheme.headlineLarge?.copyWith( color: ColorsManager.blackColor, ), ), @@ -91,7 +66,7 @@ class CommunityStructureHeader extends StatelessWidget { Flexible( child: SelectableText( selectedCommunity.name, - style: theme.textTheme.bodyLarge?.copyWith( + style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.blackColor, ), maxLines: 1, @@ -115,27 +90,8 @@ class CommunityStructureHeader extends StatelessWidget { ), ), const SizedBox(width: 8), - CommunityStructureHeaderActionButtons( - onDelete: (space) {}, - onDuplicate: (space) {}, - onEdit: (space) => SpaceDetailsDialogHelper.showEdit( - context, - spaceModel: selectedSpace!, - communityUuid: selectedCommunity.uuid, - onSuccess: (updatedSpaceDetails) { - final communitiesBloc = context.read(); - final updatedSpaces = _updateRecursive( - selectedCommunity.spaces, - updatedSpaceDetails, - ); - - final community = selectedCommunity.copyWith( - spaces: updatedSpaces, - ); - - communitiesBloc.add(CommunitiesUpdateCommunity(community)); - }, - ), + CommunityStructureHeaderActionButtonsComposer( + selectedCommunity: selectedCommunity, selectedSpace: selectedSpace, ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart index a965c866..edeb4d8e 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart @@ -19,27 +19,27 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { + if (selectedSpace == null) return const SizedBox.shrink(); + return Wrap( alignment: WrapAlignment.end, spacing: 10, children: [ - if (selectedSpace != null) ...[ - CommunityStructureHeaderButton( - label: 'Edit', - svgAsset: Assets.editSpace, - onPressed: () => onEdit(selectedSpace!), - ), - CommunityStructureHeaderButton( - label: 'Duplicate', - svgAsset: Assets.duplicate, - onPressed: () => onDuplicate(selectedSpace!), - ), - CommunityStructureHeaderButton( - label: 'Delete', - svgAsset: Assets.spaceDelete, - onPressed: () => onDelete(selectedSpace!), - ), - ], + CommunityStructureHeaderButton( + label: 'Edit', + svgAsset: Assets.editSpace, + onPressed: () => onEdit(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Duplicate', + svgAsset: Assets.duplicate, + onPressed: () => onDuplicate(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Delete', + svgAsset: Assets.spaceDelete, + onPressed: () => onDelete(selectedSpace!), + ), ], ); } diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart new file mode 100644 index 00000000..d7403588 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; + +class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget { + const CommunityStructureHeaderActionButtonsComposer({ + required this.selectedCommunity, + required this.selectedSpace, + super.key, + }); + final CommunityModel selectedCommunity; + final SpaceModel? selectedSpace; + + @override + Widget build(BuildContext context) { + return CommunityStructureHeaderActionButtons( + onDelete: (space) => showDialog( + context: context, + barrierDismissible: false, + builder: (_) => DeleteSpaceDialog( + space: space, + community: selectedCommunity, + onSuccess: () { + final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete( + selectedCommunity.spaces, + space.uuid, + ); + final community = selectedCommunity.copyWith( + spaces: updatedSpaces, + ); + context.read().add( + CommunitiesUpdateCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: selectedCommunity), + ); + }, + ), + ), + onDuplicate: (space) {}, + onEdit: (space) => SpaceDetailsDialogHelper.showEdit( + context, + spaceModel: selectedSpace!, + communityUuid: selectedCommunity.uuid, + onSuccess: (updatedSpaceDetails) { + final communitiesBloc = context.read(); + final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate( + selectedCommunity.spaces, + updatedSpaceDetails, + ); + + final community = selectedCommunity.copyWith( + spaces: updatedSpaces, + ); + + communitiesBloc.add(CommunitiesUpdateCommunity(community)); + }, + ), + selectedSpace: selectedSpace, + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index e6dfbb15..4032c2ab 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateSpaceButton extends StatefulWidget { const CreateSpaceButton({ - required this.communityUuid, + required this.community, super.key, }); - final String communityUuid; + final CommunityModel community; @override State createState() => _CreateSpaceButtonState(); @@ -25,7 +29,21 @@ class _CreateSpaceButtonState extends State { child: InkWell( onTap: () => SpaceDetailsDialogHelper.showCreate( context, - communityUuid: widget.communityUuid, + communityUuid: widget.community.uuid, + onSuccess: (updatedSpaceModel) { + final newCommunity = widget.community.copyWith( + spaces: [...widget.community.spaces, updatedSpaceModel], + ); + context.read().add( + CommunitiesUpdateCommunity(newCommunity), + ); + context.read().add( + SelectSpaceEvent( + space: updatedSpaceModel, + community: newCommunity, + ), + ); + }, ), child: MouseRegion( onEnter: (_) => setState(() => _isHovered = true), diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart index 68169861..236b73c9 100644 --- a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -2,31 +2,22 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class PlusButtonWidget extends StatelessWidget { - final Offset offset; - final void Function() onButtonTap; + final void Function() onTap; const PlusButtonWidget({ + required this.onTap, super.key, - required this.offset, - required this.onButtonTap, }); @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onButtonTap, - child: Container( - width: 30, - height: 30, - decoration: const BoxDecoration( - color: ColorsManager.spaceColor, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.add, - color: ColorsManager.whiteColors, - size: 20, - ), + return IconButton.filled( + onPressed: onTap, + style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor), + icon: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, ), ); } diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart index 54902280..da79b817 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State { widget.buildSpaceContainer(), if (isHovered) Positioned( - bottom: 0, + bottom: -5, child: PlusButtonWidget( - offset: Offset.zero, - onButtonTap: widget.onTap, + onTap: widget.onTap, ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart index 80b18526..3eb6d5df 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -20,21 +20,19 @@ class SpaceCell extends StatelessWidget { return InkWell( onTap: onTap, child: Container( - width: 150, + padding: const EdgeInsetsDirectional.only(end: 10), height: 70, decoration: _containerDecoration(), child: Row( + spacing: 10, + mainAxisSize: MainAxisSize.min, children: [ _buildIconContainer(), - const SizedBox(width: 10), - Expanded( - child: Text( - name, - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - color: ColorsManager.blackColor, - ), - overflow: TextOverflow.ellipsis, + Text( + name, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 050eac87..11478fbe 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -3,6 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; class SpaceManagementCommunityStructure extends StatelessWidget { @@ -10,31 +13,59 @@ class SpaceManagementCommunityStructure extends StatelessWidget { @override Widget build(BuildContext context) { - final selectionBloc = context.watch().state; - final selectedCommunity = selectionBloc.selectedCommunity; - final selectedSpace = selectionBloc.selectedSpace; + return BlocBuilder( + builder: (context, state) { + final selectedCommunity = state.selectedCommunity; + final selectedSpace = state.selectedSpace; + + if (selectedCommunity == null) { + return const SizedBox.shrink(); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CommunityStructureHeader(), + BlocBuilder( + builder: (context, state) { + final community = state.communities.firstWhere( + (element) => element.uuid == selectedCommunity.uuid, + orElse: () => selectedCommunity, + ); + return Visibility( + visible: community.spaces.isNotEmpty, + replacement: _buildEmptyWidget(community), + child: _buildCanvas(community, selectedSpace), + ); + }, + ), + ], + ); + }, + ); + } + + Widget _buildCanvas( + CommunityModel selectedCommunity, + SpaceModel? selectedSpace, + ) { + return Expanded( + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), + ); + } + + Widget _buildEmptyWidget(CommunityModel selectedCommunity) { const spacer = Spacer(flex: 6); - return Visibility( - visible: selectedCommunity!.spaces.isNotEmpty, - replacement: Row( + + return Expanded( + child: Row( children: [ spacer, - Expanded( - child: CreateSpaceButton(communityUuid: selectedCommunity.uuid), - ), - spacer - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CommunityStructureHeader(), - Expanded( - child: CommunityStructureCanvas( - community: selectedCommunity, - selectedSpace: selectedSpace, - ), - ), + Expanded(child: CreateSpaceButton(community: selectedCommunity)), + spacer, ], ), ); diff --git a/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart b/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart new file mode 100644 index 00000000..768f6438 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart @@ -0,0 +1,63 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteCreateSpaceService implements CreateSpaceService { + const RemoteCreateSpaceService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to create space'; + + @override + Future createSpace(CreateSpaceParam param) async { + try { + final path = await _makeUrl(param); + final response = await _httpService.post( + path: path, + body: param.toJson(), + expectedResponseModel: (data) { + final response = data as Map; + final isSuccess = response['success'] as bool; + if (!isSuccess) { + throw APIException(response['error'] as String); + } + + return SpaceModel.fromJson(response['data'] as Map); + }, + ); + + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [ + _defaultErrorMessage, + errorMessage, + ].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } + + Future _makeUrl(CreateSpaceParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + final communityUuid = param.communityUuid; + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + return '/projects/$projectUuid/communities/$communityUuid/spaces'; + } +} diff --git a/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart b/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart new file mode 100644 index 00000000..90a82a6b --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart @@ -0,0 +1,22 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +class CreateSpaceParam { + final String communityUuid; + final SpaceDetailsModel space; + final String? parentUuid; + + const CreateSpaceParam({ + required this.communityUuid, + required this.space, + required this.parentUuid, + }); + + Map toJson() { + return { + 'parentUuid': parentUuid, + ...space.toJson(), + 'x': 0, + 'y': 0, + }; + } +} diff --git a/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart b/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart new file mode 100644 index 00000000..553b87e7 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart'; + +abstract interface class CreateSpaceService { + Future createSpace(CreateSpaceParam param); +} diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart new file mode 100644 index 00000000..46a8abb8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart @@ -0,0 +1,34 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'create_space_event.dart'; +part 'create_space_state.dart'; + +class CreateSpaceBloc extends Bloc { + CreateSpaceBloc( + this._createSpaceService, + ) : super(const CreateSpaceInitial()) { + on(_onCreateSpace); + } + + final CreateSpaceService _createSpaceService; + + Future _onCreateSpace( + CreateSpace event, + Emitter emit, + ) async { + emit(const CreateSpaceLoading()); + try { + final result = await _createSpaceService.createSpace(event.param); + emit(CreateSpaceSuccess(result)); + } on APIException catch (e) { + emit(CreateSpaceFailure(e.message)); + } catch (e) { + emit(CreateSpaceFailure(e.toString())); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart new file mode 100644 index 00000000..09ef8698 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart @@ -0,0 +1,17 @@ +part of 'create_space_bloc.dart'; + +sealed class CreateSpaceEvent extends Equatable { + const CreateSpaceEvent(); + + @override + List get props => []; +} + +final class CreateSpace extends CreateSpaceEvent { + const CreateSpace(this.param); + + final CreateSpaceParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart new file mode 100644 index 00000000..c5b035bb --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart @@ -0,0 +1,31 @@ +part of 'create_space_bloc.dart'; + +sealed class CreateSpaceState extends Equatable { + const CreateSpaceState(); + + @override + List get props => []; +} + +final class CreateSpaceInitial extends CreateSpaceState { + const CreateSpaceInitial(); +} + +final class CreateSpaceLoading extends CreateSpaceState { + const CreateSpaceLoading(); +} + +final class CreateSpaceSuccess extends CreateSpaceState { + const CreateSpaceSuccess(this.space); + + final SpaceModel space; + + @override + List get props => [space]; +} + +final class CreateSpaceFailure extends CreateSpaceState { + const CreateSpaceFailure(this.errorMessage); + + final String errorMessage; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart b/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart new file mode 100644 index 00000000..5320f625 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart @@ -0,0 +1,64 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_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'; + +final class RemoteDeleteSpaceService implements DeleteSpaceService { + const RemoteDeleteSpaceService(this._httpService); + + final HTTPService _httpService; + + @override + Future delete(DeleteSpaceParam param) async { + try { + await _httpService.delete( + path: await _makeUrl(param), + expectedResponseModel: (json) { + final response = json as Map; + final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false; + + if (!hasSuccessfullyDeletedSpace) { + throw APIException('Failed to delete space'); + } + + return hasSuccessfullyDeletedSpace; + }, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + throw APIException(_getErrorMessageFromBody(message)); + } catch (e) { + throw APIException(e.toString()); + } + } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) return 'Failed to delete space'; + final error = body['error'] as Map?; + final errorMessage = error?['message'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl(DeleteSpaceParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + + if (param.communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + if (param.spaceUuid.isEmpty) { + throw APIException('Space UUID is not set'); + } + + return ApiEndpoints.deleteSpace + .replaceAll('{projectId}', projectUuid) + .replaceAll('{communityId}', param.communityUuid) + .replaceAll('{spaceId}', param.spaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart b/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart new file mode 100644 index 00000000..d6781876 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart @@ -0,0 +1,9 @@ +class DeleteSpaceParam { + const DeleteSpaceParam({ + required this.spaceUuid, + required this.communityUuid, + }); + + final String spaceUuid; + final String communityUuid; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart b/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart new file mode 100644 index 00000000..a537645c --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; + +abstract interface class DeleteSpaceService { + Future delete(DeleteSpaceParam param); +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart new file mode 100644 index 00000000..6334bb33 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart @@ -0,0 +1,31 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'delete_space_event.dart'; +part 'delete_space_state.dart'; + +class DeleteSpaceBloc extends Bloc { + DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) { + on(_onDeleteSpace); + } + + final DeleteSpaceService _deleteSpaceService; + + Future _onDeleteSpace( + DeleteSpace event, + Emitter emit, + ) async { + emit(DeleteSpaceLoading()); + try { + await _deleteSpaceService.delete(event.param); + emit(const DeleteSpaceSuccess('Space deleted successfully')); + } on APIException catch (e) { + emit(DeleteSpaceFailure(e.message)); + } catch (e) { + emit(DeleteSpaceFailure(e.toString())); + } + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart new file mode 100644 index 00000000..c80346e8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart @@ -0,0 +1,17 @@ +part of 'delete_space_bloc.dart'; + +sealed class DeleteSpaceEvent extends Equatable { + const DeleteSpaceEvent(); + + @override + List get props => []; +} + +final class DeleteSpace extends DeleteSpaceEvent { + const DeleteSpace(this.param); + + final DeleteSpaceParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart new file mode 100644 index 00000000..96b6d5b7 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart @@ -0,0 +1,30 @@ +part of 'delete_space_bloc.dart'; + +sealed class DeleteSpaceState extends Equatable { + const DeleteSpaceState(); + + @override + List get props => []; +} + +final class DeleteSpaceInitial extends DeleteSpaceState {} + +final class DeleteSpaceLoading extends DeleteSpaceState {} + +final class DeleteSpaceSuccess extends DeleteSpaceState { + const DeleteSpaceSuccess(this.successMessage); + + final String successMessage; + + @override + List get props => [successMessage]; +} + +final class DeleteSpaceFailure extends DeleteSpaceState { + const DeleteSpaceFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart new file mode 100644 index 00000000..f2ddf24a --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeleteSpaceDialog extends StatelessWidget { + const DeleteSpaceDialog({ + required this.space, + required this.community, + required this.onSuccess, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + final void Function() onSuccess; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DeleteSpaceBloc( + RemoteDeleteSpaceService(HTTPService()), + ), + child: Builder( + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + padding: const EdgeInsetsDirectional.all(32), + constraints: BoxConstraints( + maxWidth: context.screenWidth * 0.2, + ), + child: BlocConsumer( + listener: (context, state) { + if (state case DeleteSpaceSuccess()) onSuccess(); + }, + builder: (context, state) => switch (state) { + DeleteSpaceInitial() => DeleteSpaceDialogForm( + space: space, + communityUuid: community.uuid, + ), + DeleteSpaceLoading() => const DeleteSpaceLoadingWidget(), + DeleteSpaceSuccess() => DeleteSpaceStatusWidget( + message: state.successMessage, + icon: const Icon( + Icons.check_circle, + size: 92, + color: ColorsManager.goodGreen, + ), + ), + DeleteSpaceFailure() => DeleteSpaceStatusWidget( + message: state.errorMessage, + icon: const Icon( + Icons.error, + size: 92, + color: ColorsManager.red, + ), + ), + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart new file mode 100644 index 00000000..055b67b8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeleteSpaceDialogForm extends StatelessWidget { + const DeleteSpaceDialogForm({ + required this.space, + required this.communityUuid, + super.key, + }); + + final SpaceModel space; + final String communityUuid; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.xDelete, width: 36, height: 36), + const SizedBox(height: 16), + SelectableText( + 'Delete Space', + textAlign: TextAlign.center, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 24, + ), + ), + const SizedBox(height: 8), + SelectableText( + 'Are you sure you want to delete this space? This action is irreversible', + textAlign: TextAlign.center, + style: context.textTheme.bodyLarge?.copyWith( + color: ColorsManager.lightGreyColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: FilledButton( + style: _buildButtonStyle( + context, + color: ColorsManager.grey25, + textColor: ColorsManager.blackColor, + ), + onPressed: Navigator.of(context).pop, + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + style: _buildButtonStyle( + context, + color: ColorsManager.semiTransparentRed, + textColor: ColorsManager.whiteColors, + ), + onPressed: () { + context.read().add( + DeleteSpace( + DeleteSpaceParam( + spaceUuid: space.uuid, + communityUuid: communityUuid, + ), + ), + ); + }, + child: const Text('Delete'), + ), + ), + ], + ), + ], + ); + } + + ButtonStyle _buildButtonStyle( + BuildContext context, { + required Color color, + required Color textColor, + }) { + return FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + backgroundColor: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: textColor, + textStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart new file mode 100644 index 00000000..b658b3b3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class DeleteSpaceLoadingWidget extends StatelessWidget { + const DeleteSpaceLoadingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox.square( + dimension: 32, + child: Center(child: CircularProgressIndicator()), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart new file mode 100644 index 00000000..d597a451 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeleteSpaceStatusWidget extends StatelessWidget { + const DeleteSpaceStatusWidget({ + required this.message, + required this.icon, + super.key, + }); + + final String message; + final Widget icon; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 16, + children: [ + icon, + SelectableText( + message, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + fontSize: 22, + ), + textAlign: TextAlign.center, + ), + FilledButton( + onPressed: Navigator.of(context).pop, + child: const Text('Close'), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart new file mode 100644 index 00000000..c2494c09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart @@ -0,0 +1,58 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +final class RemoteReorderSpacesService implements ReorderSpacesService { + RemoteReorderSpacesService(this._httpClient); + + final HTTPService _httpClient; + + @override + Future reorderSpaces(ReorderSpacesParam param) async { + try { + await _httpClient.post( + path: await _makeUrl(param), + body: param.toJson(), + expectedResponseModel: (json) => json, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + throw APIException(_getErrorMessageFromBody(message)); + } catch (e) { + throw APIException(e.toString()); + } + } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) return 'Failed to delete space'; + final error = body['error'] as Map?; + final errorMessage = error?['message'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl(ReorderSpacesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + final communityUuid = param.communityUuid; + + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + if (param.parentSpaceUuid.isEmpty) { + throw APIException('Parent Space UUID is not set'); + } + + return ApiEndpoints.reorderSpaces + .replaceAll('{projectUuid}', projectUuid) + .replaceAll('{communityUuid}', communityUuid) + .replaceAll('{parentSpaceUuid}', param.parentSpaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart new file mode 100644 index 00000000..05316006 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class ReorderSpacesParam extends Equatable { + const ReorderSpacesParam({ + required this.communityUuid, + required this.parentSpaceUuid, + required this.spaces, + }); + + final String communityUuid; + final String parentSpaceUuid; + final List spaces; + + @override + List get props => [spaces, communityUuid, parentSpaceUuid]; + + Map toJson() => { + 'spacesUuids': spaces.map((space) => space.uuid).toList(), + }; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart new file mode 100644 index 00000000..46811fae --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; + +abstract interface class ReorderSpacesService { + Future reorderSpaces(ReorderSpacesParam param); +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart new file mode 100644 index 00000000..ecd15898 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'reorder_spaces_event.dart'; +part 'reorder_spaces_state.dart'; + +class ReorderSpacesBloc extends Bloc { + ReorderSpacesBloc( + this._reorderSpacesService, + ) : super(const ReorderSpacesInitial()) { + on(_onReorderSpacesEvent); + } + + final ReorderSpacesService _reorderSpacesService; + + Future _onReorderSpacesEvent( + ReorderSpacesEvent event, + Emitter emit, + ) async { + emit(const ReorderSpacesLoading()); + try { + await _reorderSpacesService.reorderSpaces(event.param); + emit(const ReorderSpacesSuccess()); + } on APIException catch (e) { + emit(ReorderSpacesFailure(e.message)); + } catch (e) { + emit(ReorderSpacesFailure(e.toString())); + } finally { + emit(const ReorderSpacesInitial()); + } + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart new file mode 100644 index 00000000..8cccb4f1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart @@ -0,0 +1,10 @@ +part of 'reorder_spaces_bloc.dart'; + +final class ReorderSpacesEvent extends Equatable { + const ReorderSpacesEvent(this.param); + + final ReorderSpacesParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart new file mode 100644 index 00000000..d237d93c --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart @@ -0,0 +1,29 @@ +part of 'reorder_spaces_bloc.dart'; + +sealed class ReorderSpacesState extends Equatable { + const ReorderSpacesState(); + + @override + List get props => []; +} + +final class ReorderSpacesInitial extends ReorderSpacesState { + const ReorderSpacesInitial(); +} + +final class ReorderSpacesLoading extends ReorderSpacesState { + const ReorderSpacesLoading(); +} + +final class ReorderSpacesSuccess extends ReorderSpacesState { + const ReorderSpacesSuccess(); +} + +final class ReorderSpacesFailure extends ReorderSpacesState { + const ReorderSpacesFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart b/lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart similarity index 65% rename from lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart rename to lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart index 8309c545..da7fd4eb 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart @@ -1,24 +1,29 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; -class UniqueSubspacesDecorator implements SpaceDetailsService { +class UniqueSpaceDetailsSpacesDecoratorService implements SpaceDetailsService { final SpaceDetailsService _decoratee; - const UniqueSubspacesDecorator(this._decoratee); + const UniqueSpaceDetailsSpacesDecoratorService(this._decoratee); @override Future getSpaceDetails(LoadSpaceDetailsParam param) async { final response = await _decoratee.getSpaceDetails(param); final uniqueSubspaces = {}; + final duplicateNames = {}; for (final subspace in response.subspaces) { final normalizedName = subspace.name.trim().toLowerCase(); - if (!uniqueSubspaces.containsKey(normalizedName)) { + if (uniqueSubspaces.containsKey(normalizedName)) { + duplicateNames.add(normalizedName); + } else { uniqueSubspaces[normalizedName] = subspace; } } + duplicateNames.forEach(uniqueSubspaces.remove); return response.copyWith( subspaces: uniqueSubspaces.values.toList(), diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart new file mode 100644 index 00000000..250dad5c --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:uuid/uuid.dart'; + +class ProductAllocation extends Equatable { + final String uuid; + final Product product; + final Tag tag; + + const ProductAllocation({ + required this.uuid, + required this.product, + required this.tag, + }); + + factory ProductAllocation.fromJson(Map json) { + return ProductAllocation( + uuid: json['uuid'] as String? ?? const Uuid().v4(), + product: Product.fromJson(json['product'] as Map), + tag: Tag.fromJson(json['tag'] as Map), + ); + } + + ProductAllocation copyWith({ + String? uuid, + Product? product, + Tag? tag, + }) { + return ProductAllocation( + uuid: uuid ?? this.uuid, + product: product ?? this.product, + tag: tag ?? this.tag, + ); + } + + Map toJson() { + final isNewTag = tag.uuid.isEmpty; + return { + if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid, + 'productUuid': product.uuid, + }; + } + + @override + List get props => [uuid, product, tag]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index ec3c9f81..bd8ff714 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -1,8 +1,7 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:uuid/uuid.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -26,6 +25,7 @@ class SpaceDetailsModel extends Equatable { productAllocations: [], subspaces: [], ); + factory SpaceDetailsModel.fromJson(Map json) { return SpaceDetailsModel( uuid: json['uuid'] as String, @@ -56,78 +56,21 @@ class SpaceDetailsModel extends Equatable { ); } - @override - List get props => [uuid, spaceName, icon, productAllocations, subspaces]; -} - -class ProductAllocation extends Equatable { - final String uuid; - final Product product; - final Tag tag; - - const ProductAllocation({ - required this.uuid, - required this.product, - required this.tag, - }); - - factory ProductAllocation.fromJson(Map json) { - return ProductAllocation( - uuid: json['uuid'] as String? ?? const Uuid().v4(), - product: Product.fromJson(json['product'] as Map), - tag: Tag.fromJson(json['tag'] as Map), - ); - } - - ProductAllocation copyWith({ - String? uuid, - Product? product, - Tag? tag, - }) { - return ProductAllocation( - uuid: uuid ?? this.uuid, - product: product ?? this.product, - tag: tag ?? this.tag, - ); + Map toJson() { + return { + 'spaceName': spaceName, + 'icon': icon, + 'subspaces': subspaces.map((e) => e.toJson()).toList(), + 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), + }; } @override - List get props => [uuid, product, tag]; -} - -class Subspace extends Equatable { - final String uuid; - final String name; - final List productAllocations; - - const Subspace({ - required this.uuid, - required this.name, - required this.productAllocations, - }); - - factory Subspace.fromJson(Map json) { - return Subspace( - uuid: json['uuid'] as String, - name: json['subspaceName'] as String, - productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), - ); - } - - Subspace copyWith({ - String? uuid, - String? name, - List? productAllocations, - }) { - return Subspace( - uuid: uuid ?? this.uuid, - name: name ?? this.name, - productAllocations: productAllocations ?? this.productAllocations, - ); - } - - @override - List get props => [uuid, name, productAllocations]; + List get props => [ + uuid, + spaceName, + icon, + productAllocations, + subspaces, + ]; } diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart new file mode 100644 index 00000000..4a962eb3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; + +class Subspace extends Equatable { + final String uuid; + final String name; + final List productAllocations; + + const Subspace({ + required this.uuid, + required this.name, + required this.productAllocations, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'] as String, + name: json['subspaceName'] as String, + productAllocations: (json['productAllocations'] as List) + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + final isNewSubspace = uuid.endsWith('-NewTag'); + return { + if (!isNewSubspace) 'uuid': uuid, + 'subspaceName': name, + 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), + }; + } + + Subspace copyWith({ + String? uuid, + String? name, + List? productAllocations, + }) { + return Subspace( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + productAllocations: productAllocations ?? this.productAllocations, + ); + } + + @override + List get props => [uuid, name, productAllocations]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index c5de7dad..45cb0e89 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -14,6 +17,8 @@ abstract final class SpaceDetailsDialogHelper { static void showCreate( BuildContext context, { required String communityUuid, + required void Function(SpaceModel updatedSpaceModel)? onSuccess, + String? parentUuid, }) { showDialog( context: context, @@ -24,14 +29,41 @@ abstract final class SpaceDetailsDialogHelper { RemoteSpaceDetailsService(httpService: HTTPService()), ), ), + BlocProvider( + create: (context) => CreateSpaceBloc( + RemoteCreateSpaceService(HTTPService()), + ), + ), ], child: Builder( - builder: (context) => SpaceDetailsDialog( - context: context, - title: const SelectableText('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: (space) {}, - communityUuid: communityUuid, + builder: (context) => BlocListener( + listener: (context, state) => switch (state) { + CreateSpaceInitial() => null, + CreateSpaceLoading() => _onLoading(context), + CreateSpaceSuccess() => _onCreateSuccess( + context, + state.space, + onSuccess, + ), + CreateSpaceFailure() => _onError(context, state.errorMessage), + }, + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: (space) { + context.read().add( + CreateSpace( + CreateSpaceParam( + communityUuid: communityUuid, + space: space, + parentUuid: parentUuid, + ), + ), + ); + }, + communityUuid: communityUuid, + ), ), ), ), @@ -135,4 +167,14 @@ abstract final class SpaceDetailsDialogHelper { ), ); } + + static void _onCreateSuccess( + BuildContext context, + SpaceModel space, + void Function(SpaceModel updatedSpaceModel)? onSuccess, + ) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + onSuccess?.call(space); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart index cf65dbb6..4c8fec4f 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -103,7 +103,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { ).then((resultSpace) { if (resultSpace != null) { if (context.mounted) { - context.read().add(UpdateSpaceDetails(resultSpace)); + context + .read() + .add(UpdateSpaceDetails(resultSpace)); } } }); @@ -133,6 +135,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget { DeviceType.ThreeTouch => Assets.gangSwitch, DeviceType.NCPS => Assets.sensors, DeviceType.PC => Assets.powerClamp, + DeviceType.fourSceen => Assets.fourSceenSwitch, + DeviceType.sixSceen => Assets.sixSceenSwitch, + DeviceType.SOS => Assets.sos, DeviceType.Other => Assets.blackLogo, null => Assets.blackLogo, }; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 68bf68bd..719988c6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/edit_chip.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 8faac548..587c9ea7 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart index 854b79bc..591f741c 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart index a80ddd15..6bc9f6d1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index bf13ffd3..e72bffde 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index 4c9990ae..2c100b15 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -10,7 +10,12 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AddDeviceTypeWidget extends StatefulWidget { - const AddDeviceTypeWidget({super.key}); + const AddDeviceTypeWidget({ + super.key, + this.initialProducts = const [], + }); + + final List initialProducts; @override State createState() => _AddDeviceTypeWidgetState(); @@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget { class _AddDeviceTypeWidgetState extends State { final Map _selectedProducts = {}; + final Map _initialProductCounts = {}; + + @override + void initState() { + super.initState(); + for (final product in widget.initialProducts) { + _initialProductCounts[product] = (_initialProductCounts[product] ?? 0) + 1; + } + _selectedProducts.addAll(_initialProductCounts); + } void _onIncrement(Product product) { setState(() { @@ -27,8 +42,12 @@ class _AddDeviceTypeWidgetState extends State { void _onDecrement(Product product) { setState(() { - if ((_selectedProducts[product] ?? 0) > 0) { - _selectedProducts[product] = _selectedProducts[product]! - 1; + final initialCount = _initialProductCounts[product] ?? 0; + final currentCount = _selectedProducts[product] ?? 0; + if (currentCount > initialCount) { + _selectedProducts[product] = currentCount - 1; + } else if (currentCount > 0 && initialCount == 0) { + _selectedProducts[product] = currentCount - 1; if (_selectedProducts[product] == 0) { _selectedProducts.remove(product); } @@ -63,7 +82,22 @@ class _AddDeviceTypeWidgetState extends State { actions: [ SpaceDetailsActionButtons( onSave: () { - final result = _selectedProducts.entries + final resultMap = {}; + resultMap.addAll(_selectedProducts); + + for (final entry in _initialProductCounts.entries) { + final product = entry.key; + final initialCount = entry.value; + final currentCount = resultMap[product] ?? 0; + + if (currentCount > initialCount) { + resultMap[product] = currentCount - initialCount; + } else { + resultMap.remove(product); + } + } + + final result = resultMap.entries .expand((entry) => List.generate(entry.value, (_) => entry.key)) .toList(); Navigator.of(context).pop(result); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart index 3f6d42ab..d14a3923 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; @@ -205,7 +206,14 @@ class _AssignTagsDialogState extends State { onCancel: () async { final newProducts = await showDialog>( context: context, - builder: (context) => const AddDeviceTypeWidget(), + builder: (context) => AddDeviceTypeWidget( + initialProducts: [ + ..._space.productAllocations.map((e) => e.product), + ..._space.subspaces + .expand((s) => s.productAllocations) + .map((e) => e.product), + ], + ), ); if (newProducts == null || newProducts.isEmpty) return; diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 6e7e2097..1711e019 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/dialog_dropdown.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart'; diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index a70d3b85..2585b1e7 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -34,7 +34,7 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; final formattedErrorMessage = [ _defaultErrorMessage, errorMessage, diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart index 5dd9106d..25d54caa 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -9,34 +9,5 @@ class UpdateSpaceParam { final SpaceDetailsModel space; final String communityUuid; - Map toJson() { - return { - 'spaceName': space.spaceName, - 'icon': space.icon, - 'subspaces': space.subspaces.map((e) => e._toJson()).toList(), - 'productAllocations': - space.productAllocations.map((e) => e._toJson()).toList(), - }; - } -} - -extension _ProductAllocationToJson on ProductAllocation { - Map _toJson() { - final isNewTag = tag.uuid.isEmpty; - return { - if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid, - 'productUuid': product.uuid, - }; - } -} - -extension _SubspaceToJson on Subspace { - Map _toJson() { - final isNewSubspace = uuid.endsWith('-NewTag'); - return { - if (!isNewSubspace) 'uuid': uuid, - 'subspaceName': name, - 'productAllocations': productAllocations.map((e) => e._toJson()).toList(), - }; - } + Map toJson() => space.toJson(); } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 15a22fda..4c11e694 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -1,6 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; part 'space_details_model_event.dart'; diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index c5ee3259..b963f6b4 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -181,7 +181,7 @@ class VisitorPasswordBloc effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { - // END TIME VALIDATION + if (effectiveTimeTimeStamp != null && selectedTimestamp < effectiveTimeTimeStamp!) { await showDialog( diff --git a/lib/pages/visitor_password/model/device_model.dart b/lib/pages/visitor_password/model/device_model.dart index 75d00350..99f84393 100644 --- a/lib/pages/visitor_password/model/device_model.dart +++ b/lib/pages/visitor_password/model/device_model.dart @@ -84,6 +84,14 @@ class DeviceModel { tempIcon = Assets.curtainIcon; } else if (type == DeviceType.Curtain) { tempIcon = Assets.curtainIcon; + } else if (type == DeviceType.fourSceen) { + tempIcon = Assets.fourSceenSwitch; + } else if (type == DeviceType.sixSceen) { + tempIcon = Assets.sixSceenSwitch; + } else if (type == DeviceType.SOS) { + tempIcon = Assets.sos; + } else if (type == DeviceType.NCPS) { + tempIcon = Assets.presenceSensor; } else { tempIcon = Assets.blackLogo; } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 8c74dbb1..684165e2 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -13,11 +13,15 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class DevicesManagementApi { Future> fetchDevices(String projectId, - {List? spacesId}) async { + {List? spacesId, List? communities}) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpaceDevices.replaceAll('{projectId}', projectId), - queryParameters: {if (spacesId != null) 'spaces': spacesId}, + queryParameters: { + if (spacesId != null && spacesId.isNotEmpty) 'spaces': spacesId, + if (communities != null && communities.isNotEmpty) + 'communities': communities, + }, showServerMessage: true, expectedResponseModel: (json) { final List jsonData = json['data'] as List; diff --git a/lib/services/locator.dart b/lib/services/locator.dart index 055deb05..83fa5f56 100644 --- a/lib/services/locator.dart +++ b/lib/services/locator.dart @@ -4,9 +4,7 @@ import 'package:syncrow_web/services/api/http_interceptor.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final GetIt serviceLocator = GetIt.instance; -//setupLocator() // to search for dependency injection in flutter -initialSetup() { +void initialSetup() { serviceLocator.registerSingleton(HTTPInterceptor()); - //Base classes serviceLocator.registerSingleton(HTTPService.setupDioClient()); } diff --git a/lib/syncrow_app.dart b/lib/syncrow_app.dart new file mode 100644 index 00000000..54df3351 --- /dev/null +++ b/lib/syncrow_app.dart @@ -0,0 +1,53 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; +import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; +import 'package:syncrow_web/utils/app_routes.dart'; +import 'package:syncrow_web/utils/navigation_service.dart'; +import 'package:syncrow_web/utils/theme/theme.dart'; + +class SyncrowApp extends StatelessWidget { + const SyncrowApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CreateRoutineBloc(), + ), + BlocProvider( + create: (context) => HomeBloc()..add(const FetchUserInfo()), + ), + BlocProvider( + create: (context) => VisitorPasswordBloc(), + ), + BlocProvider( + create: (context) => RoutineBloc(), + ), + BlocProvider( + create: (context) => SpaceTreeBloc(), + ), + ], + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + scrollBehavior: const MaterialScrollBehavior().copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.unknown, + }, + ), + key: NavigationService.navigatorKey, + theme: myTheme, + routerConfig: AppRoutes.router, + ), + ); + } +} diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..aea52478 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -1,17 +1,31 @@ import 'package:go_router/go_router.dart'; import 'package:syncrow_web/pages/access_management/view/access_management.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/views/analytics_page.dart'; +import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; -class AppRoutes { - static List getRoutes() { - return [ +abstract final class AppRoutes { + const AppRoutes._(); + + static final GoRouter router = GoRouter( + initialLocation: RoutesConst.auth, + redirect: (context, state) async { + final checkToken = await AuthBloc.getTokenAndValidate(); + final loggedIn = checkToken == 'Success'; + final goingToLogin = state.uri.toString() == RoutesConst.auth; + + if (!loggedIn && !goingToLogin) return RoutesConst.auth; + if (loggedIn && goingToLogin) return RoutesConst.home; + + return null; + }, + routes: [ GoRoute( path: RoutesConst.auth, builder: (context, state) => const LoginPage(), @@ -43,6 +57,6 @@ class AppRoutes { name: 'analytics', builder: (context, state) => const AnalyticsPage(), ), - ]; - } + ], + ); } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index a116104a..194a36ba 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -38,6 +38,7 @@ abstract class ColorsManager { static const Color lightGrayColor = Color(0xB2999999); static const Color grayBorder = Color(0xFFCFCFCF); static const Color textGray = Color(0xffD5D5D5); + static const Color titleGray = Color(0xB2999999); static const Color btnColor = Color(0xFF00008B); static const Color blueColor = Color(0xFF0036E6); static const Color boxColor = Color(0xFFF5F6F7); @@ -64,6 +65,7 @@ abstract class ColorsManager { static const Color circleRolesBackground = Color(0xFFF8F8F8); static const Color activeGreen = Color(0xFF99FF93); static const Color activeGreenText = Color(0xFF008905); + static const Color trueIconGreen = Color(0xFFBBEC6C); static const Color disabledPink = Color(0xFFFF9395); static const Color disabledRedText = Color(0xFF890002); static const Color invitedOrange = Color(0xFFFFE193); @@ -88,5 +90,10 @@ abstract class ColorsManager { static const Color grey50 = Color(0xFF718096); static const Color red100 = Color(0xFFFE0202); static const Color grey800 = Color(0xffF8F8F8); + static const Color shadowOfSearchTextfield = Color(0x26000000); + static const Color hintTextGrey = Colors.grey; + static const Color shadowOfDetailsContainer = Color(0x40000000); + static const Color checkBoxBorderGray = Color(0xffD0D0D0); + static const Color timePickerColor = Color(0xff000000); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index b3c8a168..cf7ffbe5 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -41,6 +41,8 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{spaceId}'; static const String getSpaceHierarchy = '/projects/{projectId}/communities/{communityId}/spaces'; + static const String reorderSpaces = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{parentSpaceUuid}/spaces/order'; // Community Module static const String createCommunity = '/projects/{projectId}/communities'; @@ -139,6 +141,12 @@ abstract class ApiEndpoints { '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; - static const String getBookableSpaces = '/bookable-spaces'; + + + ////booking System + static const String bookableSpaces = '/bookable-spaces'; static const String getCalendarEvents = '/api'; + static const String getBookings = + '/bookings?month={mm}%2F{yyyy}&space={space}'; + } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index f92975f3..f4413cda 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -4,6 +4,8 @@ class Assets { static const String webBackground = 'assets/images/web_Background.svg'; static const String webBackgroundPng = 'assets/images/web_Background.png'; static const String blackLogo = 'assets/images/black-logo.png'; + static const String fourSceenSwitch = 'assets/images/4_sceen_switch.svg'; + static const String sixSceenSwitch = 'assets/images/6_sceen_switch.svg'; static const String logo = 'assets/images/Logo.svg'; static const String logoHorizontal = 'assets/images/logo_horizontal.png'; static const String vector = 'assets/images/Vector.png'; @@ -18,6 +20,9 @@ class Assets { 'assets/images/Password_invisible.svg'; static const String visiblePassword = 'assets/images/password_visible.svg'; static const String accessIcon = 'assets/images/access_icon.svg'; + static const String addButtonIcon = 'assets/icons/add_button_Icon.svg'; + static const String backButtonIcon = 'assets/icons/back_button_icon.svg'; + static const String emptyDataTable = 'assets/icons/no_data_table.svg'; static const String spaseManagementIcon = 'assets/images/spase_management_icon.svg'; static const String devicesIcon = 'assets/images/devices_icon.svg'; @@ -205,6 +210,7 @@ class Assets { //assets/icons/ac_lock.svg static const String acLock = 'assets/icons/ac_lock.svg'; + static const String clockIcon = 'assets/icons/clock_icon.svg'; //assets/icons/ac_schedule.svg static const String acSchedule = 'assets/icons/ac_schedule.svg'; @@ -517,4 +523,5 @@ class Assets { static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; static const String homeIcon = 'assets/icons/home_icon.svg'; static const String groupIcon = 'assets/icons/group_icon.svg'; + static const String xDelete = 'assets/icons/x_delete.svg'; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 947e63aa..85b5d5fa 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -21,6 +21,9 @@ enum DeviceType { NCPS, DoorSensor, PC, + fourSceen, + sixSceen, + SOS, Other, } /* @@ -63,4 +66,7 @@ Map devicesTypesMap = { 'WL': DeviceType.WaterLeak, 'NCPS': DeviceType.NCPS, 'PC': DeviceType.PC, + '4S': DeviceType.fourSceen, + '6S': DeviceType.sixSceen, + 'SOS': DeviceType.SOS, }; diff --git a/lib/utils/string_utils.dart b/lib/utils/string_utils.dart index c8d8e2af..5df37e20 100644 --- a/lib/utils/string_utils.dart +++ b/lib/utils/string_utils.dart @@ -1,6 +1,21 @@ +import 'package:flutter/material.dart'; + class StringUtils { static String capitalizeFirstLetter(String text) { if (text.isEmpty) return text; return text[0].toUpperCase() + text.substring(1); } } + +bool isEndTimeAfterStartTime(TimeOfDay start, TimeOfDay end) { + final startMinutes = start.hour * 60 + start.minute; + final endMinutes = end.hour * 60 + end.minute; + + return endMinutes <= startMinutes; +} + +String formatTimeOfDayTo24HourString(TimeOfDay time) { + final hour = time.hour.toString().padLeft(2, '0'); + final minute = time.minute.toString().padLeft(2, '0'); + return '$hour:$minute'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 67bf5328..6736e1bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,7 +44,7 @@ dependencies: flutter_secure_storage: ^9.2.2 shared_preferences: ^2.3.0 dropdown_button2: ^2.3.9 - data_table_2: ^2.5.15 + data_table_2: ^2.6.0 go_router: intl: ^0.20.2 dropdown_search: ^6.0.2