mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 07:52:27 +00:00
Merge branch 'dev' of https://github.com/SyncrowIOT/web into release_incomplete_revamped_space_management
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||
|
||||
class RemoteCalendarService implements CalendarSystemService {
|
||||
const RemoteCalendarService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
static const _defaultErrorMessage = 'Failed to load Calendar';
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final month = params.startDate.month.toString().padLeft(2, '0');
|
||||
final year = params.startDate.year.toString();
|
||||
|
||||
try {
|
||||
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>);
|
||||
},
|
||||
);
|
||||
} 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()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -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')}';
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
class CalendarEventBooking {
|
||||
final String uuid;
|
||||
final DateTime date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final int cost;
|
||||
final BookingUser user;
|
||||
final BookingSpace space;
|
||||
|
||||
CalendarEventBooking({
|
||||
required this.uuid,
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.cost,
|
||||
required this.user,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
factory CalendarEventBooking.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEventBooking(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
date: json['date'] != null
|
||||
? DateTime.parse(json['date'] as String)
|
||||
: DateTime.now(),
|
||||
startTime: json['startTime'] as String? ?? '',
|
||||
endTime: json['endTime'] as String? ?? '',
|
||||
cost: _parseInt(json['cost']),
|
||||
user: json['user'] != null
|
||||
? BookingUser.fromJson(json['user'] as Map<String, dynamic>)
|
||||
: BookingUser.empty(),
|
||||
space: json['space'] != null
|
||||
? BookingSpace.fromJson(json['space'] as Map<String, dynamic>)
|
||||
: BookingSpace.empty(),
|
||||
);
|
||||
}
|
||||
|
||||
static int _parseInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class BookingUser {
|
||||
final String uuid;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String? companyName;
|
||||
|
||||
BookingUser({
|
||||
required this.uuid,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.email,
|
||||
this.companyName,
|
||||
});
|
||||
|
||||
factory BookingUser.fromJson(Map<String, dynamic> json) {
|
||||
return BookingUser(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
firstName: json['firstName'] as String? ?? '',
|
||||
lastName: json['lastName'] as String? ?? '',
|
||||
email: json['email'] as String? ?? '',
|
||||
companyName: json['companyName'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
factory BookingUser.empty() {
|
||||
return BookingUser(
|
||||
uuid: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
companyName: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookingSpace {
|
||||
final String uuid;
|
||||
final String spaceName;
|
||||
|
||||
BookingSpace({
|
||||
required this.uuid,
|
||||
required this.spaceName,
|
||||
});
|
||||
|
||||
factory BookingSpace.fromJson(Map<String, dynamic> json) {
|
||||
return BookingSpace(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
spaceName: json['spaceName'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
factory BookingSpace.empty() {
|
||||
return BookingSpace(
|
||||
uuid: '',
|
||||
spaceName: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarEventsResponse {
|
||||
final int statusCode;
|
||||
final String message;
|
||||
final List<CalendarEventBooking> data;
|
||||
final bool success;
|
||||
|
||||
CalendarEventsResponse({
|
||||
required this.statusCode,
|
||||
required this.message,
|
||||
required this.data,
|
||||
required this.success,
|
||||
});
|
||||
|
||||
factory CalendarEventsResponse.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEventsResponse(
|
||||
statusCode: _parseInt(json['statusCode']),
|
||||
message: json['message'] as String? ?? '',
|
||||
data: (json['data'] as List? ?? [])
|
||||
.map((e) => CalendarEventBooking.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
success: json['success'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _parseInt(dynamic value) {
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
@ -0,0 +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 LoadEventsParam params,
|
||||
});
|
||||
}
|
@ -2,79 +2,66 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
|
||||
part 'events_event.dart';
|
||||
part 'events_state.dart';
|
||||
|
||||
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final EventController eventController = EventController();
|
||||
final CalendarSystemService calendarService;
|
||||
|
||||
CalendarEventsBloc() : 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 events = _generateDummyEventsForWeek(event.weekStart);
|
||||
final response = await calendarService.getCalendarEvents(params: param);
|
||||
|
||||
final events = response.data.map(_toCalendarEventData).toList();
|
||||
eventController.addAll(events);
|
||||
emit(EventsLoaded(
|
||||
events: events,
|
||||
initialDate: event.weekStart,
|
||||
weekDays: _getWeekDays(event.weekStart),
|
||||
spaceId: spaceId,
|
||||
month: month,
|
||||
year: year,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EventsError('Failed to load events'));
|
||||
}
|
||||
}
|
||||
|
||||
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
|
||||
final events = <CalendarEventData>[];
|
||||
|
||||
for (int i = 0; i < 7; i++) {
|
||||
final date = weekStart.add(Duration(days: i));
|
||||
|
||||
events.add(CalendarEventData(
|
||||
date: date,
|
||||
startTime: date.copyWith(hour: 9, minute: 0),
|
||||
endTime: date.copyWith(hour: 10, minute: 30),
|
||||
title: 'Team Meeting',
|
||||
description: 'Daily standup',
|
||||
color: Colors.blue,
|
||||
));
|
||||
events.add(CalendarEventData(
|
||||
date: date,
|
||||
startTime: date.copyWith(hour: 14, minute: 0),
|
||||
endTime: date.copyWith(hour: 15, minute: 0),
|
||||
title: 'Client Call',
|
||||
description: 'Project discussion',
|
||||
color: Colors.green,
|
||||
));
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
|
||||
eventController.add(event.event);
|
||||
|
||||
if (state is EventsLoaded) {
|
||||
final loaded = state as EventsLoaded;
|
||||
|
||||
emit(EventsLoaded(
|
||||
events: [...eventController.events],
|
||||
initialDate: loaded.initialDate,
|
||||
weekDays: loaded.weekDays,
|
||||
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();
|
||||
@ -86,47 +73,46 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final newWeekDays = _getWeekDays(event.weekDate);
|
||||
emit(EventsLoaded(
|
||||
events: loaded.events,
|
||||
initialDate: event.weekDate,
|
||||
weekDays: newWeekDays,
|
||||
spaceId: loaded.spaceId,
|
||||
month: loaded.month,
|
||||
year: loaded.year,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
List<CalendarEventData> _generateDummyEvents() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
CalendarEventData(
|
||||
date: now,
|
||||
startTime: now.copyWith(hour: 8, minute: 00, second: 0),
|
||||
endTime: now.copyWith(hour: 9, minute: 00, second: 0),
|
||||
title: 'Team Meeting',
|
||||
description: 'Weekly team sync',
|
||||
color: Colors.blue,
|
||||
),
|
||||
CalendarEventData(
|
||||
date: now,
|
||||
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
|
||||
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
|
||||
title: 'Team Meeting',
|
||||
description: 'Weekly team sync',
|
||||
color: Colors.blue,
|
||||
),
|
||||
CalendarEventData(
|
||||
date: now.add(const Duration(days: 1)),
|
||||
startTime: now.copyWith(hour: 14, day: now.day + 1),
|
||||
endTime: now.copyWith(hour: 15, day: now.day + 1),
|
||||
title: 'Client Call',
|
||||
description: 'Project discussion',
|
||||
color: Colors.green,
|
||||
),
|
||||
CalendarEventData(
|
||||
date: now.add(const Duration(days: 2)),
|
||||
startTime: now.copyWith(hour: 11, day: now.day + 2),
|
||||
endTime: now.copyWith(hour: 12, day: now.day + 2),
|
||||
title: 'Lunch with Team',
|
||||
color: Colors.orange,
|
||||
),
|
||||
];
|
||||
CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
|
||||
final date = booking.date;
|
||||
|
||||
final localDate = date.toLocal();
|
||||
|
||||
final startParts = booking.startTime.split(':').map(int.parse).toList();
|
||||
final endParts = booking.endTime.split(':').map(int.parse).toList();
|
||||
|
||||
final startTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
startParts[0],
|
||||
startParts[1],
|
||||
);
|
||||
|
||||
final endTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
endParts[0],
|
||||
endParts[1],
|
||||
);
|
||||
|
||||
return CalendarEventData(
|
||||
date: startTime,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
title: '${booking.user.firstName} ${booking.user.lastName}',
|
||||
description: 'Cost: ${booking.cost}',
|
||||
color: Colors.blue,
|
||||
event: booking,
|
||||
);
|
||||
}
|
||||
|
||||
List<DateTime> _getWeekDays(DateTime date) {
|
||||
|
@ -6,13 +6,14 @@ abstract class CalendarEventsEvent {
|
||||
}
|
||||
|
||||
class LoadEvents extends CalendarEventsEvent {
|
||||
final DateTime weekStart;
|
||||
const LoadEvents({required this.weekStart});
|
||||
final LoadEventsParam param;
|
||||
const LoadEvents(this.param);
|
||||
}
|
||||
|
||||
|
||||
class AddEvent extends CalendarEventsEvent {
|
||||
final CalendarEventData event;
|
||||
AddEvent(this.event);
|
||||
const AddEvent(this.event);
|
||||
}
|
||||
|
||||
class StartTimer extends CalendarEventsEvent {}
|
||||
@ -23,3 +24,8 @@ class GoToWeek extends CalendarEventsEvent {
|
||||
final DateTime weekDate;
|
||||
GoToWeek(this.weekDate);
|
||||
}
|
||||
|
||||
class CheckWeekHasEvents extends CalendarEventsEvent {
|
||||
final DateTime weekStart;
|
||||
const CheckWeekHasEvents(this.weekStart);
|
||||
}
|
||||
|
@ -7,15 +7,17 @@ class EventsInitial extends CalendarEventState {}
|
||||
|
||||
class EventsLoading extends CalendarEventState {}
|
||||
|
||||
class EventsLoaded extends CalendarEventState {
|
||||
final class EventsLoaded extends CalendarEventState {
|
||||
final List<CalendarEventData> events;
|
||||
final DateTime initialDate;
|
||||
final List<DateTime> weekDays;
|
||||
final String spaceId;
|
||||
final int month;
|
||||
final int year;
|
||||
|
||||
EventsLoaded({
|
||||
required this.events,
|
||||
required this.initialDate,
|
||||
required this.weekDays,
|
||||
required this.spaceId,
|
||||
required this.month,
|
||||
required this.year,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/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';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
@ -35,33 +40,22 @@ class _BookingPageState extends State<BookingPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
|
||||
final List<CalendarEventData> events = [];
|
||||
for (int i = 0; i < 7; i++) {
|
||||
final date = weekStart.add(Duration(days: i));
|
||||
events.add(CalendarEventData(
|
||||
date: date,
|
||||
startTime: date.copyWith(hour: 9, minute: 0),
|
||||
endTime: date.copyWith(hour: 10, minute: 30),
|
||||
title: 'Team Meeting',
|
||||
description: 'Daily standup',
|
||||
color: Colors.blue,
|
||||
));
|
||||
events.add(CalendarEventData(
|
||||
date: date,
|
||||
startTime: date.copyWith(hour: 14, minute: 0),
|
||||
endTime: date.copyWith(hour: 15, minute: 0),
|
||||
title: 'Client Call',
|
||||
description: 'Project discussion',
|
||||
color: Colors.green,
|
||||
));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
void _dispatchLoadEvents(BuildContext context) {
|
||||
final selectedRoom =
|
||||
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
|
||||
final dateState = context.read<DateSelectionBloc>().state;
|
||||
|
||||
void _loadEventsForWeek(DateTime weekStart) {
|
||||
_eventController.removeWhere((_) => true);
|
||||
_eventController.addAll(_generateDummyEventsForWeek(weekStart));
|
||||
if (selectedRoom != null) {
|
||||
context.read<CalendarEventsBloc>().add(
|
||||
LoadEvents(
|
||||
LoadEventsParam(
|
||||
startDate: dateState.weekStart,
|
||||
endDate: dateState.weekStart.add(const Duration(days: 6)),
|
||||
id: selectedRoom.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -70,197 +64,185 @@ class _BookingPageState extends State<BookingPage> {
|
||||
providers: [
|
||||
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (_) => CalendarEventsBloc(
|
||||
calendarService: MemoryCalendarServiceWithRemoteFallback(
|
||||
remoteService: RemoteCalendarService(
|
||||
HTTPService(),
|
||||
),
|
||||
memoryService: MemoryCalendarService(),
|
||||
),
|
||||
)),
|
||||
],
|
||||
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.weekStart != current.weekStart,
|
||||
listener: (context, state) {
|
||||
_loadEventsForWeek(state.weekStart);
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(3, 0),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, state) {
|
||||
return BookingSidebar(
|
||||
onRoomSelected: (selectedRoom) {
|
||||
context
|
||||
.read<SelectedBookableSpaceBloc>()
|
||||
.add(SelectBookableSpace(selectedRoom));
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) =>
|
||||
BlocListener<CalendarEventsBloc, CalendarEventState>(
|
||||
listenWhen: (prev, curr) => curr is EventsLoaded,
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: BlocListener<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(3, 0),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, state) {
|
||||
return BookingSidebar(
|
||||
onRoomSelected: (selectedRoom) {
|
||||
context
|
||||
.read<SelectedBookableSpaceBloc>()
|
||||
.add(SelectBookableSpace(selectedRoom));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return CustomCalendarPage(
|
||||
selectedDate: dateState.selectedDate,
|
||||
onDateChanged: (day, month, year) {
|
||||
final newDate = DateTime(year, month, day);
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(SelectDate(newDate));
|
||||
context.read<DateSelectionBloc>().add(
|
||||
SelectDateFromSidebarCalendar(newDate));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<DateSelectionBloc, DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return CustomCalendarPage(
|
||||
selectedDate: dateState.selectedDate,
|
||||
onDateChanged: (day, month, year) {
|
||||
final newDate = DateTime(year, month, day);
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(SelectDate(newDate));
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(SelectDateFromSidebarCalendar(newDate));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
BlocBuilder<DateSelectionBloc, DateSelectionState>(
|
||||
builder: (context, state) {
|
||||
final weekStart = state.weekStart;
|
||||
final weekEnd =
|
||||
weekStart.add(const Duration(days: 6));
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_back_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: () {
|
||||
BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, state) {
|
||||
final weekStart = state.weekStart;
|
||||
final weekEnd =
|
||||
weekStart.add(const Duration(days: 6));
|
||||
return WeekNavigation(
|
||||
weekStart: weekStart,
|
||||
weekEnd: weekEnd,
|
||||
onPreviousWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(PreviousWeek());
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_getMonthYearText(weekStart, weekEnd),
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_forward_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: () {
|
||||
onNextWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(NextWeek());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, roomState) {
|
||||
final selectedRoom = roomState.selectedBookableSpace;
|
||||
return BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return WeeklyCalendarPage(
|
||||
startTime:
|
||||
selectedRoom?.bookableConfig.startTime,
|
||||
endTime: selectedRoom?.bookableConfig.endTime,
|
||||
weekStart: dateState.weekStart,
|
||||
selectedDate: dateState.selectedDate,
|
||||
eventController: _eventController,
|
||||
selectedDateFromSideBarCalender: context
|
||||
.watch<DateSelectionBloc>()
|
||||
.state
|
||||
.selectedDateFromSideBarCalender,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, roomState) {
|
||||
final selectedRoom =
|
||||
roomState.selectedBookableSpace;
|
||||
return BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return BlocListener<CalendarEventsBloc,
|
||||
CalendarEventState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
curr is EventsLoaded,
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController
|
||||
.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: WeeklyCalendarPage(
|
||||
startTime: selectedRoom
|
||||
?.bookableConfig.startTime,
|
||||
endTime: selectedRoom
|
||||
?.bookableConfig.endTime,
|
||||
weekStart: dateState.weekStart,
|
||||
selectedDate: dateState.selectedDate,
|
||||
eventController: _eventController,
|
||||
selectedDateFromSideBarCalender: context
|
||||
.watch<DateSelectionBloc>()
|
||||
.state
|
||||
.selectedDateFromSideBarCalender,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthYearText(DateTime start, DateTime end) {
|
||||
final startMonth = DateFormat('MMM').format(start);
|
||||
final endMonth = DateFormat('MMM').format(end);
|
||||
final year = start.year == end.year
|
||||
? start.year.toString()
|
||||
: '${start.year}-${end.year}';
|
||||
|
||||
if (start.month == end.month) {
|
||||
return '$startMonth $year';
|
||||
} else {
|
||||
return '$startMonth - $endMonth $year';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
},
|
||||
),
|
||||
|
@ -0,0 +1,106 @@
|
||||
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(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: events.map((event) {
|
||||
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: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.1)
|
||||
: ColorsManager.blue1.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: const Border(
|
||||
left: BorderSide(
|
||||
color: ColorsManager.grayColor,
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class HatchedColumnBackground extends StatelessWidget {
|
||||
final Color backgroundColor;
|
||||
final Color lineColor;
|
||||
final double opacity;
|
||||
final double stripeSpacing;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
const HatchedColumnBackground({
|
||||
super.key,
|
||||
required this.backgroundColor,
|
||||
required this.lineColor,
|
||||
this.opacity = 0.15,
|
||||
this.stripeSpacing = 12,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _HatchedBackgroundPainter(
|
||||
backgroundColor: backgroundColor,
|
||||
opacity: opacity,
|
||||
lineColor: lineColor,
|
||||
stripeSpacing: stripeSpacing,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
size: Size.infinite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HatchedBackgroundPainter extends CustomPainter {
|
||||
final Color backgroundColor;
|
||||
final double opacity;
|
||||
final Color lineColor;
|
||||
final double stripeSpacing;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
_HatchedBackgroundPainter({
|
||||
required this.backgroundColor,
|
||||
required this.opacity,
|
||||
required this.lineColor,
|
||||
required this.stripeSpacing,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
final RRect rrect = borderRadius?.toRRect(rect) ??
|
||||
RRect.fromRectAndRadius(rect, Radius.zero);
|
||||
final backgroundPaint = Paint()
|
||||
..color = backgroundColor.withOpacity(0.02)
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawRRect(rrect, backgroundPaint);
|
||||
canvas.save();
|
||||
canvas.clipRRect(rrect);
|
||||
final linePaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = 0.5
|
||||
..style = PaintingStyle.stroke;
|
||||
final maxExtent =
|
||||
math.sqrt(size.width * size.width + size.height * size.height);
|
||||
|
||||
canvas.translate(0, size.height);
|
||||
canvas.rotate(-math.pi / 4);
|
||||
double y = -maxExtent;
|
||||
while (y < maxExtent) {
|
||||
canvas.drawLine(
|
||||
Offset(-maxExtent, y),
|
||||
Offset(maxExtent, y),
|
||||
linePaint,
|
||||
);
|
||||
y += stripeSpacing;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) {
|
||||
return backgroundColor != oldDelegate.backgroundColor ||
|
||||
opacity != oldDelegate.opacity ||
|
||||
lineColor != oldDelegate.lineColor ||
|
||||
stripeSpacing != oldDelegate.stripeSpacing ||
|
||||
borderRadius != oldDelegate.borderRadius;
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TimeLineWidget extends StatelessWidget {
|
||||
final DateTime date;
|
||||
|
||||
const TimeLineWidget({Key? key, required this.date}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int hour =
|
||||
date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour);
|
||||
String period = date.hour >= 12 ? 'PM' : 'AM';
|
||||
return Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$hour',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2, top: 6),
|
||||
child: Text(
|
||||
period,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeekDayHeader extends StatelessWidget {
|
||||
final DateTime date;
|
||||
final bool isSelectedDay;
|
||||
|
||||
const WeekDayHeader({
|
||||
Key? key,
|
||||
required this.date,
|
||||
required this.isSelectedDay,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEE').format(date).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
color: isSelectedDay ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 20,
|
||||
color:
|
||||
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeekNavigation extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final DateTime weekEnd;
|
||||
final VoidCallback onPreviousWeek;
|
||||
final VoidCallback onNextWeek;
|
||||
|
||||
const WeekNavigation({
|
||||
Key? key,
|
||||
required this.weekStart,
|
||||
required this.weekEnd,
|
||||
required this.onPreviousWeek,
|
||||
required this.onNextWeek,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_back_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: onPreviousWeek,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_getMonthYearText(weekStart, weekEnd),
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
iconSize: 15,
|
||||
icon: const Icon(Icons.arrow_forward_ios,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
onPressed: onNextWeek,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthYearText(DateTime start, DateTime end) {
|
||||
final startMonth = DateFormat('MMM').format(start);
|
||||
final endMonth = DateFormat('MMM').format(end);
|
||||
final year = start.year == end.year
|
||||
? start.year.toString()
|
||||
: '${start.year}-${end.year}';
|
||||
|
||||
if (start.month == end.month) {
|
||||
return '$startMonth $year';
|
||||
} else {
|
||||
return '$startMonth - $endMonth $year';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeeklyCalendarPage extends StatelessWidget {
|
||||
@ -20,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) {
|
||||
@ -49,231 +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 = 80;
|
||||
const int totalDays = 7;
|
||||
const double timeLineWidth = 65;
|
||||
|
||||
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) {
|
||||
!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(
|
||||
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) {
|
||||
final index = weekDays.indexWhere((d) => isSameDay(d, date));
|
||||
final isSelectedDay = index == selectedDayIndex;
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEE').format(date).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
color: isSelectedDay ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 20,
|
||||
color: isSelectedDay
|
||||
? ColorsManager.blue1
|
||||
: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
timeLineBuilder: (date) {
|
||||
int hour = date.hour == 0
|
||||
? 12
|
||||
: (date.hour > 12 ? date.hour - 12 : date.hour);
|
||||
String period = date.hour >= 12 ? 'PM' : 'AM';
|
||||
return Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$hour',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2, top: 6),
|
||||
child: Text(
|
||||
period,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
);
|
||||
} 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 50,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
eventTileBuilder: (date, events, boundary, start, end) {
|
||||
return Container(
|
||||
margin:
|
||||
const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: events.map((event) {
|
||||
final bool isEventEnded = event.endTime != null &&
|
||||
event.endTime!.isBefore(DateTime.now());
|
||||
return Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: isEventEnded
|
||||
? ColorsManager.lightGrayBorderColor
|
||||
: ColorsManager.blue1.withOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('h:mm a').format(event.startTime!),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontalScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics:
|
||||
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||
child: SizedBox(
|
||||
width: _totalTableWidth,
|
||||
child: Column(
|
||||
@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
@ -265,7 +266,7 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: widget.size.height * 0.5),
|
||||
SizedBox(height: widget.size.height * 0.2),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -46,15 +46,16 @@ class DeviceManagementBloc
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
|
||||
if (spaceBloc.state.selectedCommunities.isEmpty) {
|
||||
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
|
||||
devices = await DevicesManagementApi().fetchDevices(
|
||||
projectUuid,
|
||||
);
|
||||
} else {
|
||||
for (final community in spaceBloc.state.selectedCommunities) {
|
||||
for (var community in spaceBloc.state.selectedCommunities) {
|
||||
final spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
|
||||
for (final space in spacesList) {
|
||||
devices.addAll(await DevicesManagementApi()
|
||||
.fetchDevices(community, space, projectUuid));
|
||||
}
|
||||
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();
|
||||
|
@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
|
||||
}
|
||||
|
||||
class _DeviceManagementPageState extends State<DeviceManagementPage> {
|
||||
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
context.read<SpaceTreeBloc>().add(InitialEvent());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State<DeviceManagementPage> {
|
||||
const TriggerSwitchTabsEvent(isRoutineTab: true));
|
||||
},
|
||||
child: Text(
|
||||
'Routines',
|
||||
'Workflow Automation',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: state.routineTab
|
||||
? ColorsManager.whiteColors
|
||||
|
@ -29,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
elevation: 2.5,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
backgroundColor: ColorsManager.boxColor,
|
||||
child: Text('Cancel', style: context.textTheme.bodyMedium),
|
||||
@ -39,6 +41,8 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
Expanded(
|
||||
child: isActive
|
||||
? DefaultButton(
|
||||
elevation: 2.5,
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
onPressed: () {
|
||||
context.read<ScheduleBloc>().add(
|
||||
@ -49,10 +53,12 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: ColorsManager.red100,
|
||||
child: const Text('Stop'),
|
||||
)
|
||||
: DefaultButton(
|
||||
elevation: 2.5,
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
onPressed: () {
|
||||
context.read<ScheduleBloc>().add(
|
||||
@ -63,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
countDownCode: countDownCode),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.primaryColor,
|
||||
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
|
@ -226,6 +226,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
index.toString().padLeft(2, '0'),
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: isActive ? ColorsManager.grayColor : Colors.black,
|
||||
),
|
||||
),
|
||||
@ -240,7 +241,8 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 18,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -31,12 +31,11 @@ class BuildScheduleView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ScheduleBloc(
|
||||
deviceId: deviceUuid,
|
||||
)
|
||||
create: (_) => ScheduleBloc(deviceId: deviceUuid,)
|
||||
..add(ScheduleGetEvent(category: category))
|
||||
..add(ScheduleFetchStatusEvent(
|
||||
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
|
||||
deviceId: deviceUuid,
|
||||
countdownCode: countdownCode ?? '')),
|
||||
child: Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
@ -77,7 +76,8 @@ class BuildScheduleView extends StatelessWidget {
|
||||
category: category,
|
||||
time: '',
|
||||
function: Status(
|
||||
code: code.toString(), value: null),
|
||||
code: code.toString(),
|
||||
value: true),
|
||||
days: [],
|
||||
),
|
||||
isEdit: false,
|
||||
|
@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
|
||||
Text(
|
||||
'Scheduling',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
|
@ -27,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget {
|
||||
width: 170,
|
||||
height: 40,
|
||||
child: DefaultButton(
|
||||
borderColor: ColorsManager.boxColor,
|
||||
borderColor: ColorsManager.grayColor.withOpacity(0.5),
|
||||
padding: 2,
|
||||
backgroundColor: ColorsManager.graysColor,
|
||||
borderRadius: 15,
|
||||
|
@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
elevation: 2.5,
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget {
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
elevation: 2.5,
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
onPressed: onSave,
|
||||
backgroundColor: ColorsManager.primaryColor,
|
||||
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
|
@ -35,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildRadioTile(
|
||||
context, 'Countdown', ScheduleModes.countdown, currentMode),
|
||||
_buildRadioTile(
|
||||
context, 'Schedule', ScheduleModes.schedule, currentMode),
|
||||
const Spacer(flex: 1),
|
||||
// _buildRadioTile(
|
||||
// context, 'Circulate', ScheduleModes.circulate, currentMode),
|
||||
// _buildRadioTile(
|
||||
@ -65,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget {
|
||||
style: context.textTheme.bodySmall!.copyWith(
|
||||
fontSize: 13,
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
leading: Radio<ScheduleModes>(
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ScheduleDialogHelper {
|
||||
static const List<String> allDays = [
|
||||
@ -56,8 +58,9 @@ class ScheduleDialogHelper {
|
||||
Text(
|
||||
isEdit ? 'Edit Schedule' : 'Add Schedule',
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.primaryColorWithOpacity,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(),
|
||||
@ -69,9 +72,9 @@ class ScheduleDialogHelper {
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundColor: ColorsManager.boxColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
@ -110,39 +113,27 @@ class ScheduleDialogHelper {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx, null);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ScheduleModeButtons(
|
||||
onSave: () {
|
||||
dynamic temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
}
|
||||
final entry = ScheduleEntry(
|
||||
category: schedule?.category ?? 'switch_1',
|
||||
time: _formatTimeOfDayToISO(selectedTime),
|
||||
function: Status(
|
||||
code: code ?? 'switch_1',
|
||||
value: temp,
|
||||
),
|
||||
days: _convertSelectedDaysToStrings(selectedDays),
|
||||
scheduleId: schedule.scheduleId,
|
||||
);
|
||||
Navigator.pop(ctx, entry);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
dynamic temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
}
|
||||
final entry = ScheduleEntry(
|
||||
category: schedule?.category ?? 'switch_1',
|
||||
time: _formatTimeOfDayToISO(selectedTime),
|
||||
function: Status(
|
||||
code: code ?? 'switch_1',
|
||||
value: temp,
|
||||
),
|
||||
days: _convertSelectedDaysToStrings(selectedDays),
|
||||
scheduleId: schedule.scheduleId,
|
||||
);
|
||||
Navigator.pop(ctx, entry);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -153,6 +153,7 @@ class EditUserModel {
|
||||
final String? jobTitle; // can be empty
|
||||
final String roleType; // e.g. "ADMIN"
|
||||
final List<UserSpaceModel> spaces;
|
||||
final String? companyName;
|
||||
|
||||
EditUserModel({
|
||||
required this.uuid,
|
||||
@ -167,6 +168,7 @@ class EditUserModel {
|
||||
required this.jobTitle,
|
||||
required this.roleType,
|
||||
required this.spaces,
|
||||
this.companyName,
|
||||
});
|
||||
|
||||
/// Create a [UserData] from JSON data
|
||||
@ -182,6 +184,7 @@ class EditUserModel {
|
||||
invitedBy: json['invitedBy'] as String,
|
||||
phoneNumber: json['phoneNumber'] ?? '',
|
||||
jobTitle: json['jobTitle'] ?? '',
|
||||
companyName: json['companyName'] as String?,
|
||||
roleType: json['roleType'] as String,
|
||||
spaces: (json['spaces'] as List<dynamic>)
|
||||
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))
|
||||
|
@ -12,7 +12,7 @@ class RolesUserModel {
|
||||
final dynamic jobTitle;
|
||||
final dynamic createdDate;
|
||||
final dynamic createdTime;
|
||||
|
||||
final String? companyName;
|
||||
RolesUserModel({
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
@ -27,6 +27,7 @@ class RolesUserModel {
|
||||
this.jobTitle,
|
||||
required this.createdDate,
|
||||
required this.createdTime,
|
||||
this.companyName,
|
||||
});
|
||||
|
||||
factory RolesUserModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -47,6 +48,7 @@ class RolesUserModel {
|
||||
: json['jobTitle'],
|
||||
createdDate: json['createdDate'],
|
||||
createdTime: json['createdTime'],
|
||||
companyName: json['companyName'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
final TextEditingController lastNameController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController jobTitleController = TextEditingController();
|
||||
final TextEditingController companyNameController = TextEditingController();
|
||||
final TextEditingController roleSearchController = TextEditingController();
|
||||
|
||||
bool? isCompleteBasics;
|
||||
@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
bool res = await UserPermissionApi().sendInviteUser(
|
||||
email: emailController.text,
|
||||
firstName: firstNameController.text,
|
||||
jobTitle: jobTitleController.text,
|
||||
companyName: companyNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
phoneNumber: phoneController.text,
|
||||
roleUuid: roleSelected,
|
||||
@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
bool res = await UserPermissionApi().editInviteUser(
|
||||
userId: event.userId,
|
||||
firstName: firstNameController.text,
|
||||
jobTitle: jobTitleController.text,
|
||||
companyName: companyNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
phoneNumber: phoneController.text,
|
||||
roleUuid: roleSelected,
|
||||
@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
Future<void> checkEmail(
|
||||
CheckEmailEvent event, Emitter<UsersState> emit) async {
|
||||
emit(UsersLoadingState());
|
||||
String? res = await UserPermissionApi().checkEmail(
|
||||
String? res = await UserPermissionApi().checkEmail(
|
||||
emailController.text,
|
||||
);
|
||||
checkEmailValid = res!;
|
||||
@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
lastNameController.text = res.lastName;
|
||||
emailController.text = res.email;
|
||||
phoneController.text = res.phoneNumber ?? '';
|
||||
jobTitleController.text = res.jobTitle ?? '';
|
||||
companyNameController.text = res.companyName ?? '';
|
||||
res.roleType;
|
||||
res.spaces.map((space) {
|
||||
selectedIds.add(space.uuid);
|
||||
@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
||||
lastNameController.dispose();
|
||||
emailController.dispose();
|
||||
phoneController.dispose();
|
||||
jobTitleController.dispose();
|
||||
companyNameController.dispose();
|
||||
roleSearchController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
@ -317,7 +317,7 @@ class BasicsView extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Job Title',
|
||||
'Company Name',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 13,
|
||||
@ -328,11 +328,11 @@ class BasicsView extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextFormField(
|
||||
controller: _blocRole.jobTitleController,
|
||||
controller: _blocRole.companyNameController,
|
||||
style:
|
||||
const TextStyle(color: ColorsManager.blackColor),
|
||||
decoration: inputTextFormDeco(
|
||||
hintText: "Job Title (Optional)")
|
||||
hintText: 'Company Name (Optional)')
|
||||
.copyWith(
|
||||
hintStyle: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
|
@ -411,7 +411,7 @@ class UsersPage extends StatelessWidget {
|
||||
titles: const [
|
||||
"Full Name",
|
||||
"Email Address",
|
||||
"Job Title",
|
||||
"Company Name",
|
||||
"Role",
|
||||
"Creation Date",
|
||||
"Creation Time",
|
||||
@ -424,7 +424,7 @@ class UsersPage extends StatelessWidget {
|
||||
return [
|
||||
Text('${user.firstName} ${user.lastName}'),
|
||||
Text(user.email),
|
||||
Text(user.jobTitle),
|
||||
Center(child: Text(user.companyName ?? '-')),
|
||||
Text(user.roleType ?? ''),
|
||||
Text(user.createdDate ?? ''),
|
||||
Text(user.createdTime ?? ''),
|
||||
|
@ -170,45 +170,45 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadScenes(
|
||||
LoadScenes event, Emitter<RoutineState> emit) async {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
List<ScenesModel> scenes = [];
|
||||
try {
|
||||
BuildContext context = NavigationService.navigatorKey.currentContext!;
|
||||
var createRoutineBloc = context.read<CreateRoutineBloc>();
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
if (createRoutineBloc.selectedSpaceId == '' &&
|
||||
createRoutineBloc.selectedCommunityId == '') {
|
||||
var spaceBloc = context.read<SpaceTreeBloc>();
|
||||
for (var communityId in spaceBloc.state.selectedCommunities) {
|
||||
List<String> spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
|
||||
for (var spaceId in spacesList) {
|
||||
scenes.addAll(
|
||||
await SceneApi.getScenes(spaceId, communityId, projectUuid));
|
||||
Future<void> _onLoadScenes(
|
||||
LoadScenes event, Emitter<RoutineState> emit) async {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
List<ScenesModel> scenes = [];
|
||||
try {
|
||||
BuildContext context = NavigationService.navigatorKey.currentContext!;
|
||||
var createRoutineBloc = context.read<CreateRoutineBloc>();
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
if (createRoutineBloc.selectedSpaceId == '' &&
|
||||
createRoutineBloc.selectedCommunityId == '') {
|
||||
var spaceBloc = context.read<SpaceTreeBloc>();
|
||||
for (var communityId in spaceBloc.state.selectedCommunities) {
|
||||
List<String> spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
|
||||
for (var spaceId in spacesList) {
|
||||
scenes.addAll(
|
||||
await SceneApi.getScenes(spaceId, communityId, projectUuid));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scenes.addAll(await SceneApi.getScenes(
|
||||
createRoutineBloc.selectedSpaceId,
|
||||
createRoutineBloc.selectedCommunityId,
|
||||
projectUuid));
|
||||
}
|
||||
} else {
|
||||
scenes.addAll(await SceneApi.getScenes(
|
||||
createRoutineBloc.selectedSpaceId,
|
||||
createRoutineBloc.selectedCommunityId,
|
||||
projectUuid));
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
scenes: scenes,
|
||||
isLoading: false,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
emit(state.copyWith(
|
||||
scenes: scenes,
|
||||
isLoading: false,
|
||||
loadScenesErrorMessage: 'Failed to load scenes',
|
||||
errorMessage: '',
|
||||
loadAutomationErrorMessage: '',
|
||||
scenes: scenes));
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
loadScenesErrorMessage: 'Failed to load scenes',
|
||||
errorMessage: '',
|
||||
loadAutomationErrorMessage: '',
|
||||
scenes: scenes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadAutomation(
|
||||
LoadAutomation event, Emitter<RoutineState> emit) async {
|
||||
@ -936,16 +936,19 @@ Future<void> _onLoadScenes(
|
||||
for (var communityId in spaceBloc.state.selectedCommunities) {
|
||||
List<String> spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
|
||||
for (var spaceId in spacesList) {
|
||||
devices.addAll(await DevicesManagementApi()
|
||||
.fetchDevices(communityId, spaceId, projectUuid));
|
||||
}
|
||||
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(
|
||||
projectUuid,
|
||||
spacesId: spacesList,
|
||||
communities: spaceBloc.state.selectedCommunities,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(
|
||||
createRoutineBloc.selectedCommunityId,
|
||||
createRoutineBloc.selectedSpaceId,
|
||||
projectUuid));
|
||||
projectUuid,
|
||||
spacesId: [createRoutineBloc.selectedSpaceId],
|
||||
communities: spaceBloc.state.selectedCommunities,
|
||||
));
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: false, devices: devices));
|
||||
|
@ -96,9 +96,7 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DialogHeader(widget.dialogType == 'THEN'
|
||||
? 'Presence Sensor Functions'
|
||||
: 'Presence Sensor Condition'),
|
||||
const DialogHeader('Presence Sensor'),
|
||||
Expanded(child: _buildMainContent(context, state)),
|
||||
_buildDialogFooter(context, state),
|
||||
],
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
|
||||
class SpaceReorderDataModel {
|
||||
const SpaceReorderDataModel({
|
||||
required this.space,
|
||||
this.parent,
|
||||
this.community,
|
||||
});
|
||||
|
||||
final SpaceModel space;
|
||||
final SpaceModel? parent;
|
||||
final CommunityModel? community;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,30 +10,46 @@ 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';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatelessWidget {
|
||||
class SpaceManagementPage extends StatefulWidget {
|
||||
const SpaceManagementPage({super.key});
|
||||
|
||||
@override
|
||||
State<SpaceManagementPage> createState() => _SpaceManagementPageState();
|
||||
}
|
||||
|
||||
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
late final CommunitiesBloc communitiesBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
communitiesBloc = CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: communitiesBloc),
|
||||
BlocProvider(
|
||||
create: (context) => CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||
create: (context) => CommunitiesTreeSelectionBloc(
|
||||
communitiesBloc: communitiesBloc,
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
UniqueSubspacesDecorator(
|
||||
UniqueSpaceDetailsSpacesDecoratorService(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
|
@ -1,13 +1,18 @@
|
||||
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';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CommunityStructureCanvas extends StatefulWidget {
|
||||
const CommunityStructureCanvas({
|
||||
@ -26,13 +31,15 @@ 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 TransformationController _transformationController;
|
||||
late AnimationController _animationController;
|
||||
late final TransformationController _transformationController;
|
||||
late final AnimationController _animationController;
|
||||
SpaceReorderDataModel? _draggedData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -47,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) {
|
||||
@ -63,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) {
|
||||
@ -97,11 +133,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
const scale = 1.5;
|
||||
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);
|
||||
|
||||
@ -112,16 +149,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
_runAnimation(matrix);
|
||||
}
|
||||
|
||||
void _onReorder(SpaceReorderDataModel data, int newIndex) {
|
||||
final newCommunity = widget.community.copyWith();
|
||||
final children = data.parent?.children ?? newCommunity.spaces;
|
||||
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
|
||||
if (oldIndex != -1) {
|
||||
final item = children.removeAt(oldIndex);
|
||||
if (newIndex > oldIndex) {
|
||||
children.insert(newIndex - 1, item);
|
||||
} else {
|
||||
children.insert(newIndex, item);
|
||||
}
|
||||
}
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(newCommunity),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSpaceTapped(SpaceModel? space) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(community: widget.community, space: space),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSelectionAndZoom() {
|
||||
void _resetSelectionAndZoom([CommunityModel? community]) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(
|
||||
community: widget.community,
|
||||
community: community ?? widget.community,
|
||||
space: null,
|
||||
),
|
||||
);
|
||||
@ -133,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,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;
|
||||
}
|
||||
@ -165,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,9 +237,13 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
List<Widget> _buildTreeWidgets() {
|
||||
_positions.clear();
|
||||
_cardWidths.clear();
|
||||
final community = widget.community;
|
||||
|
||||
_calculateLayout(community.spaces, 0, {});
|
||||
_calculateAllCardWidths(community.spaces);
|
||||
|
||||
final levelXOffset = <int, double>{};
|
||||
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
@ -193,13 +254,31 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
final widgets = <Widget>[];
|
||||
final connections = <SpaceConnectionModel>[];
|
||||
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
|
||||
_generateWidgets(
|
||||
widget.community.spaces,
|
||||
widgets,
|
||||
connections,
|
||||
highlightedUuids,
|
||||
community: widget.community,
|
||||
);
|
||||
|
||||
final createButtonX = levelXOffset[0] ?? 0.0;
|
||||
const createButtonY = 0.0;
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: createButtonX,
|
||||
top: createButtonY,
|
||||
child: CreateSpaceButton(community: widget.community),
|
||||
),
|
||||
);
|
||||
|
||||
return [
|
||||
CustomPaint(
|
||||
painter: SpacesConnectionsArrowPainter(
|
||||
connections: connections,
|
||||
positions: _positions,
|
||||
cardWidths: _cardWidths,
|
||||
highlightedUuids: highlightedUuids,
|
||||
),
|
||||
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
|
||||
@ -211,67 +290,197 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
List<SpaceModel> spaces,
|
||||
List<Widget> widgets,
|
||||
List<SpaceConnectionModel> connections,
|
||||
Set<String> highlightedUuids,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) continue;
|
||||
Set<String> highlightedUuids, {
|
||||
CommunityModel? community,
|
||||
SpaceModel? parent,
|
||||
}) {
|
||||
if (spaces.isNotEmpty) {
|
||||
final firstChildPos = _positions[spaces.first.uuid]!;
|
||||
final targetPos = Offset(
|
||||
firstChildPos.dx - (_horizontalSpacing / 4),
|
||||
firstChildPos.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
||||
}
|
||||
|
||||
for (var i = 0; i < spaces.length; i++) {
|
||||
final space = spaces[i];
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
final spaceCard = SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final reorderData = SpaceReorderDataModel(
|
||||
space: space,
|
||||
parent: parent,
|
||||
community: community,
|
||||
);
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: _cardWidth,
|
||||
width: cardWidth,
|
||||
height: _cardHeight,
|
||||
child: SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
child: Draggable<SpaceReorderDataModel>(
|
||||
data: reorderData,
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: Opacity(
|
||||
opacity: 0.2,
|
||||
child: SizedBox(
|
||||
width: cardWidth,
|
||||
height: _cardHeight,
|
||||
child: spaceCard,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
),
|
||||
),
|
||||
onDragStarted: () => setState(() => _draggedData = reorderData),
|
||||
onDragEnd: (_) => setState(() => _draggedData = null),
|
||||
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
|
||||
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
|
||||
child: spaceCard,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final targetPos = Offset(
|
||||
position.dx + cardWidth + (_horizontalSpacing / 4) - 20,
|
||||
position.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
||||
|
||||
for (final child in space.children) {
|
||||
connections.add(
|
||||
SpaceConnectionModel(from: space.uuid, to: child.uuid),
|
||||
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
|
||||
}
|
||||
|
||||
if (space.children.isNotEmpty) {
|
||||
_generateWidgets(
|
||||
space.children,
|
||||
widgets,
|
||||
connections,
|
||||
highlightedUuids,
|
||||
parent: space,
|
||||
);
|
||||
}
|
||||
_generateWidgets(space.children, widgets, connections, highlightedUuids);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDropTarget(
|
||||
SpaceModel? parent,
|
||||
CommunityModel? community,
|
||||
int index,
|
||||
Offset position,
|
||||
) {
|
||||
return Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: 40,
|
||||
height: _cardHeight,
|
||||
child: DragTarget<SpaceReorderDataModel>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
if (_draggedData == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
|
||||
_draggedData?.community == null) ||
|
||||
(_draggedData?.community?.uuid == community?.uuid &&
|
||||
_draggedData?.parent == null);
|
||||
|
||||
if (!isTargetForDragged) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 40,
|
||||
height: _cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: context.theme.colorScheme.primary.withValues(
|
||||
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: context.theme.colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
onWillAcceptWithDetails: (data) {
|
||||
final children = parent?.children ?? community?.spaces ?? [];
|
||||
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
|
||||
data.data.community == null) ||
|
||||
(data.data.community?.uuid == community?.uuid &&
|
||||
data.data.parent == null);
|
||||
|
||||
if (!isSameParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final oldIndex =
|
||||
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
||||
if (oldIndex == index || oldIndex == index - 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onAcceptWithDetails: (data) => _onReorder(data.data, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final treeWidgets = _buildTreeWidgets();
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
||||
vertical: MediaQuery.sizeOf(context).height * 0.3,
|
||||
),
|
||||
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: MediaQuery.sizeOf(context).width * 5,
|
||||
height: MediaQuery.sizeOf(context).height * 5,
|
||||
width: context.screenWidth * 5,
|
||||
height: context.screenHeight * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
),
|
||||
),
|
||||
|
@ -2,18 +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/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/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});
|
||||
|
||||
@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),
|
||||
@ -21,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -34,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),
|
||||
],
|
||||
@ -44,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 =
|
||||
@ -55,8 +53,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Community Structure',
|
||||
style: theme.textTheme.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: context.textTheme.headlineLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
if (selectedCommunity != null)
|
||||
Row(
|
||||
@ -67,8 +66,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
selectedCommunity.name,
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
@ -90,15 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) {},
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) {
|
||||
SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
);
|
||||
},
|
||||
CommunityStructureHeaderActionButtonsComposer(
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
],
|
||||
|
@ -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!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,39 +1,85 @@
|
||||
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 StatelessWidget {
|
||||
const CreateSpaceButton({super.key});
|
||||
class CreateSpaceButton extends StatefulWidget {
|
||||
const CreateSpaceButton({
|
||||
required this.community,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CommunityModel community;
|
||||
|
||||
@override
|
||||
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
|
||||
}
|
||||
|
||||
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.5),
|
||||
spreadRadius: 5,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
return Tooltip(
|
||||
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||
message: 'Create a new space',
|
||||
child: InkWell(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.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: Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: _isHovered ? 1.0 : 0.45,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.borderColor, width: 2),
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -22,22 +22,19 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: SizedBox(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: -5,
|
||||
child: PlusButtonWidget(
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -17,24 +17,22 @@ class SpaceCell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,25 +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;
|
||||
const spacer = Spacer(flex: 10);
|
||||
return Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CommunityStructureHeader(),
|
||||
Expanded(
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: 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 Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
spacer,
|
||||
Expanded(child: CreateSpaceButton(community: selectedCommunity)),
|
||||
spacer,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -46,6 +46,25 @@ class SpaceModel extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
SpaceModel copyWith({
|
||||
String? uuid,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? spaceName,
|
||||
String? icon,
|
||||
List<SpaceModel>? children,
|
||||
}) {
|
||||
return SpaceModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
spaceName: spaceName ?? this.spaceName,
|
||||
icon: icon ?? this.icon,
|
||||
children: children ?? this.children,
|
||||
parent: parent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, spaceName, icon, children];
|
||||
}
|
||||
|
@ -1,17 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
|
||||
part 'communities_tree_selection_event.dart';
|
||||
part 'communities_tree_selection_state.dart';
|
||||
|
||||
class CommunitiesTreeSelectionBloc
|
||||
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
|
||||
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
|
||||
CommunitiesTreeSelectionBloc({
|
||||
required CommunitiesBloc communitiesBloc,
|
||||
}) : _communitiesBloc = communitiesBloc,
|
||||
super(const CommunitiesTreeSelectionState()) {
|
||||
on<SelectCommunityEvent>(_onSelectCommunity);
|
||||
on<SelectSpaceEvent>(_onSelectSpace);
|
||||
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
|
||||
on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated);
|
||||
|
||||
_communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) {
|
||||
if (state.selectedCommunity != null) {
|
||||
add(_CommunitiesStateUpdated(communitiesState));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final CommunitiesBloc _communitiesBloc;
|
||||
late final StreamSubscription<CommunitiesState> _communitiesSubscription;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_communitiesSubscription.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onSelectCommunity(
|
||||
@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc
|
||||
) {
|
||||
emit(const CommunitiesTreeSelectionState());
|
||||
}
|
||||
|
||||
void _onCommunitiesStateUpdated(
|
||||
_CommunitiesStateUpdated event,
|
||||
Emitter<CommunitiesTreeSelectionState> emit,
|
||||
) {
|
||||
if (state.selectedCommunity == null) return;
|
||||
|
||||
final communities = event.communitiesState.communities;
|
||||
try {
|
||||
final updatedCommunity = communities.firstWhere(
|
||||
(c) => c.uuid == state.selectedCommunity!.uuid,
|
||||
);
|
||||
|
||||
var updatedSelectedSpace = state.selectedSpace;
|
||||
if (state.selectedSpace != null) {
|
||||
updatedSelectedSpace = _findSpaceInCommunity(
|
||||
updatedCommunity,
|
||||
state.selectedSpace!.uuid,
|
||||
);
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedCommunity: updatedCommunity,
|
||||
selectedSpace: updatedSelectedSpace,
|
||||
clearSelectedSpace: updatedSelectedSpace == null,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
add(const ClearCommunitiesTreeSelectionEvent());
|
||||
}
|
||||
}
|
||||
|
||||
SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) {
|
||||
try {
|
||||
return _findSpaceRecursive(community.spaces, spaceUuid);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
SpaceModel _findSpaceRecursive(List<SpaceModel> spaces, String spaceUuid) {
|
||||
for (final space in spaces) {
|
||||
if (space.uuid == spaceUuid) {
|
||||
return space;
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
try {
|
||||
return _findSpaceRecursive(space.children, spaceUuid);
|
||||
} catch (_) {
|
||||
// not found in this branch
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception('Space not found');
|
||||
}
|
||||
}
|
||||
|
@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent
|
||||
extends CommunitiesTreeSelectionEvent {
|
||||
const ClearCommunitiesTreeSelectionEvent();
|
||||
}
|
||||
|
||||
final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent {
|
||||
const _CommunitiesStateUpdated(this.communitiesState);
|
||||
|
||||
final CommunitiesState communitiesState;
|
||||
|
||||
@override
|
||||
List<Object> get props => [communitiesState];
|
||||
}
|
||||
|
@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable {
|
||||
CommunitiesTreeSelectionState copyWith({
|
||||
CommunityModel? selectedCommunity,
|
||||
SpaceModel? selectedSpace,
|
||||
List<CommunityModel>? expandedCommunities,
|
||||
List<SpaceModel>? expandedSpaces,
|
||||
bool clearSelectedSpace = false,
|
||||
}) {
|
||||
return CommunitiesTreeSelectionState(
|
||||
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
|
||||
selectedSpace: selectedSpace ?? this.selectedSpace,
|
||||
selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
selectedCommunity,
|
||||
selectedSpace,
|
||||
];
|
||||
}
|
||||
List<Object?> get props => [selectedCommunity, selectedSpace];
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
SelectableText(
|
||||
errorMessage ?? 'Something went wrong',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
FilledButton(
|
||||
onPressed: () => context.read<CommunitiesBloc>().add(
|
||||
LoadCommunities(
|
||||
LoadCommunitiesParam(
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
class DeleteSpaceParam {
|
||||
const DeleteSpaceParam({
|
||||
required this.spaceUuid,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
final String spaceUuid;
|
||||
final String communityUuid;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
@ -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];
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
@ -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];
|
||||
}
|
@ -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,
|
||||
@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'spaceName': spaceName,
|
||||
'icon': icon,
|
||||
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
|
||||
'subspaces': subspaces.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
SpaceDetailsModel copyWith({
|
||||
String? uuid,
|
||||
String? spaceName,
|
||||
@ -66,94 +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>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'product': product.toJson(),
|
||||
'tag': tag.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
ProductAllocation copyWith({
|
||||
String? uuid,
|
||||
Product? product,
|
||||
Tag? tag,
|
||||
}) {
|
||||
return ProductAllocation(
|
||||
uuid: uuid ?? this.uuid,
|
||||
product: product ?? this.product,
|
||||
tag: tag ?? this.tag,
|
||||
);
|
||||
}
|
||||
|
||||
@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(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'spaceName': spaceName,
|
||||
'icon': icon,
|
||||
'subspaces': subspaces.map((e) => e.toJson()).toList(),
|
||||
'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];
|
||||
List<Object?> get props => [
|
||||
uuid,
|
||||
spaceName,
|
||||
icon,
|
||||
productAllocations,
|
||||
subspaces,
|
||||
];
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
@ -1,24 +1,70 @@
|
||||
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';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
abstract final class SpaceDetailsDialogHelper {
|
||||
static void showCreate(BuildContext context) {
|
||||
static void showCreate(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
required void Function(SpaceModel updatedSpaceModel)? onSuccess,
|
||||
String? parentUuid,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: (space) {},
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => CreateSpaceBloc(
|
||||
RemoteCreateSpaceService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -27,20 +73,108 @@ abstract final class SpaceDetailsDialogHelper {
|
||||
static void showEdit(
|
||||
BuildContext context, {
|
||||
required SpaceModel spaceModel,
|
||||
required String communityUuid,
|
||||
required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Edit Space'),
|
||||
spaceModel: spaceModel,
|
||||
onSave: (space) {},
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateSpaceBloc(
|
||||
RemoteUpdateSpaceService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
|
||||
listener: (context, state) => _updateListener(
|
||||
context,
|
||||
state,
|
||||
onSuccess,
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Edit Space'),
|
||||
spaceModel: spaceModel,
|
||||
onSave: (space) => context.read<UpdateSpaceBloc>().add(
|
||||
UpdateSpace(
|
||||
UpdateSpaceParam(
|
||||
communityUuid: communityUuid,
|
||||
space: space,
|
||||
),
|
||||
),
|
||||
),
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void _updateListener(
|
||||
BuildContext context,
|
||||
UpdateSpaceState state,
|
||||
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
) {
|
||||
return switch (state) {
|
||||
UpdateSpaceInitial() => null,
|
||||
UpdateSpaceLoading() => _onLoading(context),
|
||||
UpdateSpaceSuccess(:final space) =>
|
||||
_onUpdateSuccess(context, space, onSuccess),
|
||||
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
|
||||
};
|
||||
}
|
||||
|
||||
static void _onUpdateSuccess(
|
||||
BuildContext context,
|
||||
SpaceDetailsModel space,
|
||||
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
onSuccess?.call(space);
|
||||
}
|
||||
|
||||
static void _onLoading(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
static void _onError(BuildContext context, String errorMessage) {
|
||||
Navigator.of(context).pop();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Error'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void _onCreateSuccess(
|
||||
BuildContext context,
|
||||
SpaceModel space,
|
||||
void Function(SpaceModel updatedSpaceModel)? onSuccess,
|
||||
) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
onSuccess?.call(space);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget {
|
||||
required this.spaceModel,
|
||||
required this.onSave,
|
||||
required this.context,
|
||||
required this.communityUuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
|
||||
final SpaceModel spaceModel;
|
||||
final void Function(SpaceDetailsModel space) onSave;
|
||||
final BuildContext context;
|
||||
final String communityUuid;
|
||||
|
||||
@override
|
||||
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
|
||||
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
|
||||
if (!isCreateMode) {
|
||||
final param = LoadSpaceDetailsParam(
|
||||
spaceUuid: widget.spaceModel.uuid,
|
||||
communityUuid: widget.context
|
||||
.read<CommunitiesTreeSelectionBloc>()
|
||||
.state
|
||||
.selectedCommunity!
|
||||
.uuid,
|
||||
communityUuid: widget.communityUuid,
|
||||
);
|
||||
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||
..._subspaces,
|
||||
Subspace(
|
||||
name: name,
|
||||
uuid: const Uuid().v4(),
|
||||
uuid: '${const Uuid().v4()}-NewTag',
|
||||
productAllocations: const [],
|
||||
),
|
||||
];
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart';
|
||||
class Tag extends Equatable {
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
|
||||
const Tag({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Tag.empty() => const Tag(
|
||||
uuid: '',
|
||||
name: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
);
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||
return Tag(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name, createdAt, updatedAt];
|
||||
List<Object?> get props => [uuid, name];
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
@ -214,9 +222,12 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||
for (final product in newProducts) {
|
||||
_space.productAllocations.add(
|
||||
ProductAllocation(
|
||||
uuid: const Uuid().v4(),
|
||||
uuid: '${const Uuid().v4()}-NewProductUuid',
|
||||
product: product,
|
||||
tag: Tag.empty(),
|
||||
tag: Tag(
|
||||
uuid: '${const Uuid().v4()}-NewTag',
|
||||
name: '',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ProductTagField extends StatefulWidget {
|
||||
final List<Tag> items;
|
||||
@ -53,13 +54,8 @@ class _ProductTagFieldState extends State<ProductTagField> {
|
||||
void _submit(String value) {
|
||||
final lowerCaseValue = value.toLowerCase();
|
||||
final selectedTag = widget.items.firstWhere(
|
||||
(tag) => tag.name.toLowerCase() == lowerCaseValue,
|
||||
orElse: () => Tag(
|
||||
name: value,
|
||||
uuid: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
),
|
||||
(e) => e.name.toLowerCase() == lowerCaseValue,
|
||||
orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
|
||||
);
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
@ -12,21 +14,27 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
|
||||
static const _defaultErrorMessage = 'Failed to update space';
|
||||
|
||||
@override
|
||||
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async {
|
||||
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
|
||||
try {
|
||||
final response = await _httpService.put(
|
||||
path: 'endpoint',
|
||||
body: space.toJson(),
|
||||
expectedResponseModel: (data) => SpaceDetailsModel.fromJson(
|
||||
data as Map<String, dynamic>,
|
||||
),
|
||||
final path = await _makeUrl(param);
|
||||
await _httpService.put(
|
||||
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 isSuccess;
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
return param.space;
|
||||
} 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,
|
||||
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _makeUrl(UpdateSpaceParam param) async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null || projectUuid.isEmpty) {
|
||||
throw APIException('Project UUID is not set');
|
||||
}
|
||||
|
||||
final spaceUuid = param.space.uuid;
|
||||
if (spaceUuid.isEmpty) {
|
||||
throw APIException('Space UUID is not set');
|
||||
}
|
||||
|
||||
final communityUuid = param.communityUuid;
|
||||
if (communityUuid.isEmpty) {
|
||||
throw APIException('Community UUID is not set');
|
||||
}
|
||||
|
||||
return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid';
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
|
||||
class UpdateSpaceParam {
|
||||
UpdateSpaceParam({
|
||||
required this.space,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
final String communityUuid;
|
||||
|
||||
Map<String, dynamic> toJson() => space.toJson();
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
|
||||
abstract class UpdateSpaceService {
|
||||
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space);
|
||||
abstract interface class UpdateSpaceService {
|
||||
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
|
||||
) async {
|
||||
emit(UpdateSpaceLoading());
|
||||
try {
|
||||
final updatedSpace = await _updateSpaceService.updateSpace(event.space);
|
||||
final updatedSpace = await _updateSpaceService.updateSpace(event.param);
|
||||
emit(UpdateSpaceSuccess(updatedSpace));
|
||||
} on APIException catch (e) {
|
||||
emit(UpdateSpaceFailure(e.message));
|
||||
|
@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class UpdateSpace extends UpdateSpaceEvent {
|
||||
const UpdateSpace(this.space);
|
||||
const UpdateSpace(this.param);
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
final UpdateSpaceParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [space];
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
@ -21,10 +21,10 @@ final class UpdateSpaceSuccess extends UpdateSpaceState {
|
||||
}
|
||||
|
||||
final class UpdateSpaceFailure extends UpdateSpaceState {
|
||||
final String message;
|
||||
final String errorMessage;
|
||||
|
||||
const UpdateSpaceFailure(this.message);
|
||||
const UpdateSpaceFailure(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
@ -12,20 +12,20 @@ import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||
|
||||
class DevicesManagementApi {
|
||||
Future<List<AllDevicesModel>> fetchDevices(
|
||||
String communityId, String spaceId, String projectId) async {
|
||||
Future<List<AllDevicesModel>> fetchDevices(String projectId,
|
||||
{List<String>? spacesId, List<String>? communities}) async {
|
||||
try {
|
||||
final response = await HTTPService().get(
|
||||
path: communityId.isNotEmpty && spaceId.isNotEmpty
|
||||
? ApiEndpoints.getSpaceDevices
|
||||
.replaceAll('{spaceUuid}', spaceId)
|
||||
.replaceAll('{communityUuid}', communityId)
|
||||
.replaceAll('{projectId}', projectId)
|
||||
: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId),
|
||||
path: ApiEndpoints.getSpaceDevices.replaceAll('{projectId}', projectId),
|
||||
queryParameters: {
|
||||
if (spacesId != null && spacesId.isNotEmpty) 'spaces': spacesId,
|
||||
if (communities != null && communities.isNotEmpty)
|
||||
'communities': communities,
|
||||
},
|
||||
showServerMessage: true,
|
||||
expectedResponseModel: (json) {
|
||||
List<dynamic> jsonData = json['data'];
|
||||
List<AllDevicesModel> devicesList = jsonData.map((jsonItem) {
|
||||
final List<dynamic> jsonData = json['data'] as List<dynamic>;
|
||||
final List<AllDevicesModel> devicesList = jsonData.map((jsonItem) {
|
||||
return AllDevicesModel.fromJson(jsonItem);
|
||||
}).toList();
|
||||
return devicesList;
|
||||
@ -416,5 +416,4 @@ class DevicesManagementApi {
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,8 +34,9 @@ class UserPermissionApi {
|
||||
path: ApiEndpoints.roleTypes,
|
||||
showServerMessage: true,
|
||||
expectedResponseModel: (json) {
|
||||
final List<RoleTypeModel> fetchedRoles =
|
||||
(json['data'] as List).map((item) => RoleTypeModel.fromJson(item)).toList();
|
||||
final List<RoleTypeModel> fetchedRoles = (json['data'] as List)
|
||||
.map((item) => RoleTypeModel.fromJson(item))
|
||||
.toList();
|
||||
return fetchedRoles;
|
||||
},
|
||||
);
|
||||
@ -47,7 +48,9 @@ class UserPermissionApi {
|
||||
path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid),
|
||||
showServerMessage: true,
|
||||
expectedResponseModel: (json) {
|
||||
return (json as List).map((data) => PermissionOption.fromJson(data)).toList();
|
||||
return (json as List)
|
||||
.map((data) => PermissionOption.fromJson(data))
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
return response ?? [];
|
||||
@ -57,7 +60,7 @@ class UserPermissionApi {
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? email,
|
||||
String? jobTitle,
|
||||
String? companyName,
|
||||
String? phoneNumber,
|
||||
String? roleUuid,
|
||||
List<String>? spaceUuids,
|
||||
@ -68,7 +71,7 @@ class UserPermissionApi {
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
"email": email,
|
||||
"jobTitle": jobTitle != '' ? jobTitle : null,
|
||||
"companyName": companyName != '' ? companyName : null,
|
||||
"phoneNumber": phoneNumber != '' ? phoneNumber : null,
|
||||
"roleUuid": roleUuid,
|
||||
"projectUuid": projectUuid,
|
||||
@ -140,7 +143,7 @@ class UserPermissionApi {
|
||||
String? firstName,
|
||||
String? userId,
|
||||
String? lastName,
|
||||
String? jobTitle,
|
||||
String? companyName,
|
||||
String? phoneNumber,
|
||||
String? roleUuid,
|
||||
List<String>? spaceUuids,
|
||||
@ -150,8 +153,8 @@ class UserPermissionApi {
|
||||
final body = <String, dynamic>{
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
"jobTitle": jobTitle != '' ? jobTitle : " ",
|
||||
"phoneNumber": phoneNumber != '' ? phoneNumber : " ",
|
||||
"companyName": companyName != '' ? companyName : ' ',
|
||||
"phoneNumber": phoneNumber != '' ? phoneNumber : ' ',
|
||||
"roleUuid": roleUuid,
|
||||
"projectUuid": projectUuid,
|
||||
"spaceUuids": spaceUuids,
|
||||
@ -190,12 +193,17 @@ class UserPermissionApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> changeUserStatusById(userUuid, status, String projectUuid) async {
|
||||
Future<bool> changeUserStatusById(
|
||||
userUuid, status, String projectUuid) async {
|
||||
try {
|
||||
Map<String, dynamic> bodya = {"disable": status, "projectUuid": projectUuid};
|
||||
Map<String, dynamic> bodya = {
|
||||
"disable": status,
|
||||
"projectUuid": projectUuid
|
||||
};
|
||||
|
||||
final response = await _httpService.put(
|
||||
path: ApiEndpoints.changeUserStatus.replaceAll("{invitedUserUuid}", userUuid),
|
||||
path: ApiEndpoints.changeUserStatus
|
||||
.replaceAll("{invitedUserUuid}", userUuid),
|
||||
body: bodya,
|
||||
expectedResponseModel: (json) {
|
||||
return json['success'];
|
||||
|
@ -69,7 +69,6 @@ abstract class ColorsManager {
|
||||
static const Color invitedOrange = Color(0xFFFFE193);
|
||||
static const Color invitedOrangeText = Color(0xFFFFBF00);
|
||||
static const Color lightGrayBorderColor = Color(0xB2D5D5D5);
|
||||
//background: #F8F8F8;
|
||||
static const Color vividBlue = Color(0xFF023DFE);
|
||||
static const Color semiTransparentRed = Color(0x99FF0000);
|
||||
static const Color grey700 = Color(0xFF2D3748);
|
||||
@ -85,4 +84,6 @@ abstract class ColorsManager {
|
||||
static const Color minBlueDot = Color(0xFF023DFE);
|
||||
static const Color grey25 = Color(0xFFF9F9F9);
|
||||
static const Color grey50 = Color(0xFF718096);
|
||||
static const Color red100 = Color(0xFFFE0202);
|
||||
static const Color grey800 = Color(0xffF8F8F8);
|
||||
}
|
||||
|
@ -17,8 +17,7 @@ abstract class ApiEndpoints {
|
||||
////// Devices Management ////////////////
|
||||
|
||||
static const String getAllDevices = '/projects/{projectId}/devices';
|
||||
static const String getSpaceDevices =
|
||||
'/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices';
|
||||
static const String getSpaceDevices = '/projects/{projectId}/devices';
|
||||
static const String getDeviceStatus = '/devices/{uuid}/functions/status';
|
||||
static const String getBatchStatus = '/devices/batch';
|
||||
|
||||
@ -141,4 +140,6 @@ abstract class ApiEndpoints {
|
||||
static const String saveSchedule = '/schedule/{deviceUuid}';
|
||||
|
||||
static const String getBookableSpaces = '/bookable-spaces';
|
||||
static const String getBookings =
|
||||
'/bookings?month={mm}%2F{yyyy}&space={space}';
|
||||
}
|
||||
|
@ -517,4 +517,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';
|
||||
}
|
||||
|
Reference in New Issue
Block a user