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 new file mode 100644 index 00000000..aa3307d3 --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/remote_calendar_service.dart @@ -0,0 +1,170 @@ +import 'package:dio/dio.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'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; + +class RemoteCalendarService implements CalendarSystemService { + const RemoteCalendarService(this._httpService); + + final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load Calendar'; + + @override + Future getCalendarEvents({ + required String spaceId, + }) async { + 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) { + 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) { + final errorMessage = responseData['error']?['message'] as String? ?? + responseData['message'] as String? ?? + _defaultErrorMessage; + throw APIException(errorMessage); + } + throw APIException(_defaultErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: ${e.toString()}'); + } + } +} diff --git a/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart new file mode 100644 index 00000000..4b8f1ba1 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/calendar_event_booking.dart @@ -0,0 +1,134 @@ +class CalendarEventBooking { + final String uuid; + final DateTime date; + final String startTime; + final String endTime; + final int cost; + final BookingUser user; + final BookingSpace space; + + CalendarEventBooking({ + required this.uuid, + required this.date, + required this.startTime, + required this.endTime, + required this.cost, + required this.user, + required this.space, + }); + + factory CalendarEventBooking.fromJson(Map json) { + return CalendarEventBooking( + uuid: json['uuid'] as String? ?? '', + date: json['date'] != null + ? DateTime.parse(json['date'] as String) + : DateTime.now(), + startTime: json['startTime'] as String? ?? '', + endTime: json['endTime'] as String? ?? '', + cost: _parseInt(json['cost']), + user: json['user'] != null + ? BookingUser.fromJson(json['user'] as Map) + : BookingUser.empty(), + space: json['space'] != null + ? BookingSpace.fromJson(json['space'] as Map) + : BookingSpace.empty(), + ); + } + + static int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; + } +} + +class BookingUser { + final String uuid; + final String firstName; + final String lastName; + final String email; + final String? companyName; + + BookingUser({ + required this.uuid, + required this.firstName, + required this.lastName, + required this.email, + this.companyName, + }); + + factory BookingUser.fromJson(Map json) { + return BookingUser( + uuid: json['uuid'] as String? ?? '', + firstName: json['firstName'] as String? ?? '', + lastName: json['lastName'] as String? ?? '', + email: json['email'] as String? ?? '', + companyName: json['companyName'] as String?, + ); + } + + factory BookingUser.empty() { + return BookingUser( + uuid: '', + firstName: '', + lastName: '', + email: '', + companyName: null, + ); + } +} + +class BookingSpace { + final String uuid; + final String spaceName; + + BookingSpace({ + required this.uuid, + required this.spaceName, + }); + + factory BookingSpace.fromJson(Map json) { + return BookingSpace( + uuid: json['uuid'] as String? ?? '', + spaceName: json['spaceName'] as String? ?? '', + ); + } + + factory BookingSpace.empty() { + return BookingSpace( + uuid: '', + spaceName: '', + ); + } +} + +class CalendarEventsResponse { + final int statusCode; + final String message; + final List data; + final bool success; + + CalendarEventsResponse({ + required this.statusCode, + required this.message, + required this.data, + required this.success, + }); + + factory CalendarEventsResponse.fromJson(Map json) { + return CalendarEventsResponse( + statusCode: _parseInt(json['statusCode']), + message: json['message'] as String? ?? '', + data: (json['data'] as List? ?? []) + .map((e) => CalendarEventBooking.fromJson(e as Map)) + .toList(), + success: json['success'] as bool? ?? false, + ); + } +} + +int _parseInt(dynamic value) { + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return 0; +} 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 new file mode 100644 index 00000000..9e178040 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/calendar_system_service.dart @@ -0,0 +1,7 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart'; + +abstract class CalendarSystemService { + Future getCalendarEvents({ + required String spaceId, + }); +} 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 431720af..da782d74 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,13 +2,17 @@ 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/models/calendar_event_booking.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; + part 'events_event.dart'; part 'events_state.dart'; class CalendarEventsBloc extends Bloc { final EventController eventController = EventController(); + final CalendarSystemService calendarService; - CalendarEventsBloc() : super(EventsInitial()) { + CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) { on(_onLoadEvents); on(_onAddEvent); on(_onStartTimer); @@ -22,53 +26,24 @@ class CalendarEventsBloc extends Bloc { ) async { emit(EventsLoading()); try { - final events = _generateDummyEventsForWeek(event.weekStart); + final response = await calendarService.getCalendarEvents( + spaceId: event.spaceId, + ); + final events = + response.data.map(_toCalendarEventData).toList(); eventController.addAll(events); - emit(EventsLoaded( - events: events, - initialDate: event.weekStart, - weekDays: _getWeekDays(event.weekStart), - )); + emit(EventsLoaded(events: events)); } catch (e) { emit(EventsError('Failed to load events')); } } - List _generateDummyEventsForWeek(DateTime weekStart) { - final events = []; - - for (int i = 0; i < 7; i++) { - final date = weekStart.add(Duration(days: i)); - - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 9, minute: 0), - endTime: date.copyWith(hour: 10, minute: 30), - title: 'Team Meeting', - description: 'Daily standup', - color: Colors.blue, - )); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 14, minute: 0), - endTime: date.copyWith(hour: 15, minute: 0), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - )); - } - - return events; - } - void _onAddEvent(AddEvent event, Emitter emit) { eventController.add(event.event); if (state is EventsLoaded) { final loaded = state as EventsLoaded; emit(EventsLoaded( events: [...eventController.events], - initialDate: loaded.initialDate, - weekDays: loaded.weekDays, )); } } @@ -86,47 +61,44 @@ class CalendarEventsBloc extends Bloc { final newWeekDays = _getWeekDays(event.weekDate); emit(EventsLoaded( events: loaded.events, - initialDate: event.weekDate, - weekDays: newWeekDays, )); } } - List _generateDummyEvents() { - final now = DateTime.now(); - return [ - CalendarEventData( - date: now, - startTime: now.copyWith(hour: 8, minute: 00, second: 0), - endTime: now.copyWith(hour: 9, minute: 00, second: 0), - title: 'Team Meeting', - description: 'Weekly team sync', - color: Colors.blue, - ), - CalendarEventData( - date: now, - startTime: now.copyWith(hour: 9, minute: 00, second: 0), - endTime: now.copyWith(hour: 10, minute: 30, second: 0), - title: 'Team Meeting', - description: 'Weekly team sync', - color: Colors.blue, - ), - CalendarEventData( - date: now.add(const Duration(days: 1)), - startTime: now.copyWith(hour: 14, day: now.day + 1), - endTime: now.copyWith(hour: 15, day: now.day + 1), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - ), - CalendarEventData( - date: now.add(const Duration(days: 2)), - startTime: now.copyWith(hour: 11, day: now.day + 2), - endTime: now.copyWith(hour: 12, day: now.day + 2), - title: 'Lunch with Team', - color: Colors.orange, - ), - ]; + CalendarEventData _toCalendarEventData(CalendarEventBooking booking) { + final date = booking.date; + + final localDate = date.toLocal(); + + final startParts = booking.startTime.split(':').map(int.parse).toList(); + final endParts = booking.endTime.split(':').map(int.parse).toList(); + + final startTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + startParts[0], + startParts[1], + ); + + final endTime = DateTime( + localDate.year, + localDate.month, + localDate.day, + endParts[0], + endParts[1], + ); + + return CalendarEventData( + date: startTime, + startTime: startTime, + endTime: endTime, + title: + '${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}', + description: 'Cost: ${booking.cost}', + color: Colors.blue, + event: booking, + ); } List _getWeekDays(DateTime date) { 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 e23e65de..4f4cafcf 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,13 +6,20 @@ abstract class CalendarEventsEvent { } class LoadEvents extends CalendarEventsEvent { + final String spaceId; final DateTime weekStart; - const LoadEvents({required this.weekStart}); + final DateTime weekEnd; + + const LoadEvents({ + required this.spaceId, + required this.weekStart, + required this.weekEnd, + }); } class AddEvent extends CalendarEventsEvent { final CalendarEventData event; - AddEvent(this.event); + const AddEvent(this.event); } class StartTimer extends CalendarEventsEvent {} @@ -23,3 +30,8 @@ class GoToWeek extends CalendarEventsEvent { final DateTime weekDate; GoToWeek(this.weekDate); } + +class CheckWeekHasEvents extends CalendarEventsEvent { + final DateTime weekStart; + const CheckWeekHasEvents(this.weekStart); +} 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 b7263ec8..bc0c2e31 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 @@ -9,13 +9,9 @@ class EventsLoading extends CalendarEventState {} class EventsLoaded extends CalendarEventState { final List events; - final DateTime initialDate; - final List weekDays; EventsLoaded({ required this.events, - required this.initialDate, - required this.weekDays, }); } 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 357cac41..0ff9aaf6 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,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; import 'package:calendar_view/calendar_view.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.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'; @@ -9,7 +10,9 @@ import 'package:syncrow_web/pages/access_management/booking_system/presentation/ import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.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'; @@ -35,33 +38,20 @@ class _BookingPageState extends State { super.dispose(); } - List _generateDummyEventsForWeek(DateTime weekStart) { - final List events = []; - for (int i = 0; i < 7; i++) { - final date = weekStart.add(Duration(days: i)); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 9, minute: 0), - endTime: date.copyWith(hour: 10, minute: 30), - title: 'Team Meeting', - description: 'Daily standup', - color: Colors.blue, - )); - events.add(CalendarEventData( - date: date, - startTime: date.copyWith(hour: 14, minute: 0), - endTime: date.copyWith(hour: 15, minute: 0), - title: 'Client Call', - description: 'Project discussion', - color: Colors.green, - )); - } - return events; - } + void _dispatchLoadEvents(BuildContext context) { + final selectedRoom = + context.read().state.selectedBookableSpace; + final dateState = context.read().state; - void _loadEventsForWeek(DateTime weekStart) { - _eventController.removeWhere((_) => true); - _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + if (selectedRoom != null) { + context.read().add( + LoadEvents( + spaceId: selectedRoom.uuid, + weekStart: dateState.weekStart, + weekEnd: dateState.weekStart.add(const Duration(days: 6)), + ), + ); + } } @override @@ -70,197 +60,181 @@ class _BookingPageState extends State { providers: [ BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => DateSelectionBloc()), + BlocProvider( + create: (_) => CalendarEventsBloc( + calendarService: + FakeRemoteCalendarService(HTTPService(), useDummy: true), + ), + ), ], - child: BlocListener( - listenWhen: (previous, current) => - previous.weekStart != current.weekStart, - listener: (context, state) { - _loadEventsForWeek(state.weekStart); - }, - 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)); - }, - ); - }, + child: Builder( + builder: (context) => + BlocListener( + listenWhen: (prev, curr) => curr is EventsLoaded, + 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: 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( - 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, - 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: () {}, - ), - ], - ), - BlocBuilder( - builder: (context, state) { - final weekStart = state.weekStart; - final weekEnd = - weekStart.add(const Duration(days: 6)); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: ColorsManager.circleRolesBackground, - borderRadius: BorderRadius.circular(10), - boxShadow: const [ - BoxShadow( - color: ColorsManager.lightGrayColor, - blurRadius: 4, - offset: Offset(0, 1), + ), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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: () {}, ), ], ), - child: Row( - children: [ - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_back_ios, - color: ColorsManager.lightGrayColor), - 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()); }, - ), - const SizedBox(width: 10), - Text( - _getMonthYearText(weekStart, weekEnd), - style: const TextStyle( - color: ColorsManager.lightGrayColor, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(width: 10), - IconButton( - iconSize: 15, - icon: const Icon(Icons.arrow_forward_ios, - color: ColorsManager.lightGrayColor), - onPressed: () { + onNextWeek: () { context .read() .add(NextWeek()); }, - ), - ], + ); + }, ), - ); - }, - ), - ], - ), - Expanded( - child: BlocBuilder( - builder: (context, roomState) { - final selectedRoom = roomState.selectedBookableSpace; - return BlocBuilder( - builder: (context, dateState) { - return WeeklyCalendarPage( - startTime: - selectedRoom?.bookableConfig.startTime, - endTime: selectedRoom?.bookableConfig.endTime, - weekStart: dateState.weekStart, - selectedDate: dateState.selectedDate, - eventController: _eventController, - selectedDateFromSideBarCalender: context - .watch() - .state - .selectedDateFromSideBarCalender, - ); - }, - ); - }, + ], + ), + 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, + ), + ); + }, + ); + }, + ), + ), + ], ), ), - ], - ), + ), + ], ), ), - ], + ), ), ), ); } - - String _getMonthYearText(DateTime start, DateTime end) { - final startMonth = DateFormat('MMM').format(start); - final endMonth = DateFormat('MMM').format(end); - final year = start.year == end.year - ? start.year.toString() - : '${start.year}-${end.year}'; - - if (start.month == end.month) { - return '$startMonth $year'; - } else { - return '$startMonth - $endMonth $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 new file mode 100644 index 00000000..6c0f9cb2 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart @@ -0,0 +1,60 @@ +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.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( + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = + event.endTime != null && event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.lightGrayBorderColor + : ColorsManager.blue1.withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart new file mode 100644 index 00000000..da74d07f --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class HatchedColumnBackground extends StatelessWidget { + final Color backgroundColor; + final Color lineColor; + final double opacity; + final double stripeSpacing; + final BorderRadius? borderRadius; + + const HatchedColumnBackground({ + super.key, + required this.backgroundColor, + required this.lineColor, + this.opacity = 0.15, + this.stripeSpacing = 12, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _HatchedBackgroundPainter( + backgroundColor: backgroundColor, + opacity: opacity, + lineColor: lineColor, + stripeSpacing: stripeSpacing, + borderRadius: borderRadius, + ), + size: Size.infinite, + ); + } +} + +class _HatchedBackgroundPainter extends CustomPainter { + final Color backgroundColor; + final double opacity; + final Color lineColor; + final double stripeSpacing; + final BorderRadius? borderRadius; + + _HatchedBackgroundPainter({ + required this.backgroundColor, + required this.opacity, + required this.lineColor, + required this.stripeSpacing, + this.borderRadius, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(0, 0, size.width, size.height); + final RRect rrect = borderRadius?.toRRect(rect) ?? + RRect.fromRectAndRadius(rect, Radius.zero); + final backgroundPaint = Paint() + ..color = backgroundColor.withOpacity(0.02) + ..style = PaintingStyle.fill; + canvas.drawRRect(rrect, backgroundPaint); + canvas.save(); + canvas.clipRRect(rrect); + final linePaint = Paint() + ..color = lineColor + ..strokeWidth = 0.5 + ..style = PaintingStyle.stroke; + final maxExtent = + math.sqrt(size.width * size.width + size.height * size.height); + + canvas.translate(0, size.height); + canvas.rotate(-math.pi / 4); + double y = -maxExtent; + while (y < maxExtent) { + canvas.drawLine( + Offset(-maxExtent, y), + Offset(maxExtent, y), + linePaint, + ); + y += stripeSpacing; + } + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) { + return backgroundColor != oldDelegate.backgroundColor || + opacity != oldDelegate.opacity || + lineColor != oldDelegate.lineColor || + stripeSpacing != oldDelegate.stripeSpacing || + borderRadius != oldDelegate.borderRadius; + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart new file mode 100644 index 00000000..eada3b97 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class TimeLineWidget extends StatelessWidget { + final DateTime date; + + const TimeLineWidget({Key? key, required this.date}) : super(key: key); + + @override + Widget build(BuildContext context) { + int hour = + date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..57e35c6d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekDayHeader extends StatelessWidget { + final DateTime date; + final bool isSelectedDay; + + const WeekDayHeader({ + Key? key, + required this.date, + required this.isSelectedDay, + }) : super(key: key); + + @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, + ), + ), + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: + isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor, + ), + ), + ], + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart new file mode 100644 index 00000000..bdc65b8e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class WeekNavigation extends StatelessWidget { + final DateTime weekStart; + final DateTime weekEnd; + final VoidCallback onPreviousWeek; + final VoidCallback onNextWeek; + + const WeekNavigation({ + Key? key, + required this.weekStart, + required this.weekEnd, + required this.onPreviousWeek, + required this.onNextWeek, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_back_ios, + color: ColorsManager.lightGrayColor), + onPressed: onPreviousWeek, + ), + const SizedBox(width: 10), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 10), + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_forward_ios, + color: ColorsManager.lightGrayColor), + onPressed: onNextWeek, + ), + ], + ), + ); + } + + String _getMonthYearText(DateTime start, DateTime end) { + final startMonth = DateFormat('MMM').format(start); + final endMonth = DateFormat('MMM').format(end); + final year = start.year == end.year + ? start.year.toString() + : '${start.year}-${end.year}'; + + if (start.month == end.month) { + return '$startMonth $year'; + } else { + return '$startMonth - $endMonth $year'; + } + } +} 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 5c38e2fc..0dd343a7 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,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:calendar_view/calendar_view.dart'; -import 'package:intl/intl.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'; class WeeklyCalendarPage extends StatelessWidget { @@ -60,18 +63,39 @@ class WeeklyCalendarPage extends StatelessWidget { 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); + } 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, @@ -88,70 +112,13 @@ class WeeklyCalendarPage extends StatelessWidget { height: 0, ), weekDayBuilder: (date) { - final index = weekDays.indexWhere((d) => isSameDay(d, date)); - final isSelectedDay = index == selectedDayIndex; - return Column( - children: [ - Text( - DateFormat('EEE').format(date).toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: isSelectedDay ? Colors.blue : Colors.black, - ), - ), - Text( - DateFormat('d').format(date), - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20, - color: isSelectedDay - ? ColorsManager.blue1 - : ColorsManager.blackColor, - ), - ), - ], + return WeekDayHeader( + date: date, + isSelectedDay: isSameDay(date, selectedDate), ); }, timeLineBuilder: (date) { - int hour = date.hour == 0 - ? 12 - : (date.hour > 12 ? date.hour - 12 : date.hour); - String period = date.hour >= 12 ? 'PM' : 'AM'; - return Container( - height: 60, - alignment: Alignment.center, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: '$hour', - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: ColorsManager.blackColor, - ), - ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 2, top: 6), - child: Text( - period, - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 12, - color: ColorsManager.blackColor, - letterSpacing: 1, - ), - ), - ), - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.alphabetic, - ), - ], - ), - ), - ); + return TimeLineWidget(date: date); }, timeLineWidth: timeLineWidth, weekPageHeaderBuilder: (start, end) => Container(), @@ -174,49 +141,8 @@ class WeeklyCalendarPage extends StatelessWidget { ), ), eventTileBuilder: (date, events, boundary, start, end) { - return Container( - margin: - const EdgeInsets.symmetric(vertical: 2, horizontal: 2), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: events.map((event) { - final bool isEventEnded = event.endTime != null && - event.endTime!.isBefore(DateTime.now()); - return Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: isEventEnded - ? ColorsManager.lightGrayBorderColor - : ColorsManager.blue1.withOpacity(0.25), - borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat('h:mm a').format(event.startTime!), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.black87, - ), - ), - const SizedBox(height: 2), - Text( - event.title, - style: const TextStyle( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ); - }).toList(), - ), + return EventTileWidget( + events: events, ); }, ), diff --git a/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart new file mode 100644 index 00000000..d05f22c7 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart @@ -0,0 +1,14 @@ +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'; + +class SpaceReorderDataModel { + const SpaceReorderDataModel({ + required this.space, + this.parent, + this.community, + }); + + final SpaceModel space; + final SpaceModel? parent; + final CommunityModel? community; +} 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 106b9a3a..40a37891 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 @@ -16,21 +16,37 @@ import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; -class SpaceManagementPage extends StatelessWidget { +class SpaceManagementPage extends StatefulWidget { const SpaceManagementPage({super.key}); + @override + State createState() => _SpaceManagementPageState(); +} + +class _SpaceManagementPageState extends State { + late final CommunitiesBloc communitiesBloc; + + @override + void initState() { + communitiesBloc = CommunitiesBloc( + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), + )..add(const LoadCommunities(LoadCommunitiesParam())); + + super.initState(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider.value(value: communitiesBloc), BlocProvider( - create: (context) => CommunitiesBloc( - communitiesService: DebouncedCommunitiesService( - RemoteCommunitiesService(HTTPService()), - ), - )..add(const LoadCommunities(LoadCommunitiesParam())), + create: (context) => CommunitiesTreeSelectionBloc( + communitiesBloc: communitiesBloc, + ), ), - BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), BlocProvider( create: (context) => SpaceDetailsBloc( UniqueSubspacesDecorator( 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 4aea103a..3cf761ad 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,13 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.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/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ @@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; - late TransformationController _transformationController; - late AnimationController _animationController; + late final TransformationController _transformationController; + late final AnimationController _animationController; + SpaceReorderDataModel? _draggedData; @override void initState() { @@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State final position = _positions[space.uuid]; if (position == null) return; - const scale = 1.5; + const scale = 1; final viewSize = context.size; if (viewSize == null) return; @@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onReorder(SpaceReorderDataModel data, int newIndex) { + final newCommunity = widget.community.copyWith(); + final children = data.parent?.children ?? newCommunity.spaces; + final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid); + if (oldIndex != -1) { + final item = children.removeAt(oldIndex); + if (newIndex > oldIndex) { + children.insert(newIndex - 1, item); + } else { + children.insert(newIndex, item); + } + } + context.read().add( + CommunitiesUpdateCommunity(newCommunity), + ); + } + void _onSpaceTapped(SpaceModel? space) { context.read().add( SelectSpaceEvent(community: widget.community, space: space), ); } - void _resetSelectionAndZoom() { + void _resetSelectionAndZoom([CommunityModel? community]) { context.read().add( SelectSpaceEvent( - community: widget.community, + community: community ?? widget.community, space: null, ), ); @@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State _positions.clear(); final community = widget.community; - _calculateLayout(community.spaces, 0, {}); + final levelXOffset = {}; + _calculateLayout(community.spaces, 0, levelXOffset); final selectedSpace = widget.selectedSpace; final highlightedUuids = {}; @@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections, highlightedUuids); + _generateWidgets( + widget.community.spaces, + widgets, + connections, + highlightedUuids, + community: widget.community, + ); + + final createButtonX = levelXOffset[0] ?? 0.0; + const createButtonY = 0.0; + + widgets.add( + Positioned( + left: createButtonX, + top: createButtonY, + child: CreateSpaceButton(communityUuid: widget.community.uuid), + ), + ); return [ CustomPaint( @@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, - Set highlightedUuids, - ) { - for (final space in spaces) { + Set highlightedUuids, { + CommunityModel? community, + SpaceModel? parent, + }) { + if (spaces.isNotEmpty) { + final firstChildPos = _positions[spaces.first.uuid]!; + final targetPos = Offset( + firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dy, + ); + widgets.add(_buildDropTarget(parent, community, 0, targetPos)); + } + + for (var i = 0; i < spaces.length; i++) { + final space = spaces[i]; final position = _positions[space.uuid]; - if (position == null) continue; + if (position == null) { + continue; + } final isHighlighted = highlightedUuids.contains(space.uuid); final hasNoSelectedSpace = widget.selectedSpace == null; + final spaceCard = SpaceCardWidget( + 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, + ), + ), + ); + }, + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.community.uuid, + ), + ); + + final reorderData = SpaceReorderDataModel( + space: space, + parent: parent, + community: community, + ); + widgets.add( Positioned( left: position.dx, top: position.dy, width: _cardWidth, height: _cardHeight, - child: SpaceCardWidget( - 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: Draggable( + data: reorderData, + feedback: Material( + color: Colors.transparent, + child: Opacity( + opacity: 0.2, + child: SizedBox( + width: _cardWidth, + height: _cardHeight, + child: spaceCard, ), - ); - }, - onTap: () => SpaceDetailsDialogHelper.showCreate(context), + ), + ), + onDragStarted: () => setState(() => _draggedData = reorderData), + onDragEnd: (_) => setState(() => _draggedData = null), + onDraggableCanceled: (_, __) => setState(() => _draggedData = null), + childWhenDragging: Opacity(opacity: 0.4, child: spaceCard), + child: spaceCard, ), ), ); + final targetPos = Offset( + position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, + position.dy, + ); + widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); + for (final child in space.children) { - connections.add( - SpaceConnectionModel(from: space.uuid, to: child.uuid), + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + + if (space.children.isNotEmpty) { + _generateWidgets( + space.children, + widgets, + connections, + highlightedUuids, + parent: space, ); } - _generateWidgets(space.children, widgets, connections, highlightedUuids); } } + Widget _buildDropTarget( + SpaceModel? parent, + CommunityModel? community, + int index, + Offset position, + ) { + return Positioned( + left: position.dx, + top: position.dy, + width: 40, + height: _cardHeight, + child: DragTarget( + builder: (context, candidateData, rejectedData) { + if (_draggedData == null) { + return const SizedBox(); + } + + final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && + _draggedData?.community == null) || + (_draggedData?.community?.uuid == community?.uuid && + _draggedData?.parent == null); + + if (!isTargetForDragged) { + return const SizedBox(); + } + + return Container( + width: 40, + height: _cardHeight, + decoration: BoxDecoration( + color: context.theme.colorScheme.primary.withValues( + alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.add, + color: context.theme.colorScheme.onPrimary, + ), + ); + }, + onWillAcceptWithDetails: (data) { + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (data.data.parent?.uuid == parent?.uuid && + data.data.community == null) || + (data.data.community?.uuid == community?.uuid && + data.data.parent == null); + + if (!isSameParent) { + return false; + } + + final oldIndex = + children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == index || oldIndex == index - 1) { + return false; + } + return true; + }, + onAcceptWithDetails: (data) => _onReorder(data.data, index), + ), + ); + } + @override Widget build(BuildContext context) { final treeWidgets = _buildTreeWidgets(); return InteractiveViewer( transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( - horizontal: MediaQuery.sizeOf(context).width * 0.3, - vertical: MediaQuery.sizeOf(context).height * 0.3, + horizontal: context.screenWidth * 0.3, + vertical: context.screenHeight * 0.3, ), minScale: 0.5, maxScale: 3.0, @@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State child: GestureDetector( onTap: _resetSelectionAndZoom, child: SizedBox( - width: MediaQuery.sizeOf(context).width * 5, - height: MediaQuery.sizeOf(context).height * 5, + width: context.screenWidth * 5, + height: context.screenHeight * 5, child: Stack(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 4f71075b..cb6271d1 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 @@ -3,7 +3,10 @@ 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/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'; @@ -11,6 +14,26 @@ import 'package:syncrow_web/utils/constants/assets.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); @@ -55,8 +78,9 @@ class CommunityStructureHeader extends StatelessWidget { children: [ Text( 'Community Structure', - style: theme.textTheme.headlineLarge - ?.copyWith(color: ColorsManager.blackColor), + style: theme.textTheme.headlineLarge?.copyWith( + color: ColorsManager.blackColor, + ), ), if (selectedCommunity != null) Row( @@ -67,8 +91,9 @@ class CommunityStructureHeader extends StatelessWidget { Flexible( child: SelectableText( selectedCommunity.name, - style: theme.textTheme.bodyLarge - ?.copyWith(color: ColorsManager.blackColor), + style: theme.textTheme.bodyLarge?.copyWith( + color: ColorsManager.blackColor, + ), maxLines: 1, ), ), @@ -93,12 +118,24 @@ class CommunityStructureHeader extends StatelessWidget { CommunityStructureHeaderActionButtons( onDelete: (space) {}, onDuplicate: (space) {}, - onEdit: (space) { - SpaceDetailsDialogHelper.showEdit( - context, - spaceModel: selectedSpace!, - ); - }, + 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)); + }, + ), 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 4cbfd7fd..e6dfbb15 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 @@ -2,38 +2,66 @@ import 'package:flutter/material.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 StatelessWidget { - const CreateSpaceButton({super.key}); +class CreateSpaceButton extends StatefulWidget { + const CreateSpaceButton({ + required this.communityUuid, + super.key, + }); + + final String communityUuid; + + @override + State createState() => _CreateSpaceButtonState(); +} + +class _CreateSpaceButtonState extends State { + bool _isHovered = false; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => SpaceDetailsDialogHelper.showCreate(context), - child: Container( - height: 60, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.5), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), - ), - ], + return Tooltip( + margin: const EdgeInsets.symmetric(vertical: 24), + message: 'Create a new space', + child: InkWell( + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.communityUuid, ), - child: Center( - child: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.add, - color: Colors.blue, + child: MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _isHovered ? 1.0 : 0.45, + child: Container( + width: 150, + height: 90, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.2), + spreadRadius: 3, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.borderColor, width: 2), + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), ), ), ), 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 e91e577f..54902280 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 @@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State { return MouseRegion( onEnter: (_) => setState(() => isHovered = true), onExit: (_) => setState(() => isHovered = false), - child: SizedBox( - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - widget.buildSpaceContainer(), - if (isHovered) - Positioned( - bottom: 0, - child: PlusButtonWidget( - offset: Offset.zero, - onButtonTap: widget.onTap, - ), + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + widget.buildSpaceContainer(), + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + offset: Offset.zero, + onButtonTap: 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 bcde6560..80b18526 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 @@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( + return InkWell( onTap: onTap, child: Container( width: 150, 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 e1f1fc00..050eac87 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 @@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget { final selectionBloc = context.watch().state; final selectedCommunity = selectionBloc.selectedCommunity; final selectedSpace = selectionBloc.selectedSpace; - const spacer = Spacer(flex: 10); + const spacer = Spacer(flex: 6); return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, - replacement: const Row( - children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + replacement: Row( + children: [ + spacer, + Expanded( + child: CreateSpaceButton(communityUuid: selectedCommunity.uuid), + ), + spacer + ], ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index ddcc6a86..bd5a2e50 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -46,6 +46,25 @@ class SpaceModel extends Equatable { ); } + SpaceModel copyWith({ + String? uuid, + DateTime? createdAt, + DateTime? updatedAt, + String? spaceName, + String? icon, + List? children, + }) { + return SpaceModel( + uuid: uuid ?? this.uuid, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + spaceName: spaceName ?? this.spaceName, + icon: icon ?? this.icon, + children: children ?? this.children, + parent: parent, + ); + } + @override List get props => [uuid, spaceName, icon, children]; } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bdda04ee..25263d35 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -1,17 +1,39 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.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'; part 'communities_tree_selection_event.dart'; part 'communities_tree_selection_state.dart'; class CommunitiesTreeSelectionBloc extends Bloc { - CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + CommunitiesTreeSelectionBloc({ + required CommunitiesBloc communitiesBloc, + }) : _communitiesBloc = communitiesBloc, + super(const CommunitiesTreeSelectionState()) { on(_onSelectCommunity); on(_onSelectSpace); on(_onClearSelection); + on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated); + + _communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) { + if (state.selectedCommunity != null) { + add(_CommunitiesStateUpdated(communitiesState)); + } + }); + } + + final CommunitiesBloc _communitiesBloc; + late final StreamSubscription _communitiesSubscription; + + @override + Future close() { + _communitiesSubscription.cancel(); + return super.close(); } void _onSelectCommunity( @@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc ) { emit(const CommunitiesTreeSelectionState()); } + + void _onCommunitiesStateUpdated( + _CommunitiesStateUpdated event, + Emitter emit, + ) { + if (state.selectedCommunity == null) return; + + final communities = event.communitiesState.communities; + try { + final updatedCommunity = communities.firstWhere( + (c) => c.uuid == state.selectedCommunity!.uuid, + ); + + var updatedSelectedSpace = state.selectedSpace; + if (state.selectedSpace != null) { + updatedSelectedSpace = _findSpaceInCommunity( + updatedCommunity, + state.selectedSpace!.uuid, + ); + } + emit( + state.copyWith( + selectedCommunity: updatedCommunity, + selectedSpace: updatedSelectedSpace, + clearSelectedSpace: updatedSelectedSpace == null, + ), + ); + } catch (_) { + add(const ClearCommunitiesTreeSelectionEvent()); + } + } + + SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) { + try { + return _findSpaceRecursive(community.spaces, spaceUuid); + } catch (_) { + return null; + } + } + + SpaceModel _findSpaceRecursive(List spaces, String spaceUuid) { + for (final space in spaces) { + if (space.uuid == spaceUuid) { + return space; + } + if (space.children.isNotEmpty) { + try { + return _findSpaceRecursive(space.children, spaceUuid); + } catch (_) { + // not found in this branch + } + } + } + throw Exception('Space not found'); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 21088632..43a69e05 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent extends CommunitiesTreeSelectionEvent { const ClearCommunitiesTreeSelectionEvent(); } + +final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent { + const _CommunitiesStateUpdated(this.communitiesState); + + final CommunitiesState communitiesState; + + @override + List get props => [communitiesState]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart index b14d330b..4c36f778 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable { CommunitiesTreeSelectionState copyWith({ CommunityModel? selectedCommunity, SpaceModel? selectedSpace, - List? expandedCommunities, - List? expandedSpaces, + bool clearSelectedSpace = false, }) { return CommunitiesTreeSelectionState( selectedCommunity: selectedCommunity ?? this.selectedCommunity, - selectedSpace: selectedSpace ?? this.selectedSpace, + selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace, ); } @override - List get props => [ - selectedCommunity, - selectedSpace, - ]; - } + List get props => [selectedCommunity, selectedSpace]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart index cfd32f52..277347df 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget { return Expanded( child: Center( child: Column( + spacing: 16, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + SelectableText( errorMessage ?? 'Something went wrong', textAlign: TextAlign.center, ), - const SizedBox(height: 16), - ElevatedButton( + FilledButton( onPressed: () => context.read().add( LoadCommunities( LoadCommunitiesParam( 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 b3e436b1..ec3c9f81 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 @@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'spaceName': spaceName, - 'icon': icon, - 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), - 'subspaces': subspaces.map((e) => e.toJson()).toList(), - }; - } - SpaceDetailsModel copyWith({ String? uuid, String? spaceName, @@ -89,14 +79,6 @@ class ProductAllocation extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'product': product.toJson(), - 'tag': tag.toJson(), - }; - } - ProductAllocation copyWith({ String? uuid, Product? product, @@ -134,14 +116,6 @@ class Subspace extends Equatable { ); } - Map toJson() { - return { - 'uuid': uuid, - 'name': name, - 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), - }; - } - Subspace copyWith({ String? uuid, String? name, 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 de2c40f0..c5de7dad 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 @@ -2,23 +2,37 @@ 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/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'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart'; import 'package:syncrow_web/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { - static void showCreate(BuildContext context) { + static void showCreate( + BuildContext context, { + required String communityUuid, + }) { showDialog( context: context, - builder: (_) => BlocProvider( - create: (context) => SpaceDetailsBloc( - RemoteSpaceDetailsService(httpService: HTTPService()), - ), - child: SpaceDetailsDialog( - context: context, - title: const SelectableText('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: (space) {}, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), + ], + child: Builder( + builder: (context) => SpaceDetailsDialog( + context: context, + title: const SelectableText('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: (space) {}, + communityUuid: communityUuid, + ), ), ), ); @@ -27,20 +41,98 @@ abstract final class SpaceDetailsDialogHelper { static void showEdit( BuildContext context, { required SpaceModel spaceModel, + required String communityUuid, + required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, }) { showDialog( context: context, - builder: (_) => BlocProvider( - create: (context) => SpaceDetailsBloc( - RemoteSpaceDetailsService(httpService: HTTPService()), - ), - child: SpaceDetailsDialog( - context: context, - title: const SelectableText('Edit Space'), - spaceModel: spaceModel, - onSave: (space) {}, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), + BlocProvider( + create: (context) => UpdateSpaceBloc( + RemoteUpdateSpaceService(HTTPService()), + ), + ), + ], + child: Builder( + builder: (context) => BlocListener( + listener: (context, state) => _updateListener( + context, + state, + onSuccess, + ), + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Edit Space'), + spaceModel: spaceModel, + onSave: (space) => context.read().add( + UpdateSpace( + UpdateSpaceParam( + communityUuid: communityUuid, + space: space, + ), + ), + ), + communityUuid: communityUuid, + ), + ), ), ), ); } + + static void _updateListener( + BuildContext context, + UpdateSpaceState state, + void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, + ) { + return switch (state) { + UpdateSpaceInitial() => null, + UpdateSpaceLoading() => _onLoading(context), + UpdateSpaceSuccess(:final space) => + _onUpdateSuccess(context, space, onSuccess), + UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage), + }; + } + + static void _onUpdateSuccess( + BuildContext context, + SpaceDetailsModel space, + void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess, + ) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + onSuccess?.call(space); + } + + static void _onLoading(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); + } + + static void _onError(BuildContext context, String errorMessage) { + Navigator.of(context).pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: const Text('Error'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text('OK'), + ), + ], + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index ae772036..d97442ec 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,7 +1,6 @@ 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/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/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget { required this.spaceModel, required this.onSave, required this.context, + required this.communityUuid, super.key, }); @@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget { final SpaceModel spaceModel; final void Function(SpaceDetailsModel space) onSave; final BuildContext context; + final String communityUuid; @override State createState() => _SpaceDetailsDialogState(); @@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State { if (!isCreateMode) { final param = LoadSpaceDetailsParam( spaceUuid: widget.spaceModel.uuid, - communityUuid: widget.context - .read() - .state - .selectedCommunity! - .uuid, + communityUuid: widget.communityUuid, ); widget.context.read().add(LoadSpaceDetails(param)); } 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 9e81c323..8faac548 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 @@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State { ..._subspaces, Subspace( name: name, - uuid: const Uuid().v4(), + uuid: '${const Uuid().v4()}-NewTag', productAllocations: const [], ), ]; diff --git a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart index 370bdf47..c5bccdbb 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart @@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart'; class Tag extends Equatable { final String uuid; final String name; - final String createdAt; - final String updatedAt; const Tag({ required this.uuid, required this.name, - required this.createdAt, - required this.updatedAt, }); - factory Tag.empty() => const Tag( - uuid: '', - name: '', - createdAt: '', - updatedAt: '', - ); - factory Tag.fromJson(Map json) { return Tag( uuid: json['uuid'] as String, name: json['name'] as String, - createdAt: json['createdAt'] as String, - updatedAt: json['updatedAt'] as String, ); } - Map toJson() { - return { - 'uuid': uuid, - 'name': name, - 'createdAt': createdAt, - 'updatedAt': updatedAt, - }; - } - @override - List get props => [uuid, name, createdAt, updatedAt]; + List get props => [uuid, name]; } 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 3cab4abe..3f6d42ab 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 @@ -214,9 +214,12 @@ class _AssignTagsDialogState extends State { for (final product in newProducts) { _space.productAllocations.add( ProductAllocation( - uuid: const Uuid().v4(), + uuid: '${const Uuid().v4()}-NewProductUuid', product: product, - tag: Tag.empty(), + tag: Tag( + uuid: '${const Uuid().v4()}-NewTag', + name: '', + ), ), ); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart index 8bbf379d..30282123 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:uuid/uuid.dart'; class ProductTagField extends StatefulWidget { final List items; @@ -53,13 +54,8 @@ class _ProductTagFieldState extends State { void _submit(String value) { final lowerCaseValue = value.toLowerCase(); final selectedTag = widget.items.firstWhere( - (tag) => tag.name.toLowerCase() == lowerCaseValue, - orElse: () => Tag( - name: value, - uuid: '', - createdAt: '', - updatedAt: '', - ), + (e) => e.name.toLowerCase() == lowerCaseValue, + orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value), ); widget.onSelected(selectedTag); _closeDropdown(); 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 b15e6095..a70d3b85 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 @@ -1,5 +1,7 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.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/update_space/domain/params/update_space_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { static const _defaultErrorMessage = 'Failed to update space'; @override - Future updateSpace(SpaceDetailsModel space) async { + Future updateSpace(UpdateSpaceParam param) async { try { - final response = await _httpService.put( - path: 'endpoint', - body: space.toJson(), - expectedResponseModel: (data) => SpaceDetailsModel.fromJson( - data as Map, - ), + final path = await _makeUrl(param); + await _httpService.put( + 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 isSuccess; + }, ); - return response; + return param.space; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl(UpdateSpaceParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + final spaceUuid = param.space.uuid; + if (spaceUuid.isEmpty) { + throw APIException('Space UUID is not set'); + } + + final communityUuid = param.communityUuid; + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid'; + } } 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 new file mode 100644 index 00000000..5dd9106d --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -0,0 +1,42 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; + +class UpdateSpaceParam { + UpdateSpaceParam({ + required this.space, + required this.communityUuid, + }); + + 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(), + }; + } +} diff --git a/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart index 29bc9419..c75fc0d4 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart @@ -1,5 +1,6 @@ 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/update_space/domain/params/update_space_param.dart'; -abstract class UpdateSpaceService { - Future updateSpace(SpaceDetailsModel space); +abstract interface class UpdateSpaceService { + Future updateSpace(UpdateSpaceParam param); } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart index 3bc4e187..0920b547 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.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/update_space/domain/params/update_space_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc { ) async { emit(UpdateSpaceLoading()); try { - final updatedSpace = await _updateSpaceService.updateSpace(event.space); + final updatedSpace = await _updateSpaceService.updateSpace(event.param); emit(UpdateSpaceSuccess(updatedSpace)); } on APIException catch (e) { emit(UpdateSpaceFailure(e.message)); diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart index b7d476af..ec08cdd2 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_event.dart @@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable { } final class UpdateSpace extends UpdateSpaceEvent { - const UpdateSpace(this.space); + const UpdateSpace(this.param); - final SpaceDetailsModel space; + final UpdateSpaceParam param; @override - List get props => [space]; + List get props => [param]; } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart index f0bc5a2b..437cca60 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_state.dart @@ -21,10 +21,10 @@ final class UpdateSpaceSuccess extends UpdateSpaceState { } final class UpdateSpaceFailure extends UpdateSpaceState { - final String message; + final String errorMessage; - const UpdateSpaceFailure(this.message); + const UpdateSpaceFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index f2123046..c1d791c8 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -69,7 +69,6 @@ abstract class ColorsManager { static const Color invitedOrange = Color(0xFFFFE193); static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color lightGrayBorderColor = Color(0xB2D5D5D5); - //background: #F8F8F8; static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); @@ -86,4 +85,5 @@ abstract class ColorsManager { static const Color grey25 = Color(0xFFF9F9F9); static const Color grey50 = Color(0xFF718096); static const Color red100 = Color(0xFFFE0202); + static const Color grey800 = Color(0xffF8F8F8); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index f908db85..8797f0cd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -141,4 +141,5 @@ abstract class ApiEndpoints { static const String saveSchedule = '/schedule/{deviceUuid}'; static const String getBookableSpaces = '/bookable-spaces'; + static const String getCalendarEvents = '/api'; }