Merge branch 'dev' into Implement-Spaces-Table-Empty-Filled-Failure-states-bookable-spaces

This commit is contained in:
Rafeek-khoudare
2025-07-21 09:17:04 +03:00
committed by GitHub
62 changed files with 1567 additions and 639 deletions

View File

@ -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<String, CalendarEventsResponse> _eventsCache = {};
@override
Future<CalendarEventsResponse> 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<CalendarEventsResponse> 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;
}
}
}

View File

@ -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<CalendarEventsResponse> 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<CalendarEventsResponse>(
path: ApiEndpoints.getBookings
.replaceAll('{mm}', month)
.replaceAll('{yyyy}', year)
.replaceAll('{space}', params.id),
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
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<CalendarEventsResponse> 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<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {

View File

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

View File

@ -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<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
required LoadEventsParam params,
});
}

View File

@ -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<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
final CalendarSystemService calendarService;
CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) {
CalendarEventsBloc({
required this.calendarService,
}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek);
}
Future<void> _onLoadEvents(
LoadEvents event,
Emitter<CalendarEventState> 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<CalendarEventData>(_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<CalendarEventsEvent, CalendarEventState> {
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> 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<CalendarEventState> emit) {}
void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose();
@ -61,6 +73,9 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
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<CalendarEventsEvent, CalendarEventState> {
);
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,
);
}

View File

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

View File

@ -7,11 +7,17 @@ class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState {
final class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events;
final String spaceId;
final int month;
final int year;
EventsLoaded({
required this.events,
required this.spaceId,
required this.month,
required this.year,
});
}

View File

@ -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<BookingPage> {
if (selectedRoom != null) {
context.read<CalendarEventsBloc>().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<BookingPage> {
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<BookingPage> {
),
),
Expanded(
flex: 4,
flex: 5,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@ -187,6 +194,7 @@ class _BookingPageState extends State<BookingPage> {
],
),
Expanded(
flex: 5,
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {

View File

@ -72,11 +72,7 @@ class __SidebarContentState extends State<_SidebarContent> {
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
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<SidebarBloc>().add(ResetSearch());
},
),

View File

@ -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<CalendarEventData<Object?>> 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(),

View File

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

View File

@ -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<DateTime> _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) {

View File

@ -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<DeviceManagementState> emit) {
void _onSelectDevice(
SelectDevice event, Emitter<DeviceManagementState> 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<DeviceManagementState> emit) {
void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> 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();

View File

@ -937,13 +937,17 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
List<String> 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,
));
}

View File

@ -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<SpaceModel> recusrivelyUpdate(
List<SpaceModel> 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<SpaceModel> recusrivelyDelete(
List<SpaceModel> 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<SpaceModel>().toList();
return nonNullSpaces;
}
static List<SpaceModel> recursivelyInsert({
required List<SpaceModel> 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();
}
}

View File

@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions;
final double cardWidth = 150.0;
final Map<String, double> cardWidths;
final double cardHeight = 90.0;
final Set<String> 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);
}
}
}

View File

@ -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<SpaceManagementPage> {
),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
UniqueSpaceDetailsSpacesDecoratorService(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),

View File

@ -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<CommunityStructureCanvas>
with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0;
final Map<String, double> _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<CommunityStructureCanvas>
@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<CommunityStructureCanvas>
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<SpaceModel> spaces) {
for (final space in spaces) {
_cardWidths[space.uuid] = _calculateCardWidth(space.spaceName);
if (space.children.isNotEmpty) {
_calculateAllCardWidths(space.children);
}
}
}
Set<String> _getAllDescendantUuids(SpaceModel space) {
final uuids = <String>{};
for (final child in space.children) {
@ -102,11 +133,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
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<CommunityStructureCanvas>
Map<int, double> 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<CommunityStructureCanvas>
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<CommunityStructureCanvas>
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<CommunityStructureCanvas>
List<Widget> _buildTreeWidgets() {
_positions.clear();
_cardWidths.clear();
final community = widget.community;
_calculateAllCardWidths(community.spaces);
final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
@ -231,7 +269,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid),
child: CreateSpaceButton(community: widget.community),
),
);
@ -240,6 +278,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
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<CommunityStructureCanvas>
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<CommunityStructureCanvas>
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<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(
widget.community.copyWith(spaces: updatedSpaces),
),
);
},
),
);
@ -305,7 +354,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
width: cardWidth,
height: _cardHeight,
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
@ -314,7 +363,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: _cardWidth,
width: cardWidth,
height: _cardHeight,
child: spaceCard,
),
@ -330,7 +379,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
);
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<CommunityStructureCanvas>
@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,

View File

@ -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<SpaceModel> _updateRecursive(
List<SpaceModel> 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<CommunitiesTreeSelectionBloc>().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<CommunitiesBloc>();
final updatedSpaces = _updateRecursive(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
CommunityStructureHeaderActionButtonsComposer(
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
),
],

View File

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

View File

@ -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<void>(
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<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: selectedCommunity),
);
},
),
),
onDuplicate: (space) {},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace,
);
}
}

View File

@ -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<CreateSpaceButton> createState() => _CreateSpaceButtonState();
@ -25,7 +29,21 @@ class _CreateSpaceButtonState extends State<CreateSpaceButton> {
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<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
space: updatedSpaceModel,
community: newCommunity,
),
);
},
),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),

View File

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

View File

@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
bottom: -5,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
onTap: widget.onTap,
),
),
],

View File

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

View File

@ -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<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
return BlocBuilder<CommunitiesTreeSelectionBloc, CommunitiesTreeSelectionState>(
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<CommunitiesBloc, CommunitiesState>(
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,
],
),
);

View File

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

View File

