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/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_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..b42947bd 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,9 +2,10 @@ 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'; @@ -12,27 +13,35 @@ class CalendarEventsBloc extends Bloc { final 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); } - 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 +49,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 +73,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 +105,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, ); } 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..6a368e17 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); 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..aac5c5b7 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 @@ -2,11 +2,13 @@ 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'; @@ -46,9 +48,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, + ), ), ); } @@ -61,11 +65,14 @@ class _BookingPageState extends State { BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => DateSelectionBloc()), BlocProvider( - create: (_) => CalendarEventsBloc( - calendarService: - FakeRemoteCalendarService(HTTPService(), useDummy: true), - ), - ), + create: (_) => CalendarEventsBloc( + calendarService: MemoryCalendarServiceWithRemoteFallback( + remoteService: RemoteCalendarService( + HTTPService(), + ), + memoryService: MemoryCalendarService(), + ), + )), ], child: Builder( builder: (context) => @@ -138,7 +145,7 @@ class _BookingPageState extends State { ), ), Expanded( - flex: 4, + flex: 5, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( @@ -187,6 +194,7 @@ class _BookingPageState extends State { ], ), Expanded( + flex: 5, child: BlocBuilder( builder: (context, roomState) { 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..666df3bb 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,11 +72,7 @@ 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: [ @@ -147,6 +143,7 @@ class __SidebarContentState extends State<_SidebarContent> { IconButton( icon: const Icon(Icons.close), onPressed: () { + searchController.clear(); context.read().add(ResetSearch()); }, ), 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..b7e942d6 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,86 @@ 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: 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: const Border( + left: BorderSide( + color: ColorsManager.grayColor, + 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/room_list_item.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart index 4a4b608d..83eda16b 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 @@ -24,17 +24,21 @@ class RoomListItem extends StatelessWidget { activeColor: ColorsManager.primaryColor, 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/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart index 0dd343a7..2bfd5429 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,159 @@ class WeeklyCalendarPage extends StatelessWidget { ); } - final weekDays = _getWeekDays(weekStart); + - final selectedDayIndex = - weekDays.indexWhere((d) => isSameDay(d, selectedDate)); - final selectedSidebarIndex = selectedDateFromSideBarCalender == null - ? -1 - : weekDays - .indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!)); + const double timeLineWidth = 65; - 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), + borderRadius: BorderRadius.circular(8), + ), + ); + } else if (isSelected) { + return Container( + height: height, + width: width, + decoration: BoxDecoration( + color: + ColorsManager.spaceColor.withOpacity(0.07), + borderRadius: BorderRadius.circular(8), + ), + ); + } + 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: -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, + ), + ), + ], + ), + ), + 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/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/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/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 40a37891..55e47de1 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 @@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen 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/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'; @@ -49,7 +49,7 @@ class _SpaceManagementPageState extends State { ), BlocProvider( create: (context) => SpaceDetailsBloc( - UniqueSubspacesDecorator( + UniqueSpaceDetailsSpacesDecoratorService( RemoteSpaceDetailsService(httpService: 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..692ffc0a 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'; @@ -30,10 +31,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 +54,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 +71,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 +133,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); @@ -155,13 +187,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 +205,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 +222,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,8 +237,11 @@ class _CommunityStructureCanvasState extends State List _buildTreeWidgets() { _positions.clear(); + _cardWidths.clear(); final community = widget.community; + _calculateAllCardWidths(community.spaces); + final levelXOffset = {}; _calculateLayout(community.spaces, 0, levelXOffset); @@ -231,7 +269,7 @@ class _CommunityStructureCanvasState extends State Positioned( left: createButtonX, top: createButtonY, - child: CreateSpaceButton(communityUuid: widget.community.uuid), + child: CreateSpaceButton(community: widget.community), ), ); @@ -240,6 +278,7 @@ class _CommunityStructureCanvasState extends State painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, + cardWidths: _cardWidths, highlightedUuids: highlightedUuids, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), @@ -271,6 +310,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 +318,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 +354,7 @@ class _CommunityStructureCanvasState extends State Positioned( left: position.dx, top: position.dy, - width: _cardWidth, + width: cardWidth, height: _cardHeight, child: Draggable( data: reorderData, @@ -314,7 +363,7 @@ class _CommunityStructureCanvasState extends State child: Opacity( opacity: 0.2, child: SizedBox( - width: _cardWidth, + width: cardWidth, height: _cardHeight, child: spaceCard, ), @@ -330,7 +379,7 @@ class _CommunityStructureCanvasState extends State ); final targetPos = Offset( - position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, + position.dx + cardWidth + (_horizontalSpacing / 4) - 20, position.dy, ); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); @@ -418,17 +467,17 @@ 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, 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/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_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/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/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 21588c7d..399cc1cd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -139,7 +139,11 @@ abstract class ApiEndpoints { '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; + ////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 0150ec36..4e282a88 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -521,4 +521,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'; }