mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
52 Commits
firebase_p
...
Build-Sche
Author | SHA1 | Date | |
---|---|---|---|
249cbfc242 | |||
8167926620 | |||
559091faa0 | |||
eba351c9be | |||
c112cde634 | |||
65d541d594 | |||
7331c8440b | |||
a409e34643 | |||
7cc59e43df | |||
21f8b2962c | |||
645a07287e | |||
e55e793081 | |||
885ef61114 | |||
bfd6b5c3a0 | |||
2b638940ae | |||
3e95bf4473 | |||
2d16bda61d | |||
5c90d5f6b9 | |||
d6a48850a7 | |||
6cac94a1c4 | |||
9f28e1ccef | |||
83202204b0 | |||
d87739f1fd | |||
5cd083a37b | |||
6534bfae5b | |||
2b8d987c69 | |||
707cb4791f | |||
03c45ed8d0 | |||
9e0ea4ad6f | |||
9e6b14737f | |||
7c2aed2d58 | |||
bcf62027bc | |||
b001713ce4 | |||
bab3226c73 | |||
fa1eaa570c | |||
4cfb984d2c | |||
4c06479469 | |||
3101960201 | |||
ddfd4ee153 | |||
7f0484eec6 | |||
dc7064d142 | |||
e523a83912 | |||
e917225c3d | |||
66ed30b50c | |||
47bd6ff89e | |||
df87e41d61 | |||
f0bfe085a4 | |||
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf |
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
||||
import 'package:syncrow_web/services/access_mang_api.dart';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
|
||||
abstract class AccessState extends Equatable {
|
||||
const AccessState();
|
||||
|
@ -0,0 +1,52 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_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 RemoteBookableSpacesService implements BookableSystemService {
|
||||
const RemoteBookableSpacesService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
static const _defaultErrorMessage = 'Failed to load bookable spaces';
|
||||
|
||||
@override
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getBookableSpaces,
|
||||
queryParameters: {
|
||||
'page': param.page,
|
||||
'size': param.size,
|
||||
'active': true,
|
||||
'configured': true,
|
||||
if (param.search != null &&
|
||||
param.search.isNotEmpty &&
|
||||
param.search != 'null')
|
||||
'search': param.search,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
return PaginatedBookableSpaces.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} 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,170 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||
|
||||
class RemoteCalendarService implements CalendarSystemService {
|
||||
const RemoteCalendarService(this._httpService);
|
||||
|
||||
final HTTPService _httpService;
|
||||
static const _defaultErrorMessage = 'Failed to load Calendar';
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getCalendarEvents,
|
||||
queryParameters: {
|
||||
'spaceId': spaceId,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
return CalendarEventsResponse.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
final errorMessage = responseData['error']?['message'] as String? ??
|
||||
responseData['message'] as String? ??
|
||||
_defaultErrorMessage;
|
||||
throw APIException(errorMessage);
|
||||
}
|
||||
throw APIException(_defaultErrorMessage);
|
||||
} catch (e) {
|
||||
throw APIException('$_defaultErrorMessage: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRemoteCalendarService implements CalendarSystemService {
|
||||
const FakeRemoteCalendarService(this._httpService, {this.useDummy = false});
|
||||
|
||||
final HTTPService _httpService;
|
||||
final bool useDummy;
|
||||
static const _defaultErrorMessage = 'Failed to load Calendar';
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
}) async {
|
||||
if (useDummy) {
|
||||
final dummyJson = {
|
||||
'statusCode': 200,
|
||||
'message': 'Successfully fetched all bookings',
|
||||
'data': [
|
||||
{
|
||||
'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80',
|
||||
'date': '2025-07-11T10:22:00.626Z',
|
||||
'startTime': '09:00:00',
|
||||
'endTime': '12:00:00',
|
||||
'cost': 10,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
},
|
||||
{
|
||||
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
|
||||
'date': '2025-07-11T10:22:00.626Z',
|
||||
'startTime': '12:00:00',
|
||||
'endTime': '13:00:00',
|
||||
'cost': 10,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
},
|
||||
{
|
||||
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
|
||||
'date': '2025-07-13T10:22:00.626Z',
|
||||
'startTime': '15:30:00',
|
||||
'endTime': '19:00:00',
|
||||
'cost': 20,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
}
|
||||
],
|
||||
'success': true
|
||||
};
|
||||
final response = CalendarEventsResponse.fromJson(dummyJson);
|
||||
|
||||
// Filter events by spaceId
|
||||
final filteredData = response.data.where((event) {
|
||||
return event.space.uuid == spaceId;
|
||||
}).toList();
|
||||
print('Filtering events for spaceId: $spaceId');
|
||||
print('Found ${filteredData.length} matching events');
|
||||
return filteredData.isNotEmpty
|
||||
? CalendarEventsResponse(
|
||||
statusCode: response.statusCode,
|
||||
message: response.message,
|
||||
data: filteredData,
|
||||
success: response.success,
|
||||
)
|
||||
: CalendarEventsResponse(
|
||||
statusCode: 404,
|
||||
message: 'No events found for spaceId: $spaceId',
|
||||
data: [],
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getCalendarEvents,
|
||||
queryParameters: {
|
||||
'spaceId': spaceId,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
return CalendarEventsResponse.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
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,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class LoadBookableSpacesParam extends Equatable {
|
||||
const LoadBookableSpacesParam({
|
||||
this.page = 1,
|
||||
this.size = 25,
|
||||
this.search = '',
|
||||
this.active = true,
|
||||
this.configured = true,
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int size;
|
||||
final String search;
|
||||
final bool active;
|
||||
final bool configured;
|
||||
|
||||
LoadBookableSpacesParam copyWith({
|
||||
int? page,
|
||||
int? size,
|
||||
String? search,
|
||||
bool? active,
|
||||
bool? configured,
|
||||
}) {
|
||||
return LoadBookableSpacesParam(
|
||||
page: page ?? this.page,
|
||||
size: size ?? this.size,
|
||||
search: search ?? this.search,
|
||||
active: active ?? this.active,
|
||||
configured: configured ?? this.configured,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, search, active, configured];
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
class BookableSpaceModel {
|
||||
final String uuid;
|
||||
final String spaceName;
|
||||
final String virtualLocation;
|
||||
final BookableConfig bookableConfig;
|
||||
|
||||
BookableSpaceModel({
|
||||
required this.uuid,
|
||||
required this.spaceName,
|
||||
required this.virtualLocation,
|
||||
required this.bookableConfig,
|
||||
});
|
||||
|
||||
factory BookableSpaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return BookableSpaceModel(
|
||||
uuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
virtualLocation: json['virtualLocation'] as String,
|
||||
bookableConfig: BookableConfig.fromJson(
|
||||
json['bookableConfig'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BookableConfig {
|
||||
final String uuid;
|
||||
final List<String> daysAvailable;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final bool active;
|
||||
final int points;
|
||||
|
||||
BookableConfig({
|
||||
required this.uuid,
|
||||
required this.daysAvailable,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.active,
|
||||
required this.points,
|
||||
});
|
||||
|
||||
factory BookableConfig.fromJson(Map<String, dynamic> json) {
|
||||
return BookableConfig(
|
||||
uuid: json['uuid'] as String,
|
||||
daysAvailable: (json['daysAvailable'] as List).cast<String>(),
|
||||
startTime: json['startTime'] as String,
|
||||
endTime: json['endTime'] as String,
|
||||
active: json['active'] as bool,
|
||||
points: json['points'] as int,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,40 @@
|
||||
|
||||
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
|
||||
class PaginatedBookableSpaces {
|
||||
final List<BookableSpaceModel> data;
|
||||
final String message;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalItem;
|
||||
final int totalPage;
|
||||
final bool hasNext;
|
||||
final bool hasPrevious;
|
||||
|
||||
PaginatedBookableSpaces({
|
||||
required this.data,
|
||||
required this.message,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalItem,
|
||||
required this.totalPage,
|
||||
required this.hasNext,
|
||||
required this.hasPrevious,
|
||||
});
|
||||
|
||||
factory PaginatedBookableSpaces.fromJson(Map<String, dynamic> json) {
|
||||
return PaginatedBookableSpaces(
|
||||
data: (json['data'] as List)
|
||||
.map((item) => BookableSpaceModel.fromJson(item))
|
||||
.toList(),
|
||||
message: json['message'] as String,
|
||||
page: json['page'] as int,
|
||||
size: json['size'] as int,
|
||||
totalItem: json['totalItem'] as int,
|
||||
totalPage: json['totalPage'] as int,
|
||||
hasNext: json['hasNext'] as bool,
|
||||
hasPrevious: json['hasPrevious'] as bool,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
|
||||
abstract class BookableSystemService {
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
});
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
|
||||
abstract class CalendarSystemService {
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
});
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import 'dart:async';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
|
||||
|
||||
class DebouncedBookableSpacesService implements BookableSystemService {
|
||||
final BookableSystemService _inner;
|
||||
final Duration debounceDuration;
|
||||
|
||||
Timer? _debounceTimer;
|
||||
Completer<PaginatedBookableSpaces>? _lastCompleter;
|
||||
|
||||
DebouncedBookableSpacesService(
|
||||
this._inner, {
|
||||
this.debounceDuration = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
@override
|
||||
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||
required LoadBookableSpacesParam param,
|
||||
}) {
|
||||
_debounceTimer?.cancel();
|
||||
if (_lastCompleter != null && !_lastCompleter!.isCompleted) {
|
||||
_lastCompleter!.completeError(StateError("Cancelled by new search"));
|
||||
}
|
||||
|
||||
final completer = Completer<PaginatedBookableSpaces>();
|
||||
_lastCompleter = completer;
|
||||
|
||||
_debounceTimer = Timer(debounceDuration, () async {
|
||||
try {
|
||||
final result = await _inner.getBookableSpaces(param: param);
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(result);
|
||||
}
|
||||
} catch (e, st) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e, st);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
|
||||
part 'events_event.dart';
|
||||
part 'events_state.dart';
|
||||
|
||||
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final EventController eventController = EventController();
|
||||
final CalendarSystemService calendarService;
|
||||
|
||||
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 {
|
||||
emit(EventsLoading());
|
||||
try {
|
||||
final response = await calendarService.getCalendarEvents(
|
||||
spaceId: event.spaceId,
|
||||
);
|
||||
final events =
|
||||
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
|
||||
eventController.addAll(events);
|
||||
emit(EventsLoaded(events: events));
|
||||
} catch (e) {
|
||||
emit(EventsError('Failed to load 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],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
|
||||
|
||||
void _onDisposeResources(
|
||||
DisposeResources event, Emitter<CalendarEventState> emit) {
|
||||
eventController.dispose();
|
||||
}
|
||||
|
||||
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
|
||||
if (state is EventsLoaded) {
|
||||
final loaded = state as EventsLoaded;
|
||||
final newWeekDays = _getWeekDays(event.weekDate);
|
||||
emit(EventsLoaded(
|
||||
events: loaded.events,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
|
||||
final date = booking.date;
|
||||
|
||||
final localDate = date.toLocal();
|
||||
|
||||
final startParts = booking.startTime.split(':').map(int.parse).toList();
|
||||
final endParts = booking.endTime.split(':').map(int.parse).toList();
|
||||
|
||||
final startTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
startParts[0],
|
||||
startParts[1],
|
||||
);
|
||||
|
||||
final endTime = DateTime(
|
||||
localDate.year,
|
||||
localDate.month,
|
||||
localDate.day,
|
||||
endParts[0],
|
||||
endParts[1],
|
||||
);
|
||||
|
||||
return CalendarEventData(
|
||||
date: startTime,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
title:
|
||||
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
|
||||
description: 'Cost: ${booking.cost}',
|
||||
color: Colors.blue,
|
||||
event: booking,
|
||||
);
|
||||
}
|
||||
|
||||
List<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)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
eventController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
part of 'events_bloc.dart';
|
||||
|
||||
@immutable
|
||||
abstract class CalendarEventsEvent {
|
||||
const CalendarEventsEvent();
|
||||
}
|
||||
|
||||
class LoadEvents extends CalendarEventsEvent {
|
||||
final String spaceId;
|
||||
final DateTime weekStart;
|
||||
final DateTime weekEnd;
|
||||
|
||||
const LoadEvents({
|
||||
required this.spaceId,
|
||||
required this.weekStart,
|
||||
required this.weekEnd,
|
||||
});
|
||||
}
|
||||
|
||||
class AddEvent extends CalendarEventsEvent {
|
||||
final CalendarEventData event;
|
||||
const AddEvent(this.event);
|
||||
}
|
||||
|
||||
class StartTimer extends CalendarEventsEvent {}
|
||||
|
||||
class DisposeResources extends CalendarEventsEvent {}
|
||||
|
||||
class GoToWeek extends CalendarEventsEvent {
|
||||
final DateTime weekDate;
|
||||
GoToWeek(this.weekDate);
|
||||
}
|
||||
|
||||
class CheckWeekHasEvents extends CalendarEventsEvent {
|
||||
final DateTime weekStart;
|
||||
const CheckWeekHasEvents(this.weekStart);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
part of 'events_bloc.dart';
|
||||
|
||||
@immutable
|
||||
abstract class CalendarEventState {}
|
||||
|
||||
class EventsInitial extends CalendarEventState {}
|
||||
|
||||
class EventsLoading extends CalendarEventState {}
|
||||
|
||||
class EventsLoaded extends CalendarEventState {
|
||||
final List<CalendarEventData> events;
|
||||
|
||||
EventsLoaded({
|
||||
required this.events,
|
||||
});
|
||||
}
|
||||
|
||||
class EventsError extends CalendarEventState {
|
||||
final String message;
|
||||
EventsError(this.message);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||
import 'date_selection_state.dart';
|
||||
|
||||
|
||||
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
|
||||
DateSelectionBloc() : super(DateSelectionState.initial()) {
|
||||
on<SelectDate>((event, emit) {
|
||||
final newWeekStart = _getStartOfWeek(event.selectedDate);
|
||||
emit(state.copyWith(
|
||||
selectedDate: event.selectedDate,
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<NextWeek>((event, emit) {
|
||||
final newWeekStart = state.weekStart.add(const Duration(days: 7));
|
||||
emit(state.copyWith(
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<PreviousWeek>((event, emit) {
|
||||
final newWeekStart = state.weekStart.subtract(const Duration(days: 7));
|
||||
emit(state.copyWith(
|
||||
weekStart: newWeekStart,
|
||||
));
|
||||
});
|
||||
|
||||
on<SelectDateFromSidebarCalendar>((event, emit) {
|
||||
emit(state.copyWith(
|
||||
selectedDateFromSideBarCalender: event.selectedDate,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
static DateTime _getStartOfWeek(DateTime date) {
|
||||
return date.subtract(Duration(days: date.weekday - 1));
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
|
||||
abstract class DateSelectionEvent {
|
||||
const DateSelectionEvent();
|
||||
}
|
||||
|
||||
class SelectDate extends DateSelectionEvent {
|
||||
final DateTime selectedDate;
|
||||
const SelectDate(this.selectedDate);
|
||||
}
|
||||
|
||||
class NextWeek extends DateSelectionEvent {}
|
||||
|
||||
class PreviousWeek extends DateSelectionEvent {}
|
||||
|
||||
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
|
||||
final DateTime selectedDate;
|
||||
SelectDateFromSidebarCalendar(this.selectedDate);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
|
||||
class DateSelectionState {
|
||||
final DateTime selectedDate;
|
||||
final DateTime weekStart;
|
||||
final DateTime? selectedDateFromSideBarCalender;
|
||||
|
||||
DateSelectionState({
|
||||
required this.selectedDate,
|
||||
required this.weekStart,
|
||||
this.selectedDateFromSideBarCalender,
|
||||
});
|
||||
|
||||
factory DateSelectionState.initial() {
|
||||
final now = DateTime.now();
|
||||
final weekStart = now.subtract(Duration(days: now.weekday - 1));
|
||||
return DateSelectionState(
|
||||
selectedDate: now,
|
||||
weekStart: weekStart,
|
||||
selectedDateFromSideBarCalender: null,
|
||||
);
|
||||
}
|
||||
|
||||
DateSelectionState copyWith({
|
||||
DateTime? selectedDate,
|
||||
DateTime? weekStart,
|
||||
DateTime? selectedDateFromSideBarCalender,
|
||||
}) {
|
||||
return DateSelectionState(
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
weekStart: weekStart ?? this.weekStart,
|
||||
selectedDateFromSideBarCalender: selectedDateFromSideBarCalender ?? this.selectedDateFromSideBarCalender,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
part 'selected_bookable_space_event.dart';
|
||||
part 'selected_bookable_space_state.dart';
|
||||
|
||||
class SelectedBookableSpaceBloc
|
||||
extends Bloc<SelectedBookableSpaceEvent, SelectedBookableSpaceState> {
|
||||
SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) {
|
||||
on<SelectBookableSpace>((event, emit) {
|
||||
emit(SelectedBookableSpaceState(
|
||||
selectedBookableSpace: event.bookableSpace));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
part of 'selected_bookable_space_bloc.dart';
|
||||
|
||||
abstract class SelectedBookableSpaceEvent {
|
||||
const SelectedBookableSpaceEvent();
|
||||
}
|
||||
|
||||
class SelectBookableSpace extends SelectedBookableSpaceEvent {
|
||||
final BookableSpaceModel bookableSpace;
|
||||
|
||||
const SelectBookableSpace(this.bookableSpace);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
part of 'selected_bookable_space_bloc.dart';
|
||||
|
||||
class SelectedBookableSpaceState {
|
||||
final BookableSpaceModel? selectedBookableSpace;
|
||||
|
||||
const SelectedBookableSpaceState(
|
||||
{ this.selectedBookableSpace,}
|
||||
);
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||
|
||||
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
|
||||
final BookableSystemService _bookingService;
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 20;
|
||||
String _currentSearch = '';
|
||||
|
||||
SidebarBloc(this._bookingService)
|
||||
: super(SidebarState(
|
||||
allRooms: [],
|
||||
displayedRooms: [],
|
||||
isLoading: true,
|
||||
hasMore: true,
|
||||
)) {
|
||||
on<LoadBookableSpaces>(_onLoadBookableSpaces);
|
||||
on<LoadMoreSpaces>(_onLoadMoreSpaces);
|
||||
on<SelectRoomEvent>(_onSelectRoom);
|
||||
on<SearchRoomsEvent>(_onSearchRooms);
|
||||
on<ResetSearch>(_onResetSearch);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookableSpaces(
|
||||
LoadBookableSpaces event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
_currentPage = 1;
|
||||
_currentSearch = '';
|
||||
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
allRooms: paginatedSpaces.data,
|
||||
displayedRooms: paginatedSpaces.data,
|
||||
isLoading: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'Failed to load rooms: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreSpaces(
|
||||
LoadMoreSpaces event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
if (!state.hasMore || state.isLoadingMore) return;
|
||||
|
||||
try {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
_currentPage++;
|
||||
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
|
||||
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
|
||||
|
||||
emit(state.copyWith(
|
||||
allRooms: updatedRooms,
|
||||
displayedRooms: updatedRooms,
|
||||
isLoadingMore: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
_currentPage--;
|
||||
emit(state.copyWith(
|
||||
isLoadingMore: false,
|
||||
errorMessage: 'Failed to load more rooms: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSearchRooms(
|
||||
SearchRoomsEvent event,
|
||||
Emitter<SidebarState> emit,
|
||||
) async {
|
||||
try {
|
||||
_currentSearch = event.query;
|
||||
_currentPage = 1;
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||
param: LoadBookableSpacesParam(
|
||||
page: _currentPage,
|
||||
size: _pageSize,
|
||||
search: _currentSearch,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
allRooms: paginatedSpaces.data,
|
||||
displayedRooms: paginatedSpaces.data,
|
||||
isLoading: false,
|
||||
hasMore: paginatedSpaces.hasNext,
|
||||
totalPages: paginatedSpaces.totalPage,
|
||||
currentPage: _currentPage,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: 'Search failed: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onResetSearch(
|
||||
ResetSearch event,
|
||||
Emitter<SidebarState> emit,
|
||||
) {
|
||||
_currentSearch = '';
|
||||
add(LoadBookableSpaces());
|
||||
}
|
||||
|
||||
void _onSelectRoom(
|
||||
SelectRoomEvent event,
|
||||
Emitter<SidebarState> emit,
|
||||
) {
|
||||
emit(state.copyWith(selectedRoomId: event.roomId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
abstract class SidebarEvent {}
|
||||
|
||||
class LoadBookableSpaces extends SidebarEvent {}
|
||||
|
||||
class SelectRoomEvent extends SidebarEvent {
|
||||
final String roomId;
|
||||
|
||||
SelectRoomEvent(this.roomId);
|
||||
}
|
||||
|
||||
class SearchRoomsEvent extends SidebarEvent {
|
||||
final String query;
|
||||
|
||||
SearchRoomsEvent(this.query);
|
||||
}
|
||||
|
||||
class LoadMoreSpaces extends SidebarEvent {}
|
||||
|
||||
class ResetSearch extends SidebarEvent {}
|
||||
|
||||
class ExecuteSearch extends SidebarEvent {
|
||||
final String query;
|
||||
|
||||
ExecuteSearch(this.query);
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
|
||||
class SidebarState {
|
||||
final List<BookableSpaceModel> allRooms;
|
||||
final List<BookableSpaceModel> displayedRooms;
|
||||
final bool isLoading;
|
||||
final bool isLoadingMore;
|
||||
final String? errorMessage;
|
||||
final String? selectedRoomId;
|
||||
final bool hasMore;
|
||||
final int totalPages;
|
||||
final int currentPage;
|
||||
|
||||
SidebarState({
|
||||
required this.allRooms,
|
||||
required this.displayedRooms,
|
||||
required this.isLoading,
|
||||
this.isLoadingMore = false,
|
||||
this.errorMessage,
|
||||
this.selectedRoomId,
|
||||
this.hasMore = true,
|
||||
this.totalPages = 0,
|
||||
this.currentPage = 1,
|
||||
});
|
||||
|
||||
SidebarState copyWith({
|
||||
List<BookableSpaceModel>? allRooms,
|
||||
List<BookableSpaceModel>? displayedRooms,
|
||||
bool? isLoading,
|
||||
bool? isLoadingMore,
|
||||
String? errorMessage,
|
||||
String? selectedRoomId,
|
||||
bool? hasMore,
|
||||
int? totalPages,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return SidebarState(
|
||||
allRooms: allRooms ?? this.allRooms,
|
||||
displayedRooms: displayedRooms ?? this.displayedRooms,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/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/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';
|
||||
|
||||
class BookingPage extends StatefulWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
State<BookingPage> createState() => _BookingPageState();
|
||||
}
|
||||
|
||||
class _BookingPageState extends State<BookingPage> {
|
||||
late final EventController _eventController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_eventController = EventController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispatchLoadEvents(BuildContext context) {
|
||||
final selectedRoom =
|
||||
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
|
||||
final dateState = context.read<DateSelectionBloc>().state;
|
||||
|
||||
if (selectedRoom != null) {
|
||||
context.read<CalendarEventsBloc>().add(
|
||||
LoadEvents(
|
||||
spaceId: selectedRoom.uuid,
|
||||
weekStart: dateState.weekStart,
|
||||
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (_) => CalendarEventsBloc(
|
||||
calendarService:
|
||||
FakeRemoteCalendarService(HTTPService(), useDummy: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) =>
|
||||
BlocListener<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(
|
||||
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 WeekNavigation(
|
||||
weekStart: weekStart,
|
||||
weekEnd: weekEnd,
|
||||
onPreviousWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(PreviousWeek());
|
||||
},
|
||||
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 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingSidebar extends StatelessWidget {
|
||||
final void Function(BookableSpaceModel) onRoomSelected;
|
||||
|
||||
const BookingSidebar({
|
||||
super.key,
|
||||
required this.onRoomSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SidebarBloc(RemoteBookableSpacesService(
|
||||
HTTPService(),
|
||||
))
|
||||
..add(LoadBookableSpaces()),
|
||||
child: _SidebarContent(onRoomSelected: onRoomSelected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarContent extends StatefulWidget {
|
||||
final void Function(BookableSpaceModel) onRoomSelected;
|
||||
|
||||
const _SidebarContent({
|
||||
required this.onRoomSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SidebarContent> createState() => __SidebarContentState();
|
||||
}
|
||||
|
||||
class __SidebarContentState extends State<_SidebarContent> {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_scrollListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (_scrollController.position.pixels ==
|
||||
_scrollController.position.maxScrollExtent) {
|
||||
context.read<SidebarBloc>().add(LoadMoreSpaces());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSearch(String value) {
|
||||
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<SidebarBloc, SidebarState>(
|
||||
listener: (context, state) {
|
||||
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const _SidebarHeader(title: 'Spaces'),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(0, -2),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.counterBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: _handleSearch,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search',
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: SvgPicture.asset(
|
||||
Assets.searchIconUser,
|
||||
color: ColorsManager.primaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8, horizontal: 12),
|
||||
border: const OutlineInputBorder(
|
||||
borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
context.read<SidebarBloc>().add(ResetSearch());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isLoading)
|
||||
const Expanded(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (state.errorMessage != null)
|
||||
Expanded(
|
||||
child: Center(child: Text(state.errorMessage!)),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount:
|
||||
state.displayedRooms.length + (state.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.displayedRooms.length) {
|
||||
return _buildLoadMoreIndicator(state);
|
||||
}
|
||||
|
||||
final room = state.displayedRooms[index];
|
||||
return RoomListItem(
|
||||
room: room,
|
||||
isSelected: state.selectedRoomId == room.uuid,
|
||||
onTap: () {
|
||||
context
|
||||
.read<SidebarBloc>()
|
||||
.add(SelectRoomEvent(room.uuid));
|
||||
widget.onRoomSelected(room);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadMoreIndicator(SidebarState state) {
|
||||
if (state.isLoadingMore) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else if (state.hasMore) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(child: Text('Scroll to load more')),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _SidebarHeader({
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.primaryTextColor,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CustomCalendarPage extends StatefulWidget {
|
||||
final DateTime selectedDate;
|
||||
final Function(int day, int month, int year) onDateChanged;
|
||||
|
||||
const CustomCalendarPage({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onDateChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomCalendarPage> createState() => _CustomCalendarPageState();
|
||||
}
|
||||
|
||||
class _CustomCalendarPageState extends State<CustomCalendarPage> {
|
||||
late DateTime _selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDate = widget.selectedDate;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomCalendarPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedDate != oldWidget.selectedDate) {
|
||||
setState(() {
|
||||
_selectedDate = widget.selectedDate;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = CalendarDatePicker2Config(
|
||||
calendarType: CalendarDatePicker2Type.single,
|
||||
selectedDayHighlightColor: const Color(0xFF3B82F6),
|
||||
selectedDayTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
dayTextStyle: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
weekdayLabelTextStyle: const TextStyle(
|
||||
color: ColorsManager.grey50,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
controlsTextStyle: const TextStyle(
|
||||
color: Color(0xFF232D3A),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 18,
|
||||
),
|
||||
centerAlignModePicker: false,
|
||||
disableMonthPicker: true,
|
||||
firstDayOfWeek: 1,
|
||||
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
|
||||
);
|
||||
|
||||
return CalendarDatePicker2(
|
||||
config: config,
|
||||
value: [_selectedDate],
|
||||
onValueChanged: (dates) {
|
||||
final picked = dates.first;
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_selectedDate = picked;
|
||||
});
|
||||
widget.onDateChanged(picked.day, picked.month, picked.year);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EventTileWidget extends StatelessWidget {
|
||||
final List<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 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -20,13 +20,13 @@ class SvgTextButton extends StatelessWidget {
|
||||
required this.onPressed,
|
||||
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||
this.svgColor = const Color(0xFF496EFF),
|
||||
this.labelColor = Colors.black87,
|
||||
this.labelColor = Colors.black,
|
||||
this.borderRadius = 10.0,
|
||||
this.boxShadow = const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.textGray,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
this.svgSize = 24.0,
|
||||
@ -53,15 +53,14 @@ class SvgTextButton extends StatelessWidget {
|
||||
svgAsset,
|
||||
width: svgSize,
|
||||
height: svgSize,
|
||||
color: svgColor,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RoomListItem extends StatelessWidget {
|
||||
final BookableSpaceModel room;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const RoomListItem({
|
||||
required this.room,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RadioListTile(
|
||||
value: room.uuid,
|
||||
contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
|
||||
groupValue: isSelected ? room.uuid : null,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
onChanged: (value) => onTap(),
|
||||
activeColor: ColorsManager.primaryColor,
|
||||
title: Text(
|
||||
room.spaceName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12),
|
||||
),
|
||||
subtitle: Text(
|
||||
room.virtualLocation,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.textGray,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class WeeklyCalendarPage extends StatelessWidget {
|
||||
final DateTime weekStart;
|
||||
final DateTime selectedDate;
|
||||
final EventController eventController;
|
||||
final String? startTime;
|
||||
final String? endTime;
|
||||
final DateTime? selectedDateFromSideBarCalender;
|
||||
|
||||
const WeeklyCalendarPage({
|
||||
super.key,
|
||||
required this.weekStart,
|
||||
required this.selectedDate,
|
||||
required this.eventController,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.selectedDateFromSideBarCalender,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startHour = _parseHour(startTime, defaultValue: 0);
|
||||
final endHour = _parseHour(endTime, defaultValue: 24);
|
||||
|
||||
if (endTime == null || endTime!.isEmpty) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
size: 80,
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Please select a bookable space to view the calendar.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorsManager.lightGrayColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
final DateTime highlightStart = DateTime(2025, 7, 10);
|
||||
final DateTime highlightEnd = DateTime(2025, 7, 19);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double calendarWidth = constraints.maxWidth;
|
||||
final double dayColumnWidth =
|
||||
(calendarWidth - timeLineWidth) / totalDays - 0.1;
|
||||
bool isInRange(DateTime date, DateTime start, DateTime end) {
|
||||
return !date.isBefore(start) && !date.isAfter(end);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
|
||||
child: Stack(
|
||||
children: [
|
||||
WeekView(
|
||||
weekDetectorBuilder: ({
|
||||
required date,
|
||||
required height,
|
||||
required heightPerMinute,
|
||||
required minuteSlotSize,
|
||||
required width,
|
||||
}) {
|
||||
return isInRange(date, highlightStart, highlightEnd)
|
||||
? HatchedColumnBackground(
|
||||
backgroundColor: ColorsManager.grey800,
|
||||
lineColor: ColorsManager.textGray,
|
||||
opacity: 0.3,
|
||||
stripeSpacing: 12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
pageViewPhysics: const NeverScrollableScrollPhysics(),
|
||||
key: ValueKey(weekStart),
|
||||
controller: eventController,
|
||||
initialDay: weekStart,
|
||||
startHour: startHour - 1,
|
||||
endHour: endHour,
|
||||
heightPerMinute: 1.1,
|
||||
showLiveTimeLineInAllDays: false,
|
||||
showVerticalLines: true,
|
||||
emulateVerticalOffsetBy: -80,
|
||||
startDay: WeekDays.monday,
|
||||
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
|
||||
showBullet: false,
|
||||
height: 0,
|
||||
),
|
||||
weekDayBuilder: (date) {
|
||||
return WeekDayHeader(
|
||||
date: date,
|
||||
isSelectedDay: isSameDay(date, selectedDate),
|
||||
);
|
||||
},
|
||||
timeLineBuilder: (date) {
|
||||
return TimeLineWidget(date: date);
|
||||
},
|
||||
timeLineWidth: timeLineWidth,
|
||||
weekPageHeaderBuilder: (start, end) => Container(),
|
||||
weekTitleHeight: 60,
|
||||
weekNumberBuilder: (firstDayOfWeek) => Padding(
|
||||
padding: const EdgeInsets.only(right: 15, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
eventTileBuilder: (date, events, boundary, start, end) {
|
||||
return EventTileWidget(
|
||||
events: events,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (selectedDayIndex >= 0)
|
||||
Positioned(
|
||||
left: (timeLineWidth + 3) +
|
||||
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: dayColumnWidth,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 0, horizontal: 4),
|
||||
color: ColorsManager.spaceColor.withOpacity(0.07),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selectedSidebarIndex >= 0 &&
|
||||
selectedSidebarIndex != selectedDayIndex)
|
||||
Positioned(
|
||||
left: (timeLineWidth + 3) +
|
||||
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: dayColumnWidth,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 0, horizontal: 4),
|
||||
color: Colors.orange.withOpacity(0.14),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 50,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
|
||||
}
|
||||
|
||||
int _parseHour(String? time, {required int defaultValue}) {
|
||||
if (time == null || time.isEmpty || !time.contains(':')) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return int.parse(time.split(':')[0]);
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.blueGrey[100],
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Side bar',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {}),
|
||||
SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {})
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
@ -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'),
|
||||
)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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/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/presentation/bloc/space_details_bloc.dart';
|
||||
@ -14,21 +16,37 @@ import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatelessWidget {
|
||||
class SpaceManagementPage extends StatefulWidget {
|
||||
const SpaceManagementPage({super.key});
|
||||
|
||||
@override
|
||||
State<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(
|
||||
@ -36,6 +54,11 @@ class SpaceManagementPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ProductsBloc(
|
||||
RemoteProductsService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text(
|
||||
|
@ -1,13 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CommunityStructureCanvas extends StatefulWidget {
|
||||
const CommunityStructureCanvas({
|
||||
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final double _horizontalSpacing = 150.0;
|
||||
final double _verticalSpacing = 120.0;
|
||||
|
||||
late TransformationController _transformationController;
|
||||
late AnimationController _animationController;
|
||||
late final TransformationController _transformationController;
|
||||
late final AnimationController _animationController;
|
||||
SpaceReorderDataModel? _draggedData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
const scale = 1.5;
|
||||
const scale = 1;
|
||||
final viewSize = context.size;
|
||||
if (viewSize == null) return;
|
||||
|
||||
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<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,
|
||||
),
|
||||
);
|
||||
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
_positions.clear();
|
||||
final community = widget.community;
|
||||
|
||||
_calculateLayout(community.spaces, 0, {});
|
||||
final levelXOffset = <int, double>{};
|
||||
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
@ -193,7 +216,24 @@ 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(communityUuid: widget.community.uuid),
|
||||
),
|
||||
);
|
||||
|
||||
return [
|
||||
CustomPaint(
|
||||
@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
List<SpaceModel> spaces,
|
||||
List<Widget> widgets,
|
||||
List<SpaceConnectionModel> connections,
|
||||
Set<String> highlightedUuids,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
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;
|
||||
if (position == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
final spaceCard = SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.community.uuid,
|
||||
),
|
||||
);
|
||||
|
||||
final reorderData = SpaceReorderDataModel(
|
||||
space: space,
|
||||
parent: parent,
|
||||
community: community,
|
||||
);
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
child: SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
child: Draggable<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,
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 5,
|
||||
height: MediaQuery.sizeOf(context).height * 5,
|
||||
width: context.screenWidth * 5,
|
||||
height: context.screenHeight * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
),
|
||||
),
|
||||
|
@ -3,7 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
@ -11,6 +14,26 @@ import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
class CommunityStructureHeader extends StatelessWidget {
|
||||
const CommunityStructureHeader({super.key});
|
||||
|
||||
List<SpaceModel> _updateRecursive(
|
||||
List<SpaceModel> spaces,
|
||||
SpaceDetailsModel updatedSpace,
|
||||
) {
|
||||
return spaces.map((space) {
|
||||
if (space.uuid == updatedSpace.uuid) {
|
||||
return space.copyWith(
|
||||
spaceName: updatedSpace.spaceName,
|
||||
icon: updatedSpace.icon,
|
||||
);
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
children: _updateRecursive(space.children, updatedSpace),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@ -55,8 +78,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Community Structure',
|
||||
style: theme.textTheme.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: theme.textTheme.headlineLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
if (selectedCommunity != null)
|
||||
Row(
|
||||
@ -67,8 +91,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
selectedCommunity.name,
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
@ -93,12 +118,24 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) {},
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) {
|
||||
SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
);
|
||||
},
|
||||
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
communityUuid: selectedCommunity.uuid,
|
||||
onSuccess: (updatedSpaceDetails) {
|
||||
final communitiesBloc = context.read<CommunitiesBloc>();
|
||||
final updatedSpaces = _updateRecursive(
|
||||
selectedCommunity.spaces,
|
||||
updatedSpaceDetails,
|
||||
);
|
||||
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
|
||||
communitiesBloc.add(CommunitiesUpdateCommunity(community));
|
||||
},
|
||||
),
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
],
|
||||
|
@ -2,38 +2,66 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceButton extends StatelessWidget {
|
||||
const CreateSpaceButton({super.key});
|
||||
class CreateSpaceButton extends StatefulWidget {
|
||||
const CreateSpaceButton({
|
||||
required this.communityUuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String communityUuid;
|
||||
|
||||
@override
|
||||
State<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.communityUuid,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: _isHovered ? 1.0 : 0.45,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.borderColor, width: 2),
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -22,22 +22,20 @@ 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: 0,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
|
@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
|
||||
final selectedCommunity = selectionBloc.selectedCommunity;
|
||||
final selectedSpace = selectionBloc.selectedSpace;
|
||||
const spacer = Spacer(flex: 10);
|
||||
const spacer = Spacer(flex: 6);
|
||||
return Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
replacement: Row(
|
||||
children: [
|
||||
spacer,
|
||||
Expanded(
|
||||
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
|
||||
),
|
||||
spacer
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -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(
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_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 RemoteProductsService implements ProductsService {
|
||||
const RemoteProductsService(this._httpService);
|
||||
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
|
||||
static const _defaultErrorMessage = 'Failed to load devices';
|
||||
|
||||
@override
|
||||
Future<List<Product>> getProducts(LoadProductsParam param) async {
|
||||
Future<List<Product>> getProducts() async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'devices',
|
||||
queryParameters: {
|
||||
'spaceUuid': param.spaceUuid,
|
||||
if (param.type != null) 'type': param.type,
|
||||
if (param.status != null) 'status': param.status,
|
||||
},
|
||||
path: ApiEndpoints.listProducts,
|
||||
expectedResponseModel: (data) {
|
||||
return (data as List)
|
||||
final json = data as Map<String, dynamic>;
|
||||
final products = json['data'] as List<dynamic>;
|
||||
return products
|
||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
},
|
||||
|
@ -1,18 +1,24 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class Product extends Equatable {
|
||||
final String uuid;
|
||||
final String name;
|
||||
|
||||
const Product({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.productType,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String productType;
|
||||
|
||||
String get icon => _mapIconToProduct(productType);
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
return Product(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
productType: json['prodType'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,9 +26,37 @@ class Product extends Equatable {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'productType': productType,
|
||||
};
|
||||
}
|
||||
|
||||
static String _mapIconToProduct(String prodType) {
|
||||
const iconMapping = {
|
||||
'1G': Assets.Gang1SwitchIcon,
|
||||
'1GT': Assets.oneTouchSwitch,
|
||||
'2G': Assets.Gang2SwitchIcon,
|
||||
'2GT': Assets.twoTouchSwitch,
|
||||
'3G': Assets.Gang3SwitchIcon,
|
||||
'3GT': Assets.threeTouchSwitch,
|
||||
'CUR': Assets.curtain,
|
||||
'CUR_2': Assets.curtain,
|
||||
'GD': Assets.garageDoor,
|
||||
'GW': Assets.SmartGatewayIcon,
|
||||
'DL': Assets.DoorLockIcon,
|
||||
'WL': Assets.waterLeakSensor,
|
||||
'WH': Assets.waterHeater,
|
||||
'WM': Assets.waterLeakSensor,
|
||||
'SOS': Assets.sos,
|
||||
'AC': Assets.ac,
|
||||
'CPS': Assets.presenceSensor,
|
||||
'PC': Assets.powerClamp,
|
||||
'WPS': Assets.presenceSensor,
|
||||
'DS': Assets.doorSensor
|
||||
};
|
||||
|
||||
return iconMapping[prodType] ?? Assets.presenceSensor;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name];
|
||||
List<Object?> get props => [uuid, name, productType];
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
class LoadProductsParam {
|
||||
final String spaceUuid;
|
||||
final String? type;
|
||||
final String? status;
|
||||
|
||||
const LoadProductsParam({
|
||||
required this.spaceUuid,
|
||||
this.type,
|
||||
this.status,
|
||||
});
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
||||
|
||||
abstract class ProductsService {
|
||||
Future<List<Product>> getProducts(LoadProductsParam param);
|
||||
Future<List<Product>> getProducts();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
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/products/domain/params/load_products_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -9,20 +8,20 @@ part 'products_event.dart';
|
||||
part 'products_state.dart';
|
||||
|
||||
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
||||
final ProductsService _deviceService;
|
||||
|
||||
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
|
||||
ProductsBloc(this._productsService) : super(ProductsInitial()) {
|
||||
on<LoadProducts>(_onLoadProducts);
|
||||
}
|
||||
|
||||
final ProductsService _productsService;
|
||||
|
||||
Future<void> _onLoadProducts(
|
||||
LoadProducts event,
|
||||
Emitter<ProductsState> emit,
|
||||
) async {
|
||||
emit(ProductsLoading());
|
||||
try {
|
||||
final devices = await _deviceService.getProducts(event.param);
|
||||
emit(ProductsLoaded(devices));
|
||||
final products = await _productsService.getProducts();
|
||||
emit(ProductsLoaded(products));
|
||||
} on APIException catch (e) {
|
||||
emit(ProductsFailure(e.message));
|
||||
} catch (e) {
|
||||
|
@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class LoadProducts extends ProductsEvent {
|
||||
const LoadProducts(this.param);
|
||||
|
||||
final LoadProductsParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
const LoadProducts();
|
||||
}
|
||||
|
@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
|
||||
}
|
||||
|
||||
final class ProductsFailure extends ProductsState {
|
||||
final String message;
|
||||
final String errorMessage;
|
||||
|
||||
const ProductsFailure(this.message);
|
||||
const ProductsFailure(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
@ -2,6 +2,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/utils/constants/assets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceDetailsModel extends Equatable {
|
||||
final String uuid;
|
||||
@ -21,7 +22,7 @@ class SpaceDetailsModel extends Equatable {
|
||||
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||
uuid: '',
|
||||
spaceName: '',
|
||||
icon: Assets.villa,
|
||||
icon: Assets.location,
|
||||
productAllocations: [],
|
||||
subspaces: [],
|
||||
);
|
||||
@ -31,24 +32,14 @@ class SpaceDetailsModel extends Equatable {
|
||||
spaceName: json['spaceName'] as String,
|
||||
icon: json['icon'] as String,
|
||||
productAllocations: (json['productAllocations'] as List)
|
||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
subspaces: (json['subspaces'] as List)
|
||||
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
@ -70,40 +61,38 @@ class SpaceDetailsModel extends Equatable {
|
||||
}
|
||||
|
||||
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 {
|
||||
'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 => [product, tag];
|
||||
List<Object?> get props => [uuid, product, tag];
|
||||
}
|
||||
|
||||
class Subspace extends Equatable {
|
||||
@ -127,14 +116,6 @@ class Subspace extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Subspace copyWith({
|
||||
String? uuid,
|
||||
String? name,
|
||||
|
@ -2,23 +2,37 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
abstract final class SpaceDetailsDialogHelper {
|
||||
static void showCreate(BuildContext context) {
|
||||
static void showCreate(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: print,
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: (space) {},
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -27,20 +41,98 @@ abstract final class SpaceDetailsDialogHelper {
|
||||
static void showEdit(
|
||||
BuildContext context, {
|
||||
required SpaceModel spaceModel,
|
||||
required String communityUuid,
|
||||
required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
}) {
|
||||
showDialog<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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,14 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
super.key,
|
||||
required this.onSave,
|
||||
required this.onCancel,
|
||||
this.saveButtonLabel = 'OK',
|
||||
this.cancelButtonLabel = 'Cancel',
|
||||
});
|
||||
|
||||
final VoidCallback onCancel;
|
||||
final VoidCallback? onSave;
|
||||
final String saveButtonLabel;
|
||||
final String cancelButtonLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -27,10 +31,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCancelButton(BuildContext context) {
|
||||
return CancelButton(
|
||||
onPressed: onCancel,
|
||||
label: 'Cancel',
|
||||
);
|
||||
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
@ -39,7 +40,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
borderRadius: 10,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
child: Text(saveButtonLabel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.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/presentation/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.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/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/enum/device_types.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
const SpaceDetailsDevicesBox({
|
||||
@ -15,11 +21,18 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productAllocations = space.productAllocations;
|
||||
final subspaces = space.subspaces;
|
||||
final isAnySubspaceHasProductAllocations =
|
||||
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
|
||||
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
|
||||
final allAllocations = [
|
||||
...space.productAllocations,
|
||||
...space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
if (allAllocations.isNotEmpty) {
|
||||
final productCounts = <String, int>{};
|
||||
for (final allocation in allAllocations) {
|
||||
final productType = allocation.product.productType;
|
||||
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
@ -35,46 +48,40 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
// ...TagHelper.groupTags([
|
||||
// ...?tags,
|
||||
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
|
||||
// ]).entries.map(
|
||||
// (entry) => Chip(
|
||||
// avatar: SizedBox(
|
||||
// width: 24,
|
||||
// height: 24,
|
||||
// child: SvgPicture.asset(
|
||||
// entry.key.icon ?? 'assets/icons/gateway.svg',
|
||||
// fit: BoxFit.contain,
|
||||
// ),
|
||||
// ),
|
||||
// label: Text(
|
||||
// 'x${entry.value}',
|
||||
// style: Theme.of(context)
|
||||
// .textTheme
|
||||
// .bodySmall
|
||||
// ?.copyWith(color: ColorsManager.spaceColor),
|
||||
// ),
|
||||
// backgroundColor: ColorsManager.whiteColors,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(16),
|
||||
// side: const BorderSide(
|
||||
// color: ColorsManager.spaceColor,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
EditChip(
|
||||
onTap: () {},
|
||||
),
|
||||
...productCounts.entries.map((entry) {
|
||||
final productType = entry.key;
|
||||
final count = entry.value;
|
||||
return Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
_getDeviceIcon(productType),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x$count',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
EditChip(onTap: () => _showAssignTagsDialog(context)),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () => _showAssignTagsDialog(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
@ -83,10 +90,50 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
child: ButtonContentWidget(
|
||||
svgAssets: Assets.addIcon,
|
||||
label: 'Add Devices',
|
||||
// disabled: isTagsAndSubspaceModelDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAssignTagsDialog(BuildContext context) {
|
||||
showDialog<SpaceDetailsModel>(
|
||||
context: context,
|
||||
builder: (context) => AssignTagsDialog(space: space),
|
||||
).then((resultSpace) {
|
||||
if (resultSpace != null) {
|
||||
if (context.mounted) {
|
||||
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _getDeviceIcon(String productType) =>
|
||||
switch (devicesTypesMap[productType]) {
|
||||
DeviceType.LightBulb => Assets.lightBulb,
|
||||
DeviceType.CeilingSensor => Assets.sensors,
|
||||
DeviceType.AC => Assets.ac,
|
||||
DeviceType.DoorLock => Assets.doorLock,
|
||||
DeviceType.Curtain => Assets.curtain,
|
||||
DeviceType.ThreeGang => Assets.gangSwitch,
|
||||
DeviceType.Gateway => Assets.gateway,
|
||||
DeviceType.OneGang => Assets.oneGang,
|
||||
DeviceType.TwoGang => Assets.twoGang,
|
||||
DeviceType.WH => Assets.waterHeater,
|
||||
DeviceType.DoorSensor => Assets.openCloseDoor,
|
||||
DeviceType.GarageDoor => Assets.openedDoor,
|
||||
DeviceType.WaterLeak => Assets.waterLeakNormal,
|
||||
DeviceType.Curtain2 => Assets.curtainIcon,
|
||||
DeviceType.Blind => Assets.curtainIcon,
|
||||
DeviceType.WallSensor => Assets.sensors,
|
||||
DeviceType.DS => Assets.openCloseDoor,
|
||||
DeviceType.OneTouch => Assets.gangSwitch,
|
||||
DeviceType.TowTouch => Assets.gangSwitch,
|
||||
DeviceType.ThreeTouch => Assets.gangSwitch,
|
||||
DeviceType.NCPS => Assets.sensors,
|
||||
DeviceType.PC => Assets.powerClamp,
|
||||
DeviceType.Other => Assets.blackLogo,
|
||||
null => Assets.blackLogo,
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -42,9 +42,8 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
Expanded(child: SpaceIconPicker(iconPath: space.icon)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
SpaceNameTextField(
|
||||
initialValue: space.spaceName,
|
||||
@ -52,7 +51,7 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
(subspace) => subspace.name == value,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 32),
|
||||
SpaceSubSpacesBox(
|
||||
subspaces: space.subspaces,
|
||||
),
|
||||
|
@ -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,10 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.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/domain/params/load_tags_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_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 RemoteTagsService implements TagsService {
|
||||
const RemoteTagsService(this._httpService);
|
||||
@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService {
|
||||
static const _defaultErrorMessage = 'Failed to load tags';
|
||||
|
||||
@override
|
||||
Future<List<Tag>> loadTags(LoadTagsParam param) async {
|
||||
if (param.projectUuid == null) {
|
||||
throw Exception('Project UUID is required');
|
||||
}
|
||||
|
||||
Future<List<Tag>> loadTags() async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.listTags.replaceAll(
|
||||
'{projectUuid}',
|
||||
param.projectUuid!,
|
||||
),
|
||||
path: await _makeUrl(),
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
final data = result['data'] as List<dynamic>;
|
||||
@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService {
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _makeUrl() async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null || projectUuid.isEmpty) {
|
||||
throw APIException('Project UUID is required');
|
||||
}
|
||||
return '/projects/$projectUuid/tags';
|
||||
}
|
||||
}
|
||||
|
@ -3,34 +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.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];
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
|
||||
|
||||
abstract interface class TagsService {
|
||||
Future<List<Tag>> loadTags(LoadTagsParam param);
|
||||
Future<List<Tag>> loadTags();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/domain/params/load_tags_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -21,7 +20,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
|
||||
) async {
|
||||
emit(TagsLoading());
|
||||
try {
|
||||
final tags = await _tagsService.loadTags(event.param);
|
||||
final tags = await _tagsService.loadTags();
|
||||
emit(TagsLoaded(tags));
|
||||
} on APIException catch (e) {
|
||||
emit(TagsFailure(e.message));
|
||||
|
@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadTags extends TagsEvent {
|
||||
final LoadTagsParam param;
|
||||
|
||||
const LoadTags(this.param);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [param];
|
||||
const LoadTags();
|
||||
}
|
||||
|
@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.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/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.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 AddDeviceTypeWidget extends StatefulWidget {
|
||||
const AddDeviceTypeWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
|
||||
}
|
||||
|
||||
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||
final Map<Product, int> _selectedProducts = {};
|
||||
|
||||
void _onIncrement(Product product) {
|
||||
setState(() {
|
||||
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDecrement(Product product) {
|
||||
setState(() {
|
||||
if ((_selectedProducts[product] ?? 0) > 0) {
|
||||
_selectedProducts[product] = _selectedProducts[product]! - 1;
|
||||
if (_selectedProducts[product] == 0) {
|
||||
_selectedProducts.remove(product);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
|
||||
..add(const LoadProducts()),
|
||||
child: Builder(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const SelectableText('Add Devices'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: BlocBuilder<ProductsBloc, ProductsState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
ProductsInitial() || ProductsLoading() => _buildLoading(context),
|
||||
ProductsLoaded(:final products) => ProductsGrid(
|
||||
products: products,
|
||||
selectedProducts: _selectedProducts,
|
||||
onIncrement: _onIncrement,
|
||||
onDecrement: _onDecrement,
|
||||
),
|
||||
ProductsFailure(:final errorMessage) => _buildFailure(
|
||||
context,
|
||||
errorMessage,
|
||||
),
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: () {
|
||||
final result = _selectedProducts.entries
|
||||
.expand((entry) => List.generate(entry.value, (_) => entry.key))
|
||||
.toList();
|
||||
Navigator.of(context).pop(result);
|
||||
},
|
||||
onCancel: Navigator.of(context).pop,
|
||||
saveButtonLabel: 'Next',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading(BuildContext context) => SizedBox(
|
||||
width: context.screenWidth * 0.9,
|
||||
height: context.screenHeight * 0.65,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
|
||||
Widget _buildFailure(BuildContext context, String errorMessage) {
|
||||
return SizedBox(
|
||||
width: context.screenWidth * 0.9,
|
||||
height: context.screenHeight * 0.65,
|
||||
child: Center(
|
||||
child: SelectableText(
|
||||
errorMessage,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
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/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';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AssignTagsDialog extends StatefulWidget {
|
||||
const AssignTagsDialog({required this.space, super.key});
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
|
||||
@override
|
||||
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
|
||||
}
|
||||
|
||||
class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||
late SpaceDetailsModel _space;
|
||||
final Map<String, String> _validationErrors = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_space = widget.space.copyWith(
|
||||
productAllocations:
|
||||
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
|
||||
subspaces: widget.space.subspaces
|
||||
.map(
|
||||
(s) => s.copyWith(
|
||||
productAllocations:
|
||||
s.productAllocations.map((e) => e.copyWith()).toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
void _validateAllTags() {
|
||||
final newErrors = <String, String>{};
|
||||
final allAllocations = [
|
||||
..._space.productAllocations,
|
||||
..._space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
final allocationsByProductType = <String, List<ProductAllocation>>{};
|
||||
for (final allocation in allAllocations) {
|
||||
(allocationsByProductType[allocation.product.productType] ??= [])
|
||||
.add(allocation);
|
||||
}
|
||||
|
||||
for (final productType in allocationsByProductType.keys) {
|
||||
final allocations = allocationsByProductType[productType]!;
|
||||
final tagCounts = <String, int>{};
|
||||
|
||||
for (final allocation in allocations) {
|
||||
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||
if (tagName.isEmpty) {
|
||||
newErrors[allocation.uuid] =
|
||||
'Tag for ${allocation.product.name} cannot be empty.';
|
||||
} else {
|
||||
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (final allocation in allocations) {
|
||||
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
|
||||
newErrors[allocation.uuid] =
|
||||
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_validationErrors
|
||||
..clear()
|
||||
..addAll(newErrors);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTagChange(String allocationUuid, Tag newTag) {
|
||||
setState(() {
|
||||
var index =
|
||||
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
final allocation = _space.productAllocations[index];
|
||||
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||
} else {
|
||||
for (final subspace in _space.subspaces) {
|
||||
index = subspace.productAllocations
|
||||
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
final allocation = subspace.productAllocations[index];
|
||||
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
|
||||
setState(() {
|
||||
ProductAllocation? allocationToMove;
|
||||
|
||||
var index =
|
||||
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
allocationToMove = _space.productAllocations.removeAt(index);
|
||||
} else {
|
||||
for (final subspace in _space.subspaces) {
|
||||
index = subspace.productAllocations
|
||||
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
allocationToMove = subspace.productAllocations.removeAt(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allocationToMove == null) return;
|
||||
|
||||
if (newSubspaceUuid == null) {
|
||||
_space.productAllocations.add(allocationToMove);
|
||||
} else {
|
||||
_space.subspaces
|
||||
.firstWhere((s) => s.uuid == newSubspaceUuid)
|
||||
.productAllocations
|
||||
.add(allocationToMove);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleProductDelete(String allocationUuid) {
|
||||
setState(() {
|
||||
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
|
||||
|
||||
for (final subspace in _space.subspaces) {
|
||||
subspace.productAllocations.removeWhere(
|
||||
(pa) => pa.uuid == allocationUuid,
|
||||
);
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allProductAllocations = [
|
||||
..._space.productAllocations,
|
||||
..._space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
final productLocations = <String, String?>{};
|
||||
for (final pa in _space.productAllocations) {
|
||||
productLocations[pa.uuid] = null;
|
||||
}
|
||||
for (final subspace in _space.subspaces) {
|
||||
for (final pa in subspace.productAllocations) {
|
||||
productLocations[pa.uuid] = subspace.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
final hasErrors = _validationErrors.isNotEmpty;
|
||||
|
||||
return AlertDialog(
|
||||
title: const SelectableText('Assign Tags'),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: context.screenWidth * 0.6,
|
||||
minWidth: context.screenWidth * 0.6,
|
||||
maxHeight: context.screenHeight * 0.8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: AssignTagsTable(
|
||||
productAllocations: allProductAllocations,
|
||||
subspaces: _space.subspaces,
|
||||
productLocations: productLocations,
|
||||
onTagSelected: _handleTagChange,
|
||||
onLocationSelected: _handleLocationChange,
|
||||
onProductDeleted: _handleProductDelete,
|
||||
),
|
||||
),
|
||||
if (hasErrors)
|
||||
AssignTagsErrorMessages(
|
||||
errorMessages: _validationErrors.values.toSet().toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
|
||||
onCancel: () async {
|
||||
final newProducts = await showDialog<List<Product>>(
|
||||
context: context,
|
||||
builder: (context) => const AddDeviceTypeWidget(),
|
||||
);
|
||||
|
||||
if (newProducts == null || newProducts.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
for (final product in newProducts) {
|
||||
_space.productAllocations.add(
|
||||
ProductAllocation(
|
||||
uuid: '${const Uuid().v4()}-NewProductUuid',
|
||||
product: product,
|
||||
tag: Tag(
|
||||
uuid: '${const Uuid().v4()}-NewTag',
|
||||
name: '',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
},
|
||||
cancelButtonLabel: 'Add New Device',
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AssignTagsErrorMessages extends StatelessWidget {
|
||||
const AssignTagsErrorMessages({super.key, required this.errorMessages});
|
||||
|
||||
final List<String> errorMessages;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: errorMessages
|
||||
.map(
|
||||
(error) => Text(
|
||||
'- $error',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
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/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';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.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 AssignTagsTable extends StatelessWidget {
|
||||
const AssignTagsTable({
|
||||
required this.productAllocations,
|
||||
required this.subspaces,
|
||||
required this.productLocations,
|
||||
required this.onTagSelected,
|
||||
required this.onLocationSelected,
|
||||
required this.onProductDeleted,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<ProductAllocation> productAllocations;
|
||||
final List<Subspace> subspaces;
|
||||
final Map<String, String?> productLocations;
|
||||
final void Function(String, Tag) onTagSelected;
|
||||
final void Function(String, String?) onLocationSelected;
|
||||
final void Function(String) onProductDeleted;
|
||||
|
||||
DataColumn _buildDataColumn(BuildContext context, String label) {
|
||||
return DataColumn(
|
||||
label: SelectableText(label, style: context.textTheme.bodyMedium),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<TagsBloc>(
|
||||
create: (BuildContext context) => TagsBloc(
|
||||
RemoteTagsService(HTTPService()),
|
||||
)..add(const LoadTags()),
|
||||
child: BlocBuilder<TagsBloc, TagsState>(
|
||||
builder: (context, state) {
|
||||
return switch (state) {
|
||||
TagsLoading() || TagsInitial() => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
TagsFailure(:final message) => Center(
|
||||
child: Text(message),
|
||||
),
|
||||
TagsLoaded(:final tags) => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey,
|
||||
),
|
||||
key: ValueKey(productAllocations.length),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
_buildDataColumn(context, '#'),
|
||||
_buildDataColumn(context, 'Device'),
|
||||
_buildDataColumn(context, 'Tag'),
|
||||
_buildDataColumn(context, 'Location'),
|
||||
],
|
||||
rows: productAllocations.isEmpty
|
||||
? [
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: SelectableText(
|
||||
'No Devices Available',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell.empty,
|
||||
DataCell.empty,
|
||||
DataCell.empty,
|
||||
],
|
||||
),
|
||||
]
|
||||
: List.generate(productAllocations.length, (index) {
|
||||
final productAllocation = productAllocations[index];
|
||||
final allocationUuid = productAllocation.uuid;
|
||||
|
||||
final availableTags = tags
|
||||
.where(
|
||||
(tag) =>
|
||||
!productAllocations
|
||||
.where((p) =>
|
||||
p.product.productType ==
|
||||
productAllocation.product.productType)
|
||||
.map((p) => p.tag.name.toLowerCase())
|
||||
.contains(tag.name.toLowerCase()) ||
|
||||
tag.uuid == productAllocation.tag.uuid,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final currentLocationUuid =
|
||||
productLocations[allocationUuid];
|
||||
final currentLocationName = currentLocationUuid == null
|
||||
? 'Main Space'
|
||||
: subspaces
|
||||
.firstWhere((s) => s.uuid == currentLocationUuid)
|
||||
.name;
|
||||
|
||||
return DataRow(
|
||||
key: ValueKey(allocationUuid),
|
||||
cells: [
|
||||
DataCell(Text((index + 1).toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
productAllocation.product.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
onProductDeleted(allocationUuid);
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
width: double.infinity,
|
||||
child: ProductTagField(
|
||||
key: ValueKey('dropdown_$allocationUuid'),
|
||||
productName: productAllocation.product.uuid,
|
||||
initialValue: productAllocation.tag,
|
||||
onSelected: (newTag) {
|
||||
onTagSelected(allocationUuid, newTag);
|
||||
},
|
||||
items: availableTags,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DialogDropdown(
|
||||
items: [
|
||||
'Main Space',
|
||||
...subspaces.map((s) => s.name)
|
||||
],
|
||||
selectedValue: currentLocationName,
|
||||
onSelected: (newLocationName) {
|
||||
final newSubspaceUuid = newLocationName ==
|
||||
'Main Space'
|
||||
? null
|
||||
: subspaces
|
||||
.firstWhere(
|
||||
(s) => s.name == newLocationName)
|
||||
.uuid;
|
||||
onLocationSelected(
|
||||
allocationUuid, newSubspaceUuid);
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
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;
|
||||
final ValueChanged<Tag> onSelected;
|
||||
final Tag? initialValue;
|
||||
final String productName;
|
||||
|
||||
const ProductTagField({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.initialValue,
|
||||
required this.productName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProductTagField> createState() => _ProductTagFieldState();
|
||||
}
|
||||
|
||||
class _ProductTagFieldState extends State<ProductTagField> {
|
||||
bool _isOpen = false;
|
||||
OverlayEntry? _overlayEntry;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.text = widget.initialValue?.name ?? '';
|
||||
_focusNode.addListener(_handleFocusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_handleFocusChange);
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleFocusChange() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_submit(_controller.text);
|
||||
}
|
||||
}
|
||||
|
||||
void _submit(String value) {
|
||||
final lowerCaseValue = value.toLowerCase();
|
||||
final selectedTag = widget.items.firstWhere(
|
||||
(e) => e.name.toLowerCase() == lowerCaseValue,
|
||||
orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
|
||||
);
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
}
|
||||
|
||||
void _toggleDropdown() {
|
||||
if (_isOpen) {
|
||||
_closeDropdown();
|
||||
} else {
|
||||
_openDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
void _openDropdown() {
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
setState(() => _isOpen = true);
|
||||
}
|
||||
|
||||
void _closeDropdown() {
|
||||
if (_isOpen) {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
setState(() => _isOpen = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onFieldSubmitted: _submit,
|
||||
style: context.textTheme.bodyMedium,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter or Select a tag',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _toggleDropdown,
|
||||
child: const Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final renderBox = context.findRenderObject()! as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
return OverlayEntry(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: _closeDropdown,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy + size.height,
|
||||
width: size.width,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
constraints: const BoxConstraints(maxHeight: 200.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tag = widget.items[index];
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
tag.name,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_controller.text = tag.name;
|
||||
_submit(tag.name);
|
||||
_closeDropdown();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
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/tags/presentation/widgets/product_type_card_counter.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductTypeCard extends StatelessWidget {
|
||||
const ProductTypeCard({
|
||||
required this.product,
|
||||
required this.count,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Product product;
|
||||
final int count;
|
||||
final void Function() onIncrement;
|
||||
final void Function() onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(child: DeviceIconWidget(icon: product.icon)),
|
||||
_buildName(context, product.name),
|
||||
ProductTypeCardCounter(
|
||||
onIncrement: onIncrement,
|
||||
onDecrement: onDecrement,
|
||||
count: count,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context, String name) {
|
||||
return Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Text(
|
||||
name,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductTypeCardCounter extends StatelessWidget {
|
||||
const ProductTypeCardCounter({
|
||||
super.key,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
final int count;
|
||||
final void Function() onIncrement;
|
||||
final void Function() onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.counterBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
_buildCounterButton(
|
||||
Icons.remove,
|
||||
onDecrement,
|
||||
),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
_buildCounterButton(Icons.add, onIncrement),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterButton(
|
||||
IconData icon,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
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/tags/presentation/widgets/product_type_card.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductsGrid extends StatelessWidget {
|
||||
const ProductsGrid({
|
||||
required this.products,
|
||||
required this.selectedProducts,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Product> products;
|
||||
final Map<Product, int> selectedProducts;
|
||||
final void Function(Product) onIncrement;
|
||||
final void Function(Product) onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final crossAxisCount = switch (context.screenWidth) {
|
||||
> 1200 => 8,
|
||||
> 800 => 5,
|
||||
_ => 3,
|
||||
};
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.height * 0.65,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 6,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
return ProductTypeCard(
|
||||
product: product,
|
||||
count: selectedProducts[product] ?? 0,
|
||||
onIncrement: () => onIncrement(product),
|
||||
onDecrement: () => onDecrement(product),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
|
||||
static const _defaultErrorMessage = 'Failed to update space';
|
||||
|
||||
@override
|
||||
Future<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>?;
|
||||
@ -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,42 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
|
||||
class UpdateSpaceParam {
|
||||
UpdateSpaceParam({
|
||||
required this.space,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
final String communityUuid;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'spaceName': space.spaceName,
|
||||
'icon': space.icon,
|
||||
'subspaces': space.subspaces.map((e) => e._toJson()).toList(),
|
||||
'productAllocations':
|
||||
space.productAllocations.map((e) => e._toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension _ProductAllocationToJson on ProductAllocation {
|
||||
Map<String, dynamic> _toJson() {
|
||||
final isNewTag = tag.uuid.isEmpty;
|
||||
return <String, dynamic>{
|
||||
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
|
||||
'productUuid': product.uuid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension _SubspaceToJson on Subspace {
|
||||
Map<String, dynamic> _toJson() {
|
||||
final isNewSubspace = uuid.endsWith('-NewTag');
|
||||
return <String, dynamic>{
|
||||
if (!isNewSubspace) 'uuid': uuid,
|
||||
'subspaceName': name,
|
||||
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsMod
|
||||
on<UpdateSpaceDetailsSubspaces>(_onUpdateSpaceDetailsSubspaces);
|
||||
on<UpdateSpaceDetailsProductAllocations>(
|
||||
_onUpdateSpaceDetailsProductAllocations);
|
||||
on<UpdateSpaceDetails>(_onUpdateSpaceDetails);
|
||||
}
|
||||
|
||||
void _onUpdateSpaceDetailsIcon(
|
||||
@ -42,4 +43,11 @@ class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsMod
|
||||
) {
|
||||
emit(state.copyWith(productAllocations: event.productAllocations));
|
||||
}
|
||||
|
||||
void _onUpdateSpaceDetails(
|
||||
UpdateSpaceDetails event,
|
||||
Emitter<SpaceDetailsModel> emit,
|
||||
) {
|
||||
emit(event.space);
|
||||
}
|
||||
}
|
||||
|
@ -42,3 +42,12 @@ final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent
|
||||
@override
|
||||
List<Object> get props => [productAllocations];
|
||||
}
|
||||
|
||||
final class UpdateSpaceDetails extends SpaceDetailsModelEvent {
|
||||
const UpdateSpaceDetails(this.space);
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
|
||||
@override
|
||||
List<Object> get props => [space];
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/model/device_model.dart';
|
||||
import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
@ -69,7 +69,6 @@ abstract class ColorsManager {
|
||||
static const Color invitedOrange = Color(0xFFFFE193);
|
||||
static const Color invitedOrangeText = Color(0xFFFFBF00);
|
||||
static const Color lightGrayBorderColor = Color(0xB2D5D5D5);
|
||||
//background: #F8F8F8;
|
||||
static const Color vividBlue = Color(0xFF023DFE);
|
||||
static const Color semiTransparentRed = Color(0x99FF0000);
|
||||
static const Color grey700 = Color(0xFF2D3748);
|
||||
@ -84,4 +83,7 @@ abstract class ColorsManager {
|
||||
static const Color minBlue = Color(0xFF93AAFD);
|
||||
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);
|
||||
}
|
||||
|
@ -46,7 +46,8 @@ abstract class ApiEndpoints {
|
||||
// Community Module
|
||||
static const String createCommunity = '/projects/{projectId}/communities';
|
||||
static const String getCommunityList = '/projects/{projectId}/communities';
|
||||
static const String getCommunityListv2 = '/projects/{projectId}/communities/v2';
|
||||
static const String getCommunityListv2 =
|
||||
'/projects/{projectId}/communities/v2';
|
||||
static const String getCommunityById =
|
||||
'/projects/{projectId}/communities/{communityId}';
|
||||
static const String updateCommunity =
|
||||
@ -138,4 +139,7 @@ abstract class ApiEndpoints {
|
||||
static const String assignDeviceToRoom =
|
||||
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
|
||||
static const String saveSchedule = '/schedule/{deviceUuid}';
|
||||
|
||||
static const String getBookableSpaces = '/bookable-spaces';
|
||||
static const String getCalendarEvents = '/api';
|
||||
}
|
||||
|
@ -63,6 +63,9 @@ dependencies:
|
||||
bloc: ^9.0.0
|
||||
geocoding: ^4.0.0
|
||||
gauge_indicator: ^0.4.3
|
||||
calendar_view: ^1.4.0
|
||||
calendar_date_picker2: ^2.0.1
|
||||
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
Reference in New Issue
Block a user