@ -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<String, dynamic> toJson() {
return {
'parentUuid': parentUuid,
...space.toJson(),
'x': 0,
'y': 0,
};
}
}

View File

@ -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<SpaceModel> createSpace(CreateSpaceParam param);
}

View File

@ -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<CreateSpaceEvent, CreateSpaceState> {
CreateSpaceBloc(
this._createSpaceService,
) : super(const CreateSpaceInitial()) {
on<CreateSpace>(_onCreateSpace);
}
final CreateSpaceService _createSpaceService;
Future<void> _onCreateSpace(
CreateSpace event,
Emitter<CreateSpaceState> 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()));
}
}
}

View File

@ -0,0 +1,17 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceEvent extends Equatable {
const CreateSpaceEvent();
@override
List<Object> get props => [];
}
final class CreateSpace extends CreateSpaceEvent {
const CreateSpace(this.param);
final CreateSpaceParam param;
@override
List<Object> get props => [param];
}

View File

@ -0,0 +1,31 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceState extends Equatable {
const CreateSpaceState();
@override
List<Object> 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<Object> get props => [space];
}
final class CreateSpaceFailure extends CreateSpaceState {
const CreateSpaceFailure(this.errorMessage);
final String errorMessage;
}

View File

@ -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<void> delete(DeleteSpaceParam param) async {
try {
await _httpService.delete(
path: await _makeUrl(param),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
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<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
throw APIException(e.toString());
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) return 'Failed to delete space';
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _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);
}
}

View File

@ -0,0 +1,9 @@
class DeleteSpaceParam {
const DeleteSpaceParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}

View File

@ -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<void> delete(DeleteSpaceParam param);
}

View File

@ -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<DeleteSpaceEvent, DeleteSpaceState> {
DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) {
on<DeleteSpace>(_onDeleteSpace);
}
final DeleteSpaceService _deleteSpaceService;
Future<void> _onDeleteSpace(
DeleteSpace event,
Emitter<DeleteSpaceState> 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()));
}
}
}

View File

@ -0,0 +1,17 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceEvent extends Equatable {
const DeleteSpaceEvent();
@override
List<Object> get props => [];
}
final class DeleteSpace extends DeleteSpaceEvent {
const DeleteSpace(this.param);
final DeleteSpaceParam param;
@override
List<Object> get props => [param];
}

View File

@ -0,0 +1,30 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceState extends Equatable {
const DeleteSpaceState();
@override
List<Object> 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<Object> get props => [successMessage];
}
final class DeleteSpaceFailure extends DeleteSpaceState {
const DeleteSpaceFailure(this.errorMessage);
final String errorMessage;
@override
List<Object> get props => [errorMessage];
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{};
final duplicateNames = <String>{};
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(),

View File

@ -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<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
Map<String, dynamic> toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
@override
List<Object?> get props => [uuid, product, tag];
}

View File

@ -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<String, dynamic> json) {
return SpaceDetailsModel(
uuid: json['uuid'] as String,
@ -56,78 +56,21 @@ class SpaceDetailsModel extends Equatable {
);
}
@override
List<Object?> 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<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
Map<String, dynamic> toJson() {
return {
'spaceName': spaceName,
'icon': icon,
'subspaces': subspaces.map((e) => e.toJson()).toList(),
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
@override
List<Object?> get props => [uuid, product, tag];
}
class Subspace extends Equatable {
final String uuid;
final String name;
final List<ProductAllocation> productAllocations;
const Subspace({
required this.uuid,
required this.name,
required this.productAllocations,
});
factory Subspace.fromJson(Map<String, dynamic> 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<String, dynamic>))
.toList(),
);
}
Subspace copyWith({
String? uuid,
String? name,
List<ProductAllocation>? productAllocations,
}) {
return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
}
@override
List<Object?> get props => [uuid, name, productAllocations];
List<Object?> get props => [
uuid,
spaceName,
icon,
productAllocations,
subspaces,
];
}

View File

@ -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<ProductAllocation> productAllocations;
const Subspace({
required this.uuid,
required this.name,
required this.productAllocations,
});
factory Subspace.fromJson(Map<String, dynamic> 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<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
Subspace copyWith({
String? uuid,
String? name,
List<ProductAllocation>? productAllocations,
}) {
return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
}
@override
List<Object?> get props => [uuid, name, productAllocations];
}

View File

@ -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<void>(
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<CreateSpaceBloc, CreateSpaceState>(
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<CreateSpaceBloc>().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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Product> initialProducts;
@override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget {
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {};
final Map<Product, int> _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<AddDeviceTypeWidget> {
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<AddDeviceTypeWidget> {
actions: [
SpaceDetailsActionButtons(
onSave: () {
final result = _selectedProducts.entries
final resultMap = <Product, int>{};
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);

View File

@ -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<AssignTagsDialog> {
onCancel: () async {
final newProducts = await showDialog<List<Product>>(
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;

View File

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

View File

@ -34,7 +34,7 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final errorMessage = error?['message'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,

View File

@ -9,34 +9,5 @@ class UpdateSpaceParam {
final SpaceDetailsModel space;
final String communityUuid;
Map<String, dynamic> 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<String, dynamic> _toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
}
extension _SubspaceToJson on Subspace {
Map<String, dynamic> _toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
};
}
Map<String, dynamic> toJson() => space.toJson();
}

View File

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

View File

@ -13,11 +13,15 @@ import 'package:syncrow_web/utils/constants/api_const.dart';
class DevicesManagementApi {
Future<List<AllDevicesModel>> fetchDevices(String projectId,
{List<String>? spacesId}) async {
{List<String>? spacesId, List<String>? 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<dynamic> jsonData = json['data'] as List<dynamic>;

View File

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

View File

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