diff --git a/assets/icons/empty_barred_chart.svg b/assets/icons/empty_barred_chart.svg new file mode 100644 index 00000000..723d5e14 --- /dev/null +++ b/assets/icons/empty_barred_chart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/empty_energy_management_chart.svg b/assets/icons/empty_energy_management_chart.svg new file mode 100644 index 00000000..042b9990 --- /dev/null +++ b/assets/icons/empty_energy_management_chart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/empty_energy_management_per_device.svg b/assets/icons/empty_energy_management_per_device.svg new file mode 100644 index 00000000..0408cd3a --- /dev/null +++ b/assets/icons/empty_energy_management_per_device.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/empty_heatmap.svg b/assets/icons/empty_heatmap.svg new file mode 100644 index 00000000..bbb2cfed --- /dev/null +++ b/assets/icons/empty_heatmap.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/empty_range_of_aqi.svg b/assets/icons/empty_range_of_aqi.svg new file mode 100644 index 00000000..aa51a41c --- /dev/null +++ b/assets/icons/empty_range_of_aqi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/group_icon.svg b/assets/icons/group_icon.svg new file mode 100644 index 00000000..efca14dd --- /dev/null +++ b/assets/icons/group_icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/home_icon.svg b/assets/icons/home_icon.svg new file mode 100644 index 00000000..35080c4e --- /dev/null +++ b/assets/icons/home_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/completed_done.svg b/assets/images/completed_done.svg new file mode 100644 index 00000000..759f0cba --- /dev/null +++ b/assets/images/completed_done.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart index dd82d739..11c42f3b 100644 --- a/lib/pages/access_management/bloc/access_bloc.dart +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -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'; diff --git a/lib/pages/access_management/bloc/access_state.dart b/lib/pages/access_management/bloc/access_state.dart index 0790a735..cdaf9aad 100644 --- a/lib/pages/access_management/bloc/access_state.dart +++ b/lib/pages/access_management/bloc/access_state.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(); diff --git a/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart new file mode 100644 index 00000000..3c2610db --- /dev/null +++ b/lib/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart @@ -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 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, + ); + }, + ); + return response; + } on DioException catch (e) { + final responseData = e.response?.data; + if (responseData is Map) { + final errorMessage = responseData['error']?['message'] as String? ?? + responseData['message'] as String? ?? + _defaultErrorMessage; + throw APIException(errorMessage); + } + throw APIException(_defaultErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: ${e.toString()}'); + } + } +} diff --git a/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart new file mode 100644 index 00000000..f2b2e5fe --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart @@ -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 get props => [page, size, search, active, configured]; +} diff --git a/lib/pages/access_management/booking_system/domain/models/bookable_room.dart b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart new file mode 100644 index 00000000..c7dbd0aa --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/bookable_room.dart @@ -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 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), + ); + } +} + +class BookableConfig { + final String uuid; + final List 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 json) { + return BookableConfig( + uuid: json['uuid'] as String, + daysAvailable: (json['daysAvailable'] as List).cast(), + startTime: json['startTime'] as String, + endTime: json['endTime'] as String, + active: json['active'] as bool, + points: json['points'] as int, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart new file mode 100644 index 00000000..b4b79bc2 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart @@ -0,0 +1,40 @@ + + +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class PaginatedBookableSpaces { + final List 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 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, + ); + } +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart b/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart new file mode 100644 index 00000000..c3b0bfb7 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/bookable_system_service.dart @@ -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 getBookableSpaces({ + required LoadBookableSpacesParam param, + }); +} diff --git a/lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart b/lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart new file mode 100644 index 00000000..bea3c103 --- /dev/null +++ b/lib/pages/access_management/booking_system/domain/services/debounced_bookable_spaces_service.dart @@ -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? _lastCompleter; + + DebouncedBookableSpacesService( + this._inner, { + this.debounceDuration = const Duration(milliseconds: 500), + }); + + @override + Future getBookableSpaces({ + required LoadBookableSpacesParam param, + }) { + _debounceTimer?.cancel(); + if (_lastCompleter != null && !_lastCompleter!.isCompleted) { + _lastCompleter!.completeError(StateError("Cancelled by new search")); + } + + final completer = Completer(); + _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; + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart new file mode 100644 index 00000000..431720af --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:flutter/material.dart'; +part 'events_event.dart'; +part 'events_state.dart'; + +class CalendarEventsBloc extends Bloc { + final EventController eventController = EventController(); + + CalendarEventsBloc() : super(EventsInitial()) { + on(_onLoadEvents); + on(_onAddEvent); + on(_onStartTimer); + on(_onDisposeResources); + on(_onGoToWeek); + } + + Future _onLoadEvents( + LoadEvents event, + Emitter emit, + ) async { + emit(EventsLoading()); + try { + final events = _generateDummyEventsForWeek(event.weekStart); + eventController.addAll(events); + emit(EventsLoaded( + events: events, + initialDate: event.weekStart, + weekDays: _getWeekDays(event.weekStart), + )); + } catch (e) { + emit(EventsError('Failed to load events')); + } + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final events = []; + + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + + return events; + } + + void _onAddEvent(AddEvent event, Emitter emit) { + eventController.add(event.event); + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + emit(EventsLoaded( + events: [...eventController.events], + initialDate: loaded.initialDate, + weekDays: loaded.weekDays, + )); + } + } + + void _onStartTimer(StartTimer event, Emitter emit) {} + + void _onDisposeResources( + DisposeResources event, Emitter emit) { + eventController.dispose(); + } + + void _onGoToWeek(GoToWeek event, Emitter emit) { + if (state is EventsLoaded) { + final loaded = state as EventsLoaded; + final newWeekDays = _getWeekDays(event.weekDate); + emit(EventsLoaded( + events: loaded.events, + initialDate: event.weekDate, + weekDays: newWeekDays, + )); + } + } + + List _generateDummyEvents() { + final now = DateTime.now(); + return [ + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 8, minute: 00, second: 0), + endTime: now.copyWith(hour: 9, minute: 00, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now, + startTime: now.copyWith(hour: 9, minute: 00, second: 0), + endTime: now.copyWith(hour: 10, minute: 30, second: 0), + title: 'Team Meeting', + description: 'Weekly team sync', + color: Colors.blue, + ), + CalendarEventData( + date: now.add(const Duration(days: 1)), + startTime: now.copyWith(hour: 14, day: now.day + 1), + endTime: now.copyWith(hour: 15, day: now.day + 1), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + ), + CalendarEventData( + date: now.add(const Duration(days: 2)), + startTime: now.copyWith(hour: 11, day: now.day + 2), + endTime: now.copyWith(hour: 12, day: now.day + 2), + title: 'Lunch with Team', + color: Colors.orange, + ), + ]; + } + + List _getWeekDays(DateTime date) { + final int weekday = date.weekday; + final DateTime monday = date.subtract(Duration(days: weekday - 1)); + return List.generate(7, (i) => monday.add(Duration(days: i))); + } + + @override + Future close() { + eventController.dispose(); + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart new file mode 100644 index 00000000..e23e65de --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_event.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventsEvent { + const CalendarEventsEvent(); +} + +class LoadEvents extends CalendarEventsEvent { + final DateTime weekStart; + const LoadEvents({required this.weekStart}); +} + +class AddEvent extends CalendarEventsEvent { + final CalendarEventData event; + AddEvent(this.event); +} + +class StartTimer extends CalendarEventsEvent {} + +class DisposeResources extends CalendarEventsEvent {} + +class GoToWeek extends CalendarEventsEvent { + final DateTime weekDate; + GoToWeek(this.weekDate); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart new file mode 100644 index 00000000..b7263ec8 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/calendar/events_state.dart @@ -0,0 +1,25 @@ +part of 'events_bloc.dart'; + +@immutable +abstract class CalendarEventState {} + +class EventsInitial extends CalendarEventState {} + +class EventsLoading extends CalendarEventState {} + +class EventsLoaded extends CalendarEventState { + final List events; + final DateTime initialDate; + final List weekDays; + + EventsLoaded({ + required this.events, + required this.initialDate, + required this.weekDays, + }); +} + +class EventsError extends CalendarEventState { + final String message; + EventsError(this.message); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart new file mode 100644 index 00000000..8592433f --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart @@ -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 { + DateSelectionBloc() : super(DateSelectionState.initial()) { + on((event, emit) { + final newWeekStart = _getStartOfWeek(event.selectedDate); + emit(state.copyWith( + selectedDate: event.selectedDate, + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + final newWeekStart = state.weekStart.add(const Duration(days: 7)); + emit(state.copyWith( + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + final newWeekStart = state.weekStart.subtract(const Duration(days: 7)); + emit(state.copyWith( + weekStart: newWeekStart, + )); + }); + + on((event, emit) { + emit(state.copyWith( + selectedDateFromSideBarCalender: event.selectedDate, + )); + }); + } + + static DateTime _getStartOfWeek(DateTime date) { + return date.subtract(Duration(days: date.weekday - 1)); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart new file mode 100644 index 00000000..058c0db5 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart @@ -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); +} \ No newline at end of file diff --git a/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart new file mode 100644 index 00000000..8c839c72 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart @@ -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, + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart new file mode 100644 index 00000000..70b46c1a --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart @@ -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 { + SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) { + on((event, emit) { + emit(SelectedBookableSpaceState( + selectedBookableSpace: event.bookableSpace)); + }); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart new file mode 100644 index 00000000..c74c13df --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_event.dart @@ -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); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart new file mode 100644 index 00000000..8509d5c3 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_state.dart @@ -0,0 +1,9 @@ +part of 'selected_bookable_space_bloc.dart'; + +class SelectedBookableSpaceState { + final BookableSpaceModel? selectedBookableSpace; + + const SelectedBookableSpaceState( + { this.selectedBookableSpace,} + ); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart new file mode 100644 index 00000000..1b6f41fa --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart @@ -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 { + final BookableSystemService _bookingService; + int _currentPage = 1; + final int _pageSize = 20; + String _currentSearch = ''; + + SidebarBloc(this._bookingService) + : super(SidebarState( + allRooms: [], + displayedRooms: [], + isLoading: true, + hasMore: true, + )) { + on(_onLoadBookableSpaces); + on(_onLoadMoreSpaces); + on(_onSelectRoom); + on(_onSearchRooms); + on(_onResetSearch); + } + + Future _onLoadBookableSpaces( + LoadBookableSpaces event, + Emitter 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 _onLoadMoreSpaces( + LoadMoreSpaces event, + Emitter 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 _onSearchRooms( + SearchRoomsEvent event, + Emitter 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 emit, + ) { + _currentSearch = ''; + add(LoadBookableSpaces()); + } + + void _onSelectRoom( + SelectRoomEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedRoomId: event.roomId)); + } + + @override + Future close() { + return super.close(); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart new file mode 100644 index 00000000..770e7b7e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart @@ -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); +} diff --git a/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart new file mode 100644 index 00000000..7b35474e --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart'; + +class SidebarState { + final List allRooms; + final List 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? allRooms, + List? 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, + ); + } +} diff --git a/lib/pages/access_management/model/password_model.dart b/lib/pages/access_management/booking_system/presentation/model/password_model.dart similarity index 100% rename from lib/pages/access_management/model/password_model.dart rename to lib/pages/access_management/booking_system/presentation/model/password_model.dart diff --git a/lib/pages/access_management/booking_system/presentation/view/booking_page.dart b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart new file mode 100644 index 00000000..357cac41 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/booking_page.dart @@ -0,0 +1,266 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:syncrow_web/pages/access_management/booking_system/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/weekly_calendar_page.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 createState() => _BookingPageState(); +} + +class _BookingPageState extends State { + late final EventController _eventController; + + @override + void initState() { + super.initState(); + _eventController = EventController(); + } + + @override + void dispose() { + _eventController.dispose(); + super.dispose(); + } + + List _generateDummyEventsForWeek(DateTime weekStart) { + final List events = []; + for (int i = 0; i < 7; i++) { + final date = weekStart.add(Duration(days: i)); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 9, minute: 0), + endTime: date.copyWith(hour: 10, minute: 30), + title: 'Team Meeting', + description: 'Daily standup', + color: Colors.blue, + )); + events.add(CalendarEventData( + date: date, + startTime: date.copyWith(hour: 14, minute: 0), + endTime: date.copyWith(hour: 15, minute: 0), + title: 'Client Call', + description: 'Project discussion', + color: Colors.green, + )); + } + return events; + } + + void _loadEventsForWeek(DateTime weekStart) { + _eventController.removeWhere((_) => true); + _eventController.addAll(_generateDummyEventsForWeek(weekStart)); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => SelectedBookableSpaceBloc()), + BlocProvider(create: (_) => DateSelectionBloc()), + ], + child: BlocListener( + listenWhen: (previous, current) => + previous.weekStart != current.weekStart, + listener: (context, state) { + _loadEventsForWeek(state.weekStart); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.1), + offset: const Offset(3, 0), + blurRadius: 6, + spreadRadius: 0, + ), + ], + ), + child: Column( + children: [ + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return BookingSidebar( + onRoomSelected: (selectedRoom) { + context + .read() + .add(SelectBookableSpace(selectedRoom)); + }, + ); + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, dateState) { + return CustomCalendarPage( + selectedDate: dateState.selectedDate, + onDateChanged: (day, month, year) { + final newDate = DateTime(year, month, day); + context + .read() + .add(SelectDate(newDate)); + context + .read() + .add(SelectDateFromSidebarCalendar(newDate)); + }, + ); + }, + ), + ), + ], + ), + ), + ), + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgTextButton( + svgAsset: Assets.homeIcon, + label: 'Manage Bookable Spaces', + onPressed: () {}, + ), + const SizedBox(width: 20), + SvgTextButton( + svgAsset: Assets.groupIcon, + label: 'Manage Users', + onPressed: () {}, + ), + ], + ), + BlocBuilder( + builder: (context, state) { + final weekStart = state.weekStart; + final weekEnd = + weekStart.add(const Duration(days: 6)); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: ColorsManager.circleRolesBackground, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_back_ios, + color: ColorsManager.lightGrayColor), + onPressed: () { + context + .read() + .add(PreviousWeek()); + }, + ), + const SizedBox(width: 10), + Text( + _getMonthYearText(weekStart, weekEnd), + style: const TextStyle( + color: ColorsManager.lightGrayColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(width: 10), + IconButton( + iconSize: 15, + icon: const Icon(Icons.arrow_forward_ios, + color: ColorsManager.lightGrayColor), + onPressed: () { + context + .read() + .add(NextWeek()); + }, + ), + ], + ), + ); + }, + ), + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, roomState) { + final selectedRoom = roomState.selectedBookableSpace; + return BlocBuilder( + builder: (context, dateState) { + return WeeklyCalendarPage( + startTime: + selectedRoom?.bookableConfig.startTime, + endTime: selectedRoom?.bookableConfig.endTime, + weekStart: dateState.weekStart, + selectedDate: dateState.selectedDate, + eventController: _eventController, + selectedDateFromSideBarCalender: context + .watch() + .state + .selectedDateFromSideBarCalender, + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + String _getMonthYearText(DateTime start, DateTime end) { + final startMonth = DateFormat('MMM').format(start); + final endMonth = DateFormat('MMM').format(end); + final year = start.year == end.year + ? start.year.toString() + : '${start.year}-${end.year}'; + + if (start.month == end.month) { + return '$startMonth $year'; + } else { + return '$startMonth - $endMonth $year'; + } + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart new file mode 100644 index 00000000..e3d84924 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart @@ -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().add(LoadMoreSpaces()); + } + } + + void _handleSearch(String value) { + context.read().add(SearchRoomsEvent(value)); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + 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().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() + .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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart new file mode 100644 index 00000000..eb758311 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart @@ -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 createState() => _CustomCalendarPageState(); +} + +class _CustomCalendarPageState extends State { + 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); + } + }, + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart new file mode 100644 index 00000000..c7c660c1 --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SvgTextButton extends StatelessWidget { + final String svgAsset; + final String label; + final VoidCallback onPressed; + final Color backgroundColor; + final Color svgColor; + final Color labelColor; + final double borderRadius; + final List boxShadow; + final double svgSize; + + const SvgTextButton({ + super.key, + required this.svgAsset, + required this.label, + required this.onPressed, + this.backgroundColor = ColorsManager.circleRolesBackground, + this.svgColor = const Color(0xFF496EFF), + this.labelColor = Colors.black, + this.borderRadius = 10.0, + this.boxShadow = const [ + BoxShadow( + color: ColorsManager.lightGrayColor, + blurRadius: 4, + offset: Offset(0, 1), + ), + ], + this.svgSize = 24.0, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onPressed, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + boxShadow: boxShadow, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + svgAsset, + width: svgSize, + height: svgSize, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + color: labelColor, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart new file mode 100644 index 00000000..4a4b608d --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart @@ -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, + ), + ), + ); + } +} diff --git a/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart new file mode 100644 index 00000000..5c38e2fc --- /dev/null +++ b/lib/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:calendar_view/calendar_view.dart'; +import 'package:intl/intl.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; + + return LayoutBuilder( + builder: (context, constraints) { + final double calendarWidth = constraints.maxWidth; + final double dayColumnWidth = + (calendarWidth - timeLineWidth) / totalDays - 0.1; + + return Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), + child: Stack( + children: [ + WeekView( + pageViewPhysics: const NeverScrollableScrollPhysics(), + key: ValueKey(weekStart), + controller: eventController, + initialDay: weekStart, + startHour: startHour - 1, + endHour: endHour, + heightPerMinute: 1.1, + showLiveTimeLineInAllDays: false, + showVerticalLines: true, + emulateVerticalOffsetBy: -80, + startDay: WeekDays.monday, + liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( + showBullet: false, + height: 0, + ), + weekDayBuilder: (date) { + final index = weekDays.indexWhere((d) => isSameDay(d, date)); + final isSelectedDay = index == selectedDayIndex; + return Column( + children: [ + Text( + DateFormat('EEE').format(date).toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: isSelectedDay ? Colors.blue : Colors.black, + ), + ), + Text( + DateFormat('d').format(date), + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20, + color: isSelectedDay + ? ColorsManager.blue1 + : ColorsManager.blackColor, + ), + ), + ], + ); + }, + timeLineBuilder: (date) { + int hour = date.hour == 0 + ? 12 + : (date.hour > 12 ? date.hour - 12 : date.hour); + String period = date.hour >= 12 ? 'PM' : 'AM'; + return Container( + height: 60, + alignment: Alignment.center, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$hour', + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24, + color: ColorsManager.blackColor, + ), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 2, top: 6), + child: Text( + period, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: ColorsManager.blackColor, + letterSpacing: 1, + ), + ), + ), + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + ), + ], + ), + ), + ); + }, + 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 Container( + margin: + const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: events.map((event) { + final bool isEventEnded = event.endTime != null && + event.endTime!.isBefore(DateTime.now()); + return Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isEventEnded + ? ColorsManager.lightGrayBorderColor + : ColorsManager.blue1.withOpacity(0.25), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat('h:mm a').format(event.startTime!), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + event.title, + style: const TextStyle( + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + }, + ), + if (selectedDayIndex >= 0) + Positioned( + left: (timeLineWidth + 3) + + (dayColumnWidth - 8) * (selectedDayIndex - 0.01), + top: 0, + bottom: 0, + width: dayColumnWidth, + child: IgnorePointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 0, horizontal: 4), + color: ColorsManager.spaceColor.withOpacity(0.07), + ), + ), + ), + if (selectedSidebarIndex >= 0 && + selectedSidebarIndex != selectedDayIndex) + Positioned( + left: (timeLineWidth + 3) + + (dayColumnWidth - 8) * (selectedSidebarIndex - 0.01), + top: 0, + bottom: 0, + width: dayColumnWidth, + child: IgnorePointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 0, horizontal: 4), + color: Colors.orange.withOpacity(0.14), + ), + ), + ), + Positioned( + right: 0, + top: 50, + bottom: 0, + child: IgnorePointer( + child: Container( + width: 1, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + ), + ], + ), + ); + }, + ); + } + + List _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; + } +} diff --git a/lib/pages/access_management/view/access_management.dart b/lib/pages/access_management/view/access_management.dart index d7c7a9dd..4e31f23f 100644 --- a/lib/pages/access_management/view/access_management.dart +++ b/lib/pages/access_management/view/access_management.dart @@ -2,302 +2,86 @@ 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/bloc/access_state.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; -import 'package:syncrow_web/pages/common/custom_table.dart'; -import 'package:syncrow_web/pages/common/date_time_widget.dart'; -import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; -import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.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/pages/visitor_password/view/visitor_password_dialog.dart'; -// import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/app_enum.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; -class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout { +class AccessManagementPage extends StatefulWidget { const AccessManagementPage({super.key}); @override - Widget build(BuildContext context) { - final isLargeScreen = isLargeScreenSize(context); - final isSmallScreen = isSmallScreenSize(context); - final isHalfMediumScreen = isHafMediumScreenSize(context); - final padding = - isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15); + State createState() => _AccessManagementPageState(); +} - return WebScaffold( +class _AccessManagementPageState extends State + with HelperResponsiveLayout { + final PageController _pageController = PageController(initialPage: 0); + int _currentPageIndex = 0; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + child: WebScaffold( enableMenuSidebar: false, appBarTitle: Text( 'Access Management', style: ResponsiveTextTheme.of(context).deviceManagementTitle, ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocProvider( - create: (BuildContext context) => - AccessBloc()..add(FetchTableData()), - child: BlocConsumer( - listener: (context, state) {}, - builder: (context, state) { - final accessBloc = BlocProvider.of(context); - final filteredData = accessBloc.filteredData; - return state is AccessLoaded - ? const Center(child: CircularProgressIndicator()) - : Container( - padding: padding, - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FilterWidget( - size: MediaQuery.of(context).size, - tabs: accessBloc.tabs, - selectedIndex: accessBloc.selectedIndex, - onTabChanged: (index) { - accessBloc.add(TabChangedEvent(index)); - }, - ), - const SizedBox(height: 20), - if (isSmallScreen || isHalfMediumScreen) - _buildSmallSearchFilters(context, accessBloc) - else - _buildNormalSearchWidgets(context, accessBloc), - const SizedBox(height: 20), - _buildVisitorAdminPasswords(context, accessBloc), - const SizedBox(height: 20), - Expanded( - child: DynamicTable( - tableName: 'AccessManagement', - uuidIndex: 1, - withSelectAll: true, - isEmpty: filteredData.isEmpty, - withCheckBox: false, - size: MediaQuery.of(context).size, - cellDecoration: containerDecoration, - headers: const [ - 'Name', - 'Access Type', - 'Access Start', - 'Access End', - 'Accessible Device', - 'Authorizer', - 'Authorization Date & Time', - 'Access Status' - ], - data: filteredData.map((item) { - return [ - item.passwordName, - item.passwordType.value, - accessBloc - .timestampToDate(item.effectiveTime), - accessBloc - .timestampToDate(item.invalidTime), - item.deviceName.toString(), - item.authorizerEmail.toString(), - accessBloc - .timestampToDate(item.invalidTime), - item.passwordStatus.value, - ]; - }).toList(), - )), - ], - ), - ); - }))); - } - - Wrap _buildVisitorAdminPasswords( - BuildContext context, AccessBloc accessBloc) { - return Wrap( - spacing: 10, - runSpacing: 10, - children: [ - Container( - width: 205, - height: 42, - decoration: containerDecoration, - child: DefaultButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return const VisitorPasswordDialog(); - }, - ).then((v) { - if (v != null) { - accessBloc.add(FetchTableData()); - } - }); - }, - borderRadius: 8, + centerBody: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () => _switchPage(0), child: Text( - 'Create Visitor Password ', - style: context.textTheme.titleSmall! - .copyWith(color: Colors.white, fontSize: 12), - )), + 'Access Overview', + style: context.textTheme.titleMedium?.copyWith( + color: _currentPageIndex == 0 ? Colors.white : Colors.grey, + fontWeight: _currentPageIndex == 0 + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + TextButton( + onPressed: () => _switchPage(1), + child: Text( + 'Booking System', + style: context.textTheme.titleMedium?.copyWith( + color: _currentPageIndex == 1 ? Colors.white : Colors.grey, + fontWeight: _currentPageIndex == 1 + ? FontWeight.w700 + : FontWeight.w400, + ), + ), + ), + ], ), - // Container( - // width: 133, - // height: 42, - // decoration: containerDecoration, - // child: DefaultButton( - // borderRadius: 8, - // backgroundColor: ColorsManager.whiteColors, - // child: Text( - // 'Admin Password', - // style: context.textTheme.titleSmall! - // .copyWith(color: Colors.black, fontSize: 12), - // )), - // ), - ], + rightBody: const NavigateHomeGridView(), + scaffoldBody: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + AccessOverviewContent(), + BookingPage(), + ], + ), + ), ); } - Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) { - // TimeOfDay _selectedTime = TimeOfDay.now(); - - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - textBaseline: TextBaseline.ideographic, - children: [ - SizedBox( - width: 250, - child: CustomWebTextField( - controller: accessBloc.passwordName, - height: 43, - isRequired: false, - textFieldName: 'Name', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - ), - ), - const SizedBox(width: 15), - SizedBox( - width: 250, - child: CustomWebTextField( - controller: accessBloc.emailAuthorizer, - height: 43, - isRequired: false, - textFieldName: 'Authorizer', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - ), - ), - const SizedBox(width: 15), - SizedBox( - child: DateTimeWebWidget( - icon: Assets.calendarIcon, - isRequired: false, - title: 'Access Time', - size: MediaQuery.of(context).size, - endTime: () { - accessBloc.add(SelectTime(context: context, isStart: false)); - }, - startTime: () { - accessBloc.add(SelectTime(context: context, isStart: true)); - }, - firstString: BlocProvider.of(context).startTime, - secondString: BlocProvider.of(context).endTime, - ), - ), - const SizedBox(width: 15), - SearchResetButtons( - onSearch: () { - accessBloc.add(FilterDataEvent( - emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - onReset: () { - accessBloc.add(ResetSearch()); - }, - ), - ], - ); - } - - Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) { - return Wrap( - spacing: 20, - runSpacing: 10, - children: [ - SizedBox( - width: 300, - child: CustomWebTextField( - controller: accessBloc.passwordName, - isRequired: true, - height: 40, - textFieldName: 'Name', - description: '', - onSubmitted: (value) { - accessBloc.add(FilterDataEvent( - emailAuthorizer: - accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }), - ), - DateTimeWebWidget( - icon: Assets.calendarIcon, - isRequired: false, - title: 'Access Time', - size: MediaQuery.of(context).size, - endTime: () { - accessBloc.add(SelectTime(context: context, isStart: false)); - }, - startTime: () { - accessBloc.add(SelectTime(context: context, isStart: true)); - }, - firstString: BlocProvider.of(context).startTime, - secondString: BlocProvider.of(context).endTime, - ), - SearchResetButtons( - onSearch: () { - accessBloc.add(FilterDataEvent( - emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), - selectedTabIndex: - BlocProvider.of(context).selectedIndex, - passwordName: accessBloc.passwordName.text.toLowerCase(), - startTime: accessBloc.effectiveTimeTimeStamp, - endTime: accessBloc.expirationTimeTimeStamp)); - }, - onReset: () { - accessBloc.add(ResetSearch()); - }, - ), - ], - ); + void _switchPage(int index) { + setState(() => _currentPageIndex = index); + _pageController.jumpToPage(index); } } diff --git a/lib/pages/access_management/view/access_overview_content.dart b/lib/pages/access_management/view/access_overview_content.dart new file mode 100644 index 00000000..b6b8748a --- /dev/null +++ b/lib/pages/access_management/view/access_overview_content.dart @@ -0,0 +1,289 @@ +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart'; +import 'package:syncrow_web/pages/common/custom_table.dart'; +import 'package:syncrow_web/pages/common/date_time_widget.dart'; +import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/access_management/bloc/access_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/constants/assets.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/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AccessOverviewContent extends StatelessWidget + with HelperResponsiveLayout { + const AccessOverviewContent({super.key}); + + @override + Widget build(BuildContext context) { + final isLargeScreen = isLargeScreenSize(context); + final isSmallScreen = isSmallScreenSize(context); + final isHalfMediumScreen = isHafMediumScreenSize(context); + final padding = + isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15); + + return BlocProvider( + create: (BuildContext context) => AccessBloc()..add(FetchTableData()), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final accessBloc = BlocProvider.of(context); + final filteredData = accessBloc.filteredData; + return state is AccessLoaded + ? const Center(child: CircularProgressIndicator()) + : Container( + padding: padding, + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FilterWidget( + size: MediaQuery.of(context).size, + tabs: accessBloc.tabs, + selectedIndex: accessBloc.selectedIndex, + onTabChanged: (index) { + accessBloc.add(TabChangedEvent(index)); + }, + ), + const SizedBox(height: 20), + if (isSmallScreen || isHalfMediumScreen) + _buildSmallSearchFilters(context, accessBloc) + else + _buildNormalSearchWidgets(context, accessBloc), + const SizedBox(height: 20), + _buildVisitorAdminPasswords(context, accessBloc), + const SizedBox(height: 20), + Expanded( + child: DynamicTable( + tableName: 'AccessManagement', + uuidIndex: 1, + withSelectAll: true, + isEmpty: filteredData.isEmpty, + withCheckBox: false, + size: MediaQuery.of(context).size, + cellDecoration: containerDecoration, + headers: const [ + 'Name', + 'Access Type', + 'Access Start', + 'Access End', + 'Accessible Device', + 'Authorizer', + 'Authorization Date & Time', + 'Access Status' + ], + data: filteredData.map((item) { + return [ + item.passwordName, + item.passwordType.value, + accessBloc.timestampToDate(item.effectiveTime), + accessBloc.timestampToDate(item.invalidTime), + item.deviceName.toString(), + item.authorizerEmail.toString(), + accessBloc.timestampToDate(item.invalidTime), + item.passwordStatus.value, + ]; + }).toList(), + )), + ], + ), + ); + })); + } + + Wrap _buildVisitorAdminPasswords( + BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: [ + Container( + width: 205, + height: 42, + decoration: containerDecoration, + child: DefaultButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const VisitorPasswordDialog(); + }, + ).then((v) { + if (v != null) { + accessBloc.add(FetchTableData()); + } + }); + }, + borderRadius: 8, + child: Text( + 'Create Visitor Password ', + style: context.textTheme.titleSmall! + .copyWith(color: Colors.white, fontSize: 12), + )), + ), + // Container( + // width: 133, + // height: 42, + // decoration: containerDecoration, + // child: DefaultButton( + // borderRadius: 8, + // backgroundColor: ColorsManager.whiteColors, + // child: Text( + // 'Admin Password', + // style: context.textTheme.titleSmall! + // .copyWith(color: Colors.black, fontSize: 12), + // )), + // ), + ], + ); + } + + Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) { + // TimeOfDay _selectedTime = TimeOfDay.now(); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.ideographic, + children: [ + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.passwordName, + height: 43, + isRequired: false, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + width: 250, + child: CustomWebTextField( + controller: accessBloc.emailAuthorizer, + height: 43, + isRequired: false, + textFieldName: 'Authorizer', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + ), + ), + const SizedBox(width: 15), + SizedBox( + child: DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + ), + const SizedBox(width: 15), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } + + Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) { + return Wrap( + spacing: 20, + runSpacing: 10, + children: [ + SizedBox( + width: 300, + child: CustomWebTextField( + controller: accessBloc.passwordName, + isRequired: true, + height: 40, + textFieldName: 'Name', + description: '', + onSubmitted: (value) { + accessBloc.add(FilterDataEvent( + emailAuthorizer: + accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }), + ), + DateTimeWebWidget( + icon: Assets.calendarIcon, + isRequired: false, + title: 'Access Time', + size: MediaQuery.of(context).size, + endTime: () { + accessBloc.add(SelectTime(context: context, isStart: false)); + }, + startTime: () { + accessBloc.add(SelectTime(context: context, isStart: true)); + }, + firstString: BlocProvider.of(context).startTime, + secondString: BlocProvider.of(context).endTime, + ), + SearchResetButtons( + onSearch: () { + accessBloc.add(FilterDataEvent( + emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(), + selectedTabIndex: + BlocProvider.of(context).selectedIndex, + passwordName: accessBloc.passwordName.text.toLowerCase(), + startTime: accessBloc.effectiveTimeTimeStamp, + endTime: accessBloc.expirationTimeTimeStamp)); + }, + onReset: () { + accessBloc.add(ResetSearch()); + }, + ), + ], + ); + } +} diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 869de23f..6571eae4 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -39,8 +39,12 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, - latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null, - longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null, + latitude: json['lat'] != null && json['lat'] != '' + ? double.tryParse(json['lat']?.toString() ?? '0.0') + : null, + longitude: json['lon'] != null && json['lon'] != '' + ? double.tryParse(json['lon']?.toString() ?? '0.0') + : null, ); } } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart index 40d51d2b..455dff23 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -46,11 +46,11 @@ class AirQualityDistributionBloc } } - Future _onClearAirQualityDistribution( + void _onClearAirQualityDistribution( ClearAirQualityDistribution event, Emitter emit, - ) async { - emit(const AirQualityDistributionState()); + ) { + emit(AirQualityDistributionState(selectedAqiType: state.selectedAqiType)); } void _onUpdateAqiTypeEvent( diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart index 88c3715e..326a87a2 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -75,6 +75,6 @@ class RangeOfAqiBloc extends Bloc { ClearRangeOfAqiEvent event, Emitter emit, ) { - emit(const RangeOfAqiState()); + emit(RangeOfAqiState(selectedAqiType: state.selectedAqiType)); } } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 4fdd8a2a..20365325 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget { return BarChart( BarChartData( maxY: 100.1, + alignment: BarChartAlignment.start, gridData: EnergyManagementChartsHelper.gridData( horizontalInterval: 20, ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 25cfd19d..41448f4e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class AqiDistributionChartBox extends StatelessWidget { @@ -32,8 +34,20 @@ class AqiDistributionChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: AqiDistributionChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty, + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == AirQualityDistributionStatus.loading, + isError: state.status == AirQualityDistributionStatus.failure, + isInitial: state.status == AirQualityDistributionStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyBarredChart, + ), + child: Expanded( + child: AqiDistributionChart( + chartData: state.chartData, + ), + ), ), ], ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index f7be6ee3..7b6b113a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -34,6 +34,7 @@ class AqiDistributionChartTitle extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AqiTypeDropdown( + selectedAqiType: context.watch().state.selectedAqiType, onChanged: (value) { if (value != null) { final bloc = context.read(); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 6640c717..8233fe5a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -18,19 +18,20 @@ enum AqiType { } class AqiTypeDropdown extends StatefulWidget { - const AqiTypeDropdown({super.key, required this.onChanged}); + const AqiTypeDropdown({ + required this.onChanged, + this.selectedAqiType, + super.key, + }); final ValueChanged onChanged; + final AqiType? selectedAqiType; @override State createState() => _AqiTypeDropdownState(); } class _AqiTypeDropdownState extends State { - AqiType? _selectedItem = AqiType.aqi; - - void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); - @override Widget build(BuildContext context) { return Container( @@ -41,8 +42,8 @@ class _AqiTypeDropdownState extends State { width: 1, ), ), - child: DropdownButton( - value: _selectedItem, + child: DropdownButton( + value: widget.selectedAqiType, isDense: true, borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, @@ -59,10 +60,7 @@ class _AqiTypeDropdownState extends State { items: AqiType.values .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) .toList(), - onChanged: (value) { - _updateSelectedItem(value); - widget.onChanged(value); - }, + onChanged: widget.onChanged, ), ); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index cb189dce..5ec5158f 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class RangeOfAqiChartBox extends StatelessWidget { @@ -32,10 +34,20 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - chartData: state.filteredRangeOfAqi, - selectedAqiType: state.selectedAqiType, + Visibility( + visible: state.filteredRangeOfAqi.isNotEmpty, + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == RangeOfAqiStatus.loading, + isError: state.status == RangeOfAqiStatus.failure, + isInitial: state.status == RangeOfAqiStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyRangeOfAqi, + ), + child: Expanded( + child: RangeOfAqiChart( + chartData: state.filteredRangeOfAqi, + selectedAqiType: state.selectedAqiType, + ), ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 1b0da288..421fbb13 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -63,15 +63,15 @@ class RangeOfAqiChartTitle extends StatelessWidget { fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AqiTypeDropdown( + selectedAqiType: context.watch().state.selectedAqiType, onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - - if (spaceUuid == null) return; - if (value != null) { context.read().add(UpdateAqiTypeEvent(value)); } + + if (spaceUuid == null) return; }, ), ), diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart index 09fb6155..a5af9e10 100644 --- a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: MediaQuery.sizeOf(context).height * 1, + height: MediaQuery.sizeOf(context).height * 1.05, child: const Column( children: [ Expanded( diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index 06b6c529..48c9af94 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -5,8 +5,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { @@ -54,8 +56,24 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(height: 0), const SizedBox(height: 20), - Expanded( - child: EnergyConsumptionPerDeviceChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty && + state.chartData + .every((e) => e.energy.every((e) => e.value != 0)), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: + state.status == EnergyConsumptionPerDeviceStatus.loading, + isError: state.status == EnergyConsumptionPerDeviceStatus.failure, + isInitial: + state.status == EnergyConsumptionPerDeviceStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyEnergyManagementPerDevice, + ), + child: Expanded( + child: EnergyConsumptionPerDeviceChart( + chartData: state.chartData, + ), + ), ), ], ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index 4d88471d..a7992223 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -3,8 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class TotalEnergyConsumptionChartBox extends StatelessWidget { @@ -41,7 +43,18 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - TotalEnergyConsumptionChart(chartData: state.chartData), + Visibility( + visible: state.chartData.isNotEmpty && + state.chartData.every((e) => e.value != 0), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == TotalEnergyConsumptionStatus.loading, + isError: state.status == TotalEnergyConsumptionStatus.failure, + isInitial: state.status == TotalEnergyConsumptionStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyEnergyManagementChart, + ), + child: TotalEnergyConsumptionChart(chartData: state.chartData), + ), ], ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 1205a66e..23cc527d 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -18,6 +18,7 @@ class OccupancyChart extends StatelessWidget { return BarChart( BarChartData( maxY: 100.001, + alignment: BarChartAlignment.start, gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 20, diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart index 08f7223f..30d96ac5 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart @@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class OccupancyChartBox extends StatelessWidget { @@ -67,7 +69,24 @@ class OccupancyChartBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - Expanded(child: OccupancyChart(chartData: state.chartData)), + Visibility( + visible: state.chartData.isNotEmpty && + state.chartData.every( + (e) => e.occupancy.isNotEmpty, + ), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == OccupancyStatus.loading, + isError: state.status == OccupancyStatus.failure, + isInitial: state.status == OccupancyStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyBarredChart, + ), + child: Expanded( + child: OccupancyChart( + chartData: state.chartData, + ), + ), + ), ], ), ); diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index a5f56aa4..9c8e3a1b 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; class OccupancyHeatMapBox extends StatelessWidget { @@ -68,16 +70,29 @@ class OccupancyHeatMapBox extends StatelessWidget { const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), - Expanded( - child: OccupancyHeatMap( - selectedDate: - context.watch().state.yearlyDate, - heatMapData: state.heatMapData.asMap().map( - (_, value) => MapEntry( - value.eventDate, - value.countTotalPresenceDetected, + Visibility( + visible: state.heatMapData.isNotEmpty && + state.heatMapData.every( + (e) => e.countTotalPresenceDetected != 0, + ), + replacement: AnalyticsChartEmptyStateWidget( + isLoading: state.status == OccupancyHeatMapStatus.loading, + isError: state.status == OccupancyHeatMapStatus.failure, + isInitial: state.status == OccupancyHeatMapStatus.initial, + errorMessage: state.errorMessage, + iconPath: Assets.emptyHeatmap, + ), + child: Expanded( + child: OccupancyHeatMap( + selectedDate: + context.watch().state.yearlyDate, + heatMapData: state.heatMapData.asMap().map( + (_, value) => MapEntry( + value.eventDate, + value.countTotalPresenceDetected, + ), ), - ), + ), ), ), ], diff --git a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart index f38f607d..0a49a797 100644 --- a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart +++ b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart @@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService { 'reverse', queryParameters: { 'format': 'json', - 'lat': param.latitude, - 'lon': param.longitude, + 'lat': 25.1880567, + 'lon': 55.266608, }, ); diff --git a/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart new file mode 100644 index 00000000..f65e1de0 --- /dev/null +++ b/lib/pages/analytics/widgets/analytics_chart_empty_state_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsChartEmptyStateWidget extends StatelessWidget { + const AnalyticsChartEmptyStateWidget({ + required this.iconPath, + this.isLoading = false, + this.isError = false, + this.isInitial = false, + this.errorMessage, + this.noDataMessage = 'No data to display', + this.initialMessage = 'Please select a space to see data', + super.key, + }); + + final bool isLoading; + final bool isError; + final bool isInitial; + final String? errorMessage; + final String noDataMessage; + final String initialMessage; + final String iconPath; + + @override + Widget build(BuildContext context) { + return Expanded( + child: _buildWidgetBasedOnState(context), + ); + } + + Widget _buildWidgetBasedOnState(BuildContext context) { + final widgetsMap = { + isLoading: const AppLoadingIndicator(), + isInitial: _buildState(context, initialMessage), + isError: _buildState(context, errorMessage ?? 'Something went wrong'), + }; + + return widgetsMap[true] ?? _buildState(context, noDataMessage); + } + + Widget _buildState(BuildContext context, String message) { + return Center( + child: Column( + spacing: 16, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 16), + Expanded(child: SvgPicture.asset(iconPath, fit: BoxFit.contain)), + SelectableText( + message, + style: isError + ? context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.red, + fontSize: 16, + fontWeight: FontWeight.w700, + ) + : null, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ], + ), + ); + } +} diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index fb8237b7..93f8998e 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -50,6 +50,9 @@ class _DynamicTableState extends State { bool _selectAll = false; final ScrollController _verticalScrollController = ScrollController(); final ScrollController _horizontalScrollController = ScrollController(); + static const double _fixedRowHeight = 60; + static const double _checkboxColumnWidth = 50; + static const double _settingsColumnWidth = 100; @override void initState() { @@ -67,7 +70,6 @@ class _DynamicTableState extends State { bool _compareListOfLists( List> oldList, List> newList) { - // Check if the old and new lists are the same if (oldList.length != newList.length) return false; for (int i = 0; i < oldList.length; i++) { @@ -104,73 +106,130 @@ class _DynamicTableState extends State { context.read().add(UpdateSelection(_selectedRows)); } + double get _totalTableWidth { + final hasSettings = widget.headers.contains('Settings'); + final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) + + (hasSettings ? _settingsColumnWidth : 0); + final regularCount = widget.headers.length - (hasSettings ? 1 : 0); + final regularWidth = (widget.size.width - base) / regularCount; + return base + regularCount * regularWidth; + } + @override Widget build(BuildContext context) { return Container( + width: widget.size.width, + height: widget.size.height, decoration: widget.cellDecoration, - child: Scrollbar( - controller: _verticalScrollController, - thumbVisibility: true, - trackVisibility: true, + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), child: Scrollbar( - //fixed the horizontal scrollbar issue controller: _horizontalScrollController, thumbVisibility: true, trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, + notificationPredicate: (notif) => + notif.metrics.axis == Axis.horizontal, child: SingleChildScrollView( - controller: _verticalScrollController, - child: SingleChildScrollView( - controller: _horizontalScrollController, - scrollDirection: Axis.horizontal, - child: SizedBox( - width: widget.size.width, - child: Column( - children: [ - Container( - decoration: widget.headerDecoration ?? - const BoxDecoration( - color: ColorsManager.boxColor, + controller: _horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: _totalTableWidth, + child: Column( + children: [ + Container( + height: _fixedRowHeight, + decoration: widget.headerDecoration ?? + const BoxDecoration(color: ColorsManager.boxColor), + child: Row( + children: [ + if (widget.withCheckBox) + _buildSelectAllCheckbox(_checkboxColumnWidth), + for (var i = 0; i < widget.headers.length; i++) + _buildTableHeaderCell( + widget.headers[i], + widget.headers[i] == 'Settings' + ? _settingsColumnWidth + : (_totalTableWidth - + (widget.withCheckBox + ? _checkboxColumnWidth + : 0) - + (widget.headers.contains('Settings') + ? _settingsColumnWidth + : 0)) / + (widget.headers.length - + (widget.headers.contains('Settings') + ? 1 + : 0)), ), - child: Row( - children: [ - if (widget.withCheckBox) _buildSelectAllCheckbox(), - ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell( - widget.headers[index], index); - }) - //...widget.headers.map((header) => _buildTableHeaderCell(header)), - ], - ), + ], ), - SizedBox( - width: widget.size.width, - child: widget.isEmpty - ? _buildEmptyState() - : Column( - children: - List.generate(widget.data.length, (rowIndex) { + ), + + Expanded( + child: widget.isEmpty + ? _buildEmptyState() + : Scrollbar( + controller: _verticalScrollController, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => + notif.metrics.axis == Axis.vertical, + child: ListView.builder( + controller: _verticalScrollController, + itemCount: widget.data.length, + itemBuilder: (_, rowIndex) { final row = widget.data[rowIndex]; - return Row( - children: [ - if (widget.withCheckBox) - _buildRowCheckbox( - rowIndex, widget.size.height * 0.08), - ...row.asMap().entries.map((entry) { - return _buildTableCell( - entry.value.toString(), - widget.size.height * 0.08, - rowIndex: rowIndex, - columnIndex: entry.key, - ); - }).toList(), - ], + return SizedBox( + height: _fixedRowHeight, + child: Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox( + rowIndex, + _checkboxColumnWidth, + ), + for (var colIndex = 0; + colIndex < row.length; + colIndex++) + widget.headers[colIndex] == 'Settings' + ? buildSettingsIcon( + width: _settingsColumnWidth, + onTap: () => widget + .onSettingsPressed + ?.call(rowIndex), + ) + : _buildTableCell( + row[colIndex].toString(), + width: widget.headers[ + colIndex] == + 'Settings' + ? _settingsColumnWidth + : (_totalTableWidth - + (widget.withCheckBox + ? _checkboxColumnWidth + : 0) - + (widget.headers + .contains( + 'Settings') + ? _settingsColumnWidth + : 0)) / + (widget.headers.length - + (widget.headers + .contains( + 'Settings') + ? 1 + : 0)), + rowIndex: rowIndex, + columnIndex: colIndex, + ), + ], + ), ); - }), + }, ), - ), - ], - ), + ), + ), + ], ), ), ), @@ -210,9 +269,10 @@ class _DynamicTableState extends State { ], ), ); - Widget _buildSelectAllCheckbox() { + + Widget _buildSelectAllCheckbox(double width) { return Container( - width: 50, + width: width, decoration: const BoxDecoration( border: Border.symmetric( vertical: BorderSide(color: ColorsManager.boxDivider), @@ -227,11 +287,11 @@ class _DynamicTableState extends State { ); } - Widget _buildRowCheckbox(int index, double size) { + Widget _buildRowCheckbox(int index, double width) { return Container( - width: 50, + width: width, padding: const EdgeInsets.all(8.0), - height: size, + height: _fixedRowHeight, decoration: const BoxDecoration( border: Border( bottom: BorderSide( @@ -253,50 +313,47 @@ class _DynamicTableState extends State { ); } - Widget _buildTableHeaderCell(String title, int index) { - return Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border.symmetric( - vertical: BorderSide(color: ColorsManager.boxDivider), - ), + Widget _buildTableHeaderCell(String title, double width) { + return Container( + width: width, + decoration: const BoxDecoration( + border: Border.symmetric( + vertical: BorderSide(color: ColorsManager.boxDivider), ), - constraints: const BoxConstraints.expand(height: 40), - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: index == widget.headers.length - 1 ? 12 : 8.0, - vertical: 4), - child: Text( - title, - style: context.textTheme.titleSmall!.copyWith( - color: ColorsManager.grayColor, - fontSize: 12, - fontWeight: FontWeight.w400, - ), - maxLines: 2, + ), + constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight), + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Text( + title, + style: context.textTheme.titleSmall!.copyWith( + color: ColorsManager.grayColor, + fontSize: 12, + fontWeight: FontWeight.w400, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), ); } - Widget _buildTableCell(String content, double size, - {required int rowIndex, required int columnIndex}) { + Widget _buildTableCell(String content, + {required double width, + required int rowIndex, + required int columnIndex}) { bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; if (isBatteryLevel) { batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); } + bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; if (isSettingsColumn) { return buildSettingsIcon( - width: 120, - height: 60, - iconSize: 40, - onTap: () => widget.onSettingsPressed?.call(rowIndex), - ); + width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex)); } Color? statusColor; @@ -320,92 +377,82 @@ class _DynamicTableState extends State { statusColor = Colors.black; } - return Expanded( - child: Container( - height: size, - padding: const EdgeInsets.all(5.0), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: ColorsManager.boxDivider, - width: 1.0, - ), + return Container( + width: width, + height: _fixedRowHeight, + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, ), - color: Colors.white, ), - alignment: Alignment.centerLeft, - child: Text( - content, - style: TextStyle( - color: (batteryLevel != null && batteryLevel < 20) - ? ColorsManager.red - : (batteryLevel != null && batteryLevel > 20) - ? ColorsManager.green - : statusColor, - fontSize: 13, - fontWeight: FontWeight.w400), - maxLines: 2, + color: Colors.white, + ), + alignment: Alignment.centerLeft, + child: Text( + content, + style: TextStyle( + color: (batteryLevel != null && batteryLevel < 20) + ? ColorsManager.red + : (batteryLevel != null && batteryLevel > 20) + ? ColorsManager.green + : statusColor, + fontSize: 13, + fontWeight: FontWeight.w400, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ); } - Widget buildSettingsIcon( - {double width = 120, - double height = 60, - double iconSize = 40, - VoidCallback? onTap}) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10), - margin: const EdgeInsets.only(right: 15), - decoration: const BoxDecoration( - color: ColorsManager.whiteColors, - border: Border( - bottom: BorderSide( - color: ColorsManager.boxDivider, - width: 1.0, - ), - ), + Widget buildSettingsIcon({required double width, VoidCallback? onTap}) { + return Container( + width: width, + height: _fixedRowHeight, + padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10), + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, ), - width: width, - child: Padding( - padding: const EdgeInsets.only( - right: 16.0, - left: 17.0, - ), - child: Container( - width: 50, - decoration: BoxDecoration( - color: const Color(0xFFF7F8FA), - borderRadius: BorderRadius.circular(height / 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.17), - blurRadius: 14, - offset: const Offset(0, 4), - ), - ], + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: 50, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.17), + blurRadius: 14, + offset: const Offset(0, 4), ), - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: SvgPicture.asset( - Assets.settings, - width: 40, - height: 22, - color: ColorsManager.primaryColor, - ), - ), + ], + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Center( + child: SvgPicture.asset( + Assets.settings, + width: 40, + height: 20, + color: ColorsManager.primaryColor, ), ), ), ), ), - ], + ), ); } } diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index aad0669b..61168f55 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -15,7 +15,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class AcDeviceBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const AcDeviceBatchControlView({super.key, required this.devicesIds}); final List devicesIds; @@ -51,7 +52,7 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo deviceId: devicesIds.first, code: 'switch', value: state.status.acSwitch, - label: 'ThermoState', + label: 'Thermostat', icon: Assets.ac, onChange: (value) { context.read().add(AcBatchControlEvent( @@ -100,8 +101,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo ), Text( 'h', - style: - context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blackColor), ), Text( '30', @@ -148,7 +149,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo callFactoryReset: () { context.read().add(AcFactoryResetEvent( deviceId: state.status.uuid, - factoryResetModel: FactoryResetModel(devicesUuid: devicesIds), + factoryResetModel: + FactoryResetModel(devicesUuid: devicesIds), )); }, ), diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index eeb5e45c..d8cd04df 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -16,11 +16,12 @@ class DeviceManagementBloc int _onlineCount = 0; int _offlineCount = 0; int _lowBatteryCount = 0; - List _selectedDevices = []; + final List _selectedDevices = []; List _filteredDevices = []; String currentProductName = ''; String? currentCommunity; String? currentUnitName; + String subSpaceName = ''; DeviceManagementBloc() : super(DeviceManagementInitial()) { on(_onFetchDevices); @@ -31,15 +32,17 @@ class DeviceManagementBloc on(_onResetFilters); on(_onResetSelectedDevices); on(_onUpdateSelection); + on(_onUpdateDeviceName); + on(_onUpdateSubSpaceName); } Future _onFetchDevices( FetchDevices event, Emitter emit) async { emit(DeviceManagementLoading()); try { - List devices = []; + var devices = []; _devices.clear(); - var spaceBloc = event.context.read(); + final spaceBloc = event.context.read(); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (spaceBloc.state.selectedCommunities.isEmpty) { @@ -73,7 +76,7 @@ class DeviceManagementBloc } } - void _onFilterDevices( + Future _onFilterDevices( FilterDevices event, Emitter emit) async { if (_devices.isNotEmpty) { _filteredDevices = List.from(_devices.where((device) { @@ -155,8 +158,7 @@ class DeviceManagementBloc add(FilterDevices(_getFilterFromIndex(_selectedIndex))); } - void _onSelectDevice( - SelectDevice event, Emitter emit) { + void _onSelectDevice(SelectDevice event, Emitter emit) { final selectedUuid = event.selectedDevice.uuid; if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { @@ -165,9 +167,9 @@ class DeviceManagementBloc _selectedDevices.add(event.selectedDevice); } - List clonedSelectedDevices = List.from(_selectedDevices); + final clonedSelectedDevices = List.from(_selectedDevices); - bool isControlButtonEnabled = + final isControlButtonEnabled = _checkIfControlButtonEnabled(clonedSelectedDevices); if (state is DeviceManagementLoaded) { @@ -197,8 +199,8 @@ class DeviceManagementBloc void _onUpdateSelection( UpdateSelection event, Emitter emit) { - List selectedDevices = []; - List devicesToSelectFrom = []; + final selectedDevices = []; + var devicesToSelectFrom = []; if (state is DeviceManagementLoaded) { devicesToSelectFrom = (state as DeviceManagementLoaded).devices; @@ -206,7 +208,7 @@ class DeviceManagementBloc devicesToSelectFrom = (state as DeviceManagementFiltered).filteredDevices; } - for (int i = 0; i < event.selectedRows.length; i++) { + for (var i = 0; i < event.selectedRows.length; i++) { if (event.selectedRows[i]) { selectedDevices.add(devicesToSelectFrom[i]); } @@ -252,8 +254,7 @@ class DeviceManagementBloc _onlineCount = _devices.where((device) => device.online == true).length; _offlineCount = _devices.where((device) => device.online == false).length; _lowBatteryCount = _devices - .where((device) => - device.batteryLevel != null && device.batteryLevel! < 20) + .where((device) => device.batteryLevel != null && device.batteryLevel! < 20) .length; } @@ -270,8 +271,7 @@ class DeviceManagementBloc } } - void _onSearchDevices( - SearchDevices event, Emitter emit) { + void _onSearchDevices(SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) && (event.deviceNameOrProductName == null || @@ -300,7 +300,7 @@ class DeviceManagementBloc currentCommunity = event.community; currentUnitName = event.unitName; - List devicesToSearch = _devices; + final devicesToSearch = _devices; if (devicesToSearch.isNotEmpty) { final searchText = event.deviceNameOrProductName?.toLowerCase() ?? ''; @@ -343,5 +343,134 @@ class DeviceManagementBloc } } + void _onUpdateDeviceName( + UpdateDeviceName event, Emitter emit) { + final devices = _devices.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + _selectedDevices.removeWhere((device) => device.uuid == event.deviceId); + _selectedDevices.add(modifiedDevice); + return modifiedDevice; + } + return device; + }).toList(); + + final filteredDevices = _filteredDevices.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + _selectedDevices.removeWhere((device) => device.uuid == event.deviceId); + _selectedDevices.add(modifiedDevice); + return modifiedDevice; + } + return device; + }).toList(); + + _devices = devices; + _filteredDevices = filteredDevices; + + if (state is DeviceManagementLoaded) { + final loaded = state as DeviceManagementLoaded; + final selectedDevices01 = _selectedDevices.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + return modifiedDevice; + } + return device; + }).toList(); + emit(DeviceManagementLoaded( + devices: devices, + selectedIndex: loaded.selectedIndex, + onlineCount: loaded.onlineCount, + offlineCount: loaded.offlineCount, + lowBatteryCount: loaded.lowBatteryCount, + selectedDevice: selectedDevices01, + isControlButtonEnabled: loaded.isControlButtonEnabled, + )); + } else if (state is DeviceManagementFiltered) { + final filtered = state as DeviceManagementFiltered; + final selectedDevices01 = filtered.selectedDevice?.map((device) { + if (device.uuid == event.deviceId) { + final modifiedDevice = device.copyWith(name: event.newName); + return modifiedDevice; + } + return device; + }).toList(); + emit(DeviceManagementFiltered( + filteredDevices: filteredDevices, + selectedIndex: filtered.selectedIndex, + onlineCount: filtered.onlineCount, + offlineCount: filtered.offlineCount, + lowBatteryCount: filtered.lowBatteryCount, + selectedDevice: selectedDevices01, + isControlButtonEnabled: filtered.isControlButtonEnabled, + )); + } + } + + void _onUpdateSubSpaceName( + UpdateSubSpaceName event, Emitter emit) { + final devices = _devices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + } + return device; + }).toList(); + + final filteredDevices = _filteredDevices.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + } + return device; + }).toList(); + + _devices = devices; + _filteredDevices = filteredDevices; + + if (state is DeviceManagementLoaded) { + final loaded = state as DeviceManagementLoaded; + final selectedDevices = loaded.selectedDevice?.map((device) { + if (device.uuid == event.deviceId) { + return device.copyWith( + subspace: + device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); + } + return device; + }).toList(); + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: loaded.selectedIndex, + onlineCount: loaded.onlineCount, + offlineCount: loaded.offlineCount, + lowBatteryCount: loaded.lowBatteryCount, + selectedDevice: selectedDevices, + isControlButtonEnabled: loaded.isControlButtonEnabled, + )); + } else if (state is DeviceManagementFiltered) { + // final filtered = state as DeviceManagementFiltered; + // emit(DeviceManagementFiltered( + // filteredDevices: _filteredDevices, + // selectedIndex: filtered.selectedIndex, + // onlineCount: filtered.onlineCount, + // offlineCount: filtered.offlineCount, + // lowBatteryCount: filtered.lowBatteryCount, + // selectedDevice: filtered.selectedDevice, + // isControlButtonEnabled: filtered.isControlButtonEnabled, + // )); + } + } + + void changeSubspaceName( + String deviceId, String newSubSpaceName, String subspaceId) { + add(UpdateSubSpaceName( + deviceId: deviceId, + newSubSpaceName: newSubSpaceName, + subspaceId: subspaceId, + )); + } + List get selectedDevices => _selectedDevices; } diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart index 5292de0e..e3b3acac 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart @@ -70,3 +70,21 @@ class UpdateSelection extends DeviceManagementEvent { const UpdateSelection(this.selectedRows); } + +class UpdateDeviceName extends DeviceManagementEvent { + final String deviceId; + final String newName; + + const UpdateDeviceName({required this.deviceId, required this.newName}); +} + +class UpdateSubSpaceName extends DeviceManagementEvent { + final String deviceId; + final String newSubSpaceName; + final String subspaceId; + + const UpdateSubSpaceName( + {required this.deviceId, + required this.newSubSpaceName, + required this.subspaceId}); +} diff --git a/lib/pages/device_managment/all_devices/models/device_status.dart b/lib/pages/device_managment/all_devices/models/device_status.dart index b78f2a30..1f23e3f9 100644 --- a/lib/pages/device_managment/all_devices/models/device_status.dart +++ b/lib/pages/device_managment/all_devices/models/device_status.dart @@ -60,4 +60,13 @@ class Status { factory Status.fromJson(String source) => Status.fromMap(json.decode(source)); String toJson() => json.encode(toMap()); + Status copyWith({ + String? code, + dynamic value, + }) { + return Status( + code: code ?? this.code, + value: value ?? this.value, + ); + } } diff --git a/lib/pages/device_managment/all_devices/models/device_subspace.model.dart b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart index dc2386de..5d5f44bf 100644 --- a/lib/pages/device_managment/all_devices/models/device_subspace.model.dart +++ b/lib/pages/device_managment/all_devices/models/device_subspace.model.dart @@ -44,4 +44,20 @@ class DeviceSubspace { static List> listToJson(List subspaces) { return subspaces.map((subspace) => subspace.toJson()).toList(); } + + DeviceSubspace copyWith({ + String? uuid, + DateTime? createdAt, + DateTime? updatedAt, + String? subspaceName, + bool? disabled, + }) { + return DeviceSubspace( + uuid: uuid ?? this.uuid, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + subspaceName: subspaceName ?? this.subspaceName, + disabled: disabled ?? this.disabled, + ); + } } diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index e491214d..21fd1193 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -588,4 +588,72 @@ SOS "NCPS": DeviceType.NCPS, "PC": DeviceType.PC, }; + + AllDevicesModel copyWith({ + DevicesModelRoom? room, + DeviceSubspace? subspace, + DevicesModelUnit? unit, + DeviceCommunityModel? community, + String? productUuid, + String? productType, + String? permissionType, + int? activeTime, + String? category, + String? categoryName, + int? createTime, + String? gatewayId, + String? icon, + String? ip, + String? lat, + String? localKey, + String? lon, + String? model, + String? name, + String? nodeId, + bool? online, + String? ownerId, + bool? sub, + String? timeZone, + int? updateTime, + String? uuid, + int? batteryLevel, + String? productName, + List? spaces, + List? deviceTags, + DeviceSubSpace? deviceSubSpace, + }) { + return AllDevicesModel( + room: room ?? this.room, + subspace: subspace ?? this.subspace, + unit: unit ?? this.unit, + community: community ?? this.community, + productUuid: productUuid ?? this.productUuid, + productType: productType ?? this.productType, + permissionType: permissionType ?? this.permissionType, + activeTime: activeTime ?? this.activeTime, + category: category ?? this.category, + categoryName: categoryName ?? this.categoryName, + createTime: createTime ?? this.createTime, + gatewayId: gatewayId ?? this.gatewayId, + icon: icon ?? this.icon, + ip: ip ?? this.ip, + lat: lat ?? this.lat, + localKey: localKey ?? this.localKey, + lon: lon ?? this.lon, + model: model ?? this.model, + name: name ?? this.name, + nodeId: nodeId ?? this.nodeId, + online: online ?? this.online, + ownerId: ownerId ?? this.ownerId, + sub: sub ?? this.sub, + timeZone: timeZone ?? this.timeZone, + updateTime: updateTime ?? this.updateTime, + uuid: uuid ?? this.uuid, + batteryLevel: batteryLevel ?? this.batteryLevel, + productName: productName ?? this.productName, + spaces: spaces ?? this.spaces, + deviceTags: deviceTags ?? this.deviceTags, + deviceSubSpace: deviceSubSpace ?? this.deviceSubSpace, + ); + } } diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index c865a5dc..5b709540 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -23,6 +23,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { @override Widget build(BuildContext context) { return BlocBuilder( + buildWhen: (previous, current) => previous != current, builder: (context, state) { List devicesToShow = []; int selectedIndex = 0; @@ -31,7 +32,6 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { int lowBatteryCount = 0; bool isControlButtonEnabled = false; List selectedDevices = []; - if (state is DeviceManagementLoaded) { devicesToShow = state.devices; selectedIndex = state.selectedIndex; @@ -68,6 +68,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { children: [ Expanded(child: SpaceTreeView( onSelect: () { + context.read().add(ResetFilters()); context.read().add(FetchDevices(context)); }, )), @@ -111,6 +112,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { onPressed: isControlButtonEnabled ? () { if (isAnyDeviceOffline) { + ScaffoldMessenger.of(context) + .clearSnackBars(); ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( @@ -190,7 +193,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Product Name', 'Device ID', 'Space Name', - 'location', + 'Location', 'Battery Level', 'Installation Date and Time', 'Status', @@ -242,7 +245,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, - onSettingsPressed: (rowIndex) { + onSettingsPressed: (rowIndex) async { final device = devicesToShow[rowIndex]; showDeviceSettingsSidebar(context, device); }, @@ -264,7 +267,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { barrierDismissible: true, barrierLabel: "Device Settings", transitionDuration: const Duration(milliseconds: 300), - pageBuilder: (context, anim1, anim2) { + pageBuilder: (_, anim1, anim2) { return Align( alignment: Alignment.centerRight, child: Material( @@ -274,6 +277,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { child: DeviceSettingsPanel( device: device, onClose: () => Navigator.of(context).pop(), + deviceManagementBloc: context.read(), ), ), ), diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart index 82c812ce..2bd6f9cd 100644 --- a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, - category: 'CUR_2', + category: 'Timer', code: 'control', - + countdownCode: 'Timer', + deviceType: 'CUR_2', ), )); }, diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart index 54107420..0d3a1a92 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class AccurteCalibratingDialog extends StatelessWidget { final String deviceId; @@ -17,14 +18,15 @@ class AccurteCalibratingDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Calibrating', body: const NormalTextBodyForDialog( title: '', step1: - '1. Click Close Button to make the Curtain run to Full Close and Position.', - step2: '2. click Next to complete the Calibration.', + 'Click Close Button to make the Curtain run to Full Close and Position.', + step2: 'click Next to complete the Calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart index a9d1b010..7124639d 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class AccurateCalibrationDialog extends StatelessWidget { final String deviceId; @@ -15,13 +16,14 @@ class AccurateCalibrationDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Accurate Calibration', body: const NormalTextBodyForDialog( title: 'Prepare Calibration:', - step1: '1. Run The Curtain to the Fully Open Position,and pause.', - step2: '2. click Next to Start accurate calibration.', + step1: 'Run The Curtain to the Fully Open Position,and pause.', + step2: 'click Next to Start accurate calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart index 5be376ae..0d6ea90c 100644 --- a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -17,78 +17,102 @@ class AccurateDialogWidget extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 300, - width: 400, + height: 250, + width: 500, child: Column( children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Text( - title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: ColorsManager.blueColor, - ), + Expanded( + flex: 3, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.dialogBlueTitle, + ), + ), + ), + const Divider( + indent: 60, + endIndent: 60, + ), + ], ), ), - const SizedBox(height: 5), - const Divider( - indent: 10, - endIndent: 10, - ), - Padding( - padding: const EdgeInsets.all(10), + Expanded( + flex: 5, child: body, ), - const SizedBox(height: 20), - const Spacer(), - const Divider(), - Row( - children: [ - Expanded( - child: InkWell( - onTap: leftOnTap, - child: Container( - height: 60, - alignment: Alignment.center, - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: ColorsManager.grayBorder, + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Expanded(child: Divider()), + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + onTap: leftOnTap, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(26), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.grayBorder), + ), ), ), ), - child: const Text( - 'Cancel', - style: TextStyle(color: ColorsManager.grayBorder), - ), - ), - ), - ), - Expanded( - child: InkWell( - onTap: rightOnTap, - child: Container( - height: 60, - alignment: Alignment.center, - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: ColorsManager.grayBorder, + Expanded( + child: InkWell( + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(26), + ), + onTap: rightOnTap, + child: Container( + height: 40, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(26), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + color: ColorsManager.blueColor, + ), + ), ), ), - ), - child: const Text( - 'Next', - style: TextStyle( - color: ColorsManager.blueColor, - ), - ), - ), - ), - ) - ], + ) + ], + ) + ], + ), ) ], ), diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart index 9b2b5ea9..77957d76 100644 --- a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; class CalibrateCompletedDialog extends StatelessWidget { final BuildContext parentContext; @@ -15,58 +17,69 @@ class CalibrateCompletedDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: SizedBox( height: 250, width: 400, child: Column( children: [ - const Padding( - padding: EdgeInsets.all(10), - child: Text( - 'Calibration Completed', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: ColorsManager.blueColor, - ), + Expanded( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + 'Calibration Completed', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.dialogBlueTitle, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + ], ), ), - const SizedBox(height: 5), - const Divider( - indent: 10, - endIndent: 10, + Expanded( + child: SvgPicture.asset(Assets.completedDoneIcon), ), - const Icon( - Icons.check_circle, - size: 100, - color: ColorsManager.blueColor, - ), - const Spacer(), - const Divider( - indent: 10, - endIndent: 10, - ), - InkWell( - onTap: () { - parentContext.read().add( - FetchCurtainModuleStatusEvent( - deviceId: deviceId, - ), - ); - Navigator.of(parentContext).pop(); - Navigator.of(parentContext).pop(); - }, - child: Container( - height: 40, - width: double.infinity, - alignment: Alignment.center, - child: const Text( - 'Close', - style: TextStyle( - color: ColorsManager.grayBorder, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Divider( + indent: 10, + endIndent: 10, ), - ), + InkWell( + onTap: () { + parentContext.read().add( + FetchCurtainModuleStatusEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + Navigator.of(parentContext).pop(); + }, + child: Container( + height: 40, + width: double.infinity, + alignment: Alignment.center, + child: const Text( + 'Close', + style: TextStyle( + color: ColorsManager.grayBorder, + ), + ), + ), + ) + ], ), ) ], diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart index 8818cb7b..fa293ec6 100644 --- a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -15,28 +15,72 @@ class NormalTextBodyForDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - color: ColorsManager.grayColor, + return Padding( + padding: EdgeInsetsGeometry.only(left: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title.isEmpty) + const SizedBox() + else + Expanded( + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 15, + ), + ), + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 10, + ), + const Text('1. ', + style: TextStyle( + color: ColorsManager.grayColor, + fontSize: 15, + )), + SizedBox( + width: 450, + child: Text( + step1, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 15, + ), + ), + ), + ], + ), ), - ), - Text( - step1, - style: const TextStyle( - color: ColorsManager.grayColor, - ), - ), - Text( - step2, - style: const TextStyle( - color: ColorsManager.grayColor, - ), - ) - ], + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 10, + ), + const Text('2. ', + style: TextStyle( + color: ColorsManager.grayColor, + fontSize: 15, + )), + Text( + step2, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 15, + ), + ), + ], + ), + ) + ], + ), ); } } diff --git a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart index ea95f838..428f6531 100644 --- a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart +++ b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart @@ -19,7 +19,7 @@ class NumberInputField extends StatelessWidget { contentPadding: EdgeInsets.zero, ), style: const TextStyle( - fontSize: 20, + fontSize: 15, color: ColorsManager.blackColor, ), ); diff --git a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart index 81912e80..85c45d27 100644 --- a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart +++ b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart @@ -18,7 +18,7 @@ class PrefReversCardWidget extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultContainer( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart index 1e4f932c..35844c05 100644 --- a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -23,12 +23,12 @@ class CurtainModulePrefrencesDialog extends StatelessWidget { Widget build(_) { return AlertDialog( backgroundColor: ColorsManager.CircleImageBackground, - contentPadding: const EdgeInsets.all(30), - title: const Center( + contentPadding: const EdgeInsets.all(20), + title: Center( child: Text( 'Preferences', style: TextStyle( - color: ColorsManager.blueColor, + color: ColorsManager.dialogBlueTitle, fontSize: 24, fontWeight: FontWeight.bold, ), diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart index 0b86c96e..6fc9adf2 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -63,55 +63,82 @@ class _QuickCalibratingDialogState extends State { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Calibrating', body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '1. please Enter the Travel Time:', - style: TextStyle(color: ColorsManager.grayBorder), - ), - const SizedBox(height: 10), - Container( - width: 150, - height: 40, - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: NumberInputField(controller: _controller), - ), - const Expanded( - child: Text( - 'seconds', - style: TextStyle( - fontSize: 15, - color: ColorsManager.blueColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - if (_errorText != null) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _errorText!, - style: const TextStyle( - color: ColorsManager.red, - fontSize: 14, + const Expanded( + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(right: 75), + child: Text( + '1.please Enter the Travel Time:', + style: TextStyle(color: ColorsManager.lightGrayColor), ), ), ), + ), + Expanded( + child: Align( + alignment: Alignment.center, + child: Container( + width: 130, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: ColorsManager.neutralGray.withValues( + alpha: 0.5, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsetsGeometry.only(left: 5), + child: NumberInputField(controller: _controller)), + ), + Expanded( + child: Text( + 'seconds', + style: TextStyle( + fontSize: 12, + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ), + ), + if (_errorText != null) + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _errorText!, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 14, + ), + ), + ), + ), + const Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: Text( + '2.click Next to Complete the calibration', + style: TextStyle(color: ColorsManager.lightGrayColor), + ), + ), + ) ], ), leftOnTap: () => Navigator.of(widget.parentContext).pop(), diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart index 803d904f..06b386c8 100644 --- a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; class QuickCalibrationDialog extends StatelessWidget { final int timControl; @@ -17,14 +18,15 @@ class QuickCalibrationDialog extends StatelessWidget { @override Widget build(_) { return AlertDialog( + backgroundColor: ColorsManager.whiteColors, contentPadding: EdgeInsets.zero, content: AccurateDialogWidget( title: 'Quick Calibration', body: const NormalTextBodyForDialog( title: 'Prepare Calibration:', step1: - '1. Confirm that the curtain is in the fully closed and suspended state.', - step2: '2. click Next to Start calibration.', + 'Confirm that the curtain is in the fully closed and suspended state.', + step2: 'click Next to Start calibration.', ), leftOnTap: () => Navigator.of(parentContext).pop(), rightOnTap: () { diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart index a087e5bb..c73899fc 100644 --- a/lib/pages/device_managment/device_setting/device_management_content.dart +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -19,11 +19,14 @@ class DeviceManagementContent extends StatelessWidget { required this.device, required this.subSpaces, required this.deviceInfo, + required this.deviceManagementBloc, }); final AllDevicesModel device; final List subSpaces; final DeviceInfoModel deviceInfo; + final DeviceManagementBloc deviceManagementBloc; + @override Widget build(BuildContext context) { @@ -87,6 +90,11 @@ class DeviceManagementContent extends StatelessWidget { ), ); }); + + deviceManagementBloc.add(UpdateSubSpaceName( + subspaceId: selectedSubSpace.id!, + deviceId: device.uuid!, + newSubSpaceName: selectedSubSpace.name ?? '')); } }, child: infoRow( diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart index 48458b3b..0856b5d0 100644 --- a/lib/pages/device_managment/device_setting/device_settings_panel.dart +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -17,7 +19,13 @@ import 'package:syncrow_web/web_layout/default_container.dart'; class DeviceSettingsPanel extends StatelessWidget { final VoidCallback? onClose; final AllDevicesModel device; - const DeviceSettingsPanel({super.key, this.onClose, required this.device}); + final DeviceManagementBloc deviceManagementBloc; + const DeviceSettingsPanel({ + super.key, + this.onClose, + required this.device, + required this.deviceManagementBloc, + }); @override Widget build(BuildContext context) { @@ -71,10 +79,10 @@ class DeviceSettingsPanel extends StatelessWidget { 'Device Settings', style: context.theme.textTheme.titleLarge! .copyWith( - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w700, color: ColorsManager.vividBlue .withOpacity(0.7), - fontSize: 24), + fontSize: 24), ), ], ), @@ -134,8 +142,14 @@ class DeviceSettingsPanel extends StatelessWidget { onFieldSubmitted: (value) { _bloc.add(const ChangeNameEvent( value: false)); + deviceManagementBloc + ..add(UpdateDeviceName( + deviceId: device.uuid!, + newName: _bloc + .nameController + .text))..add(ResetSelectedDevices()); }, - decoration: InputDecoration( + decoration:const InputDecoration( isDense: true, contentPadding: EdgeInsets.zero, border: InputBorder.none, @@ -157,7 +171,7 @@ class DeviceSettingsPanel extends StatelessWidget { onTap: () { _bloc.add( const ChangeNameEvent( - value: true)); + value: true)); }, child: SvgPicture.asset( Assets @@ -190,6 +204,7 @@ class DeviceSettingsPanel extends StatelessWidget { device: device, subSpaces: subSpaces.cast(), deviceInfo: deviceInfo, + deviceManagementBloc: deviceManagementBloc, ), const SizedBox(height: 32), RemoveDeviceWidget(bloc: _bloc), diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart index c1e976ab..bb6f8e29 100644 --- a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc emit(OneGangGlassSwitchLoading()); try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId, emit); + _listenToChanges(event.deviceId); deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { @@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc } } - void _listenToChanges( - String deviceId, - Emitter emit, - ) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; - stream.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + final usersMap = event.snapshot.value! as Map; final statusList = []; - if (data['status'] != null) { - for (var element in data['status']) { - statusList.add( - Status( - code: element['code'].toString(), - value: element['value'].toString(), - ), - ); - } - } - if (statusList.isNotEmpty) { - final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList); - if (newStatus != deviceStatus) { - deviceStatus = newStatus; - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - } - } + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - emit(OneGangGlassSwitchError('Failed to listen to changes: $e')); - } + } catch (_) {} } void _onStatusUpdated( @@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc deviceStatus = deviceStatus.copyWith(switch1: value); } } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); + } } diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 1ad5d43b..6b180f8d 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '1GT', ), )); }, diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index 2f6008d2..a9d7c47b 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '1G', ), )); }, diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 11d1cc8f..03202ba0 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -277,6 +277,32 @@ class SmartPowerDeviceControl extends StatelessWidget totalConsumption: 10000, date: blocProvider.formattedDate, ), + EnergyConsumptionPage( + formattedDate: + '${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}', + onTap: () { + blocProvider.add(SelectDateEvent(context: context)); + }, + widget: blocProvider.dateSwitcher(), + chartData: blocProvider.energyDataList.isNotEmpty + ? blocProvider.energyDataList + : [ + EnergyData('12:00 AM', 4.0), + EnergyData('01:00 AM', 6.5), + EnergyData('02:00 AM', 3.8), + EnergyData('03:00 AM', 3.2), + EnergyData('04:00 AM', 6.0), + EnergyData('05:00 AM', 3.4), + EnergyData('06:00 AM', 5.2), + EnergyData('07:00 AM', 3.5), + EnergyData('08:00 AM', 6.8), + EnergyData('09:00 AM', 5.6), + EnergyData('10:00 AM', 3.9), + EnergyData('11:00 AM', 4.0), + ], + totalConsumption: 10000, + date: blocProvider.formattedDate, + ), ], ), ), diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 0ec55e39..62bef920 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc { final success = await RemoteControlDeviceService().controlDevice( deviceUuid: deviceId, status: Status( - code: 'countdown_1', + code: event.countdownCode, value: 0, ), ); @@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc { ) { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownSeconds: currentState.countdownSeconds, + selectedTime: currentState.selectedTime, + deviceId: deviceId, scheduleMode: event.scheduleMode, - countdownRemaining: Duration.zero, - countdownHours: 0, - countdownMinutes: 0, - inchingHours: 0, - inchingMinutes: 0, - isCountdownActive: false, + countdownHours: currentState.countdownHours, + countdownMinutes: currentState.countdownMinutes, + inchingHours: currentState.inchingHours, + inchingMinutes: currentState.inchingMinutes, isInchingActive: false, + isCountdownActive: currentState.countdownRemaining > Duration.zero, )); } } @@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc { deviceId, event.category, ); - if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( @@ -230,7 +232,6 @@ class ScheduleBloc extends Bloc { selectedDays: List.filled(7, false), functionOn: false, isEditing: false, - countdownRemaining: Duration.zero, )); } else { emit(ScheduleLoaded( @@ -285,12 +286,22 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { + Status status = Status(code: '', value: ''); + if (event.deviceType == 'CUR_2') { + status = status.copyWith( + code: 'control', + value: event.functionOn == true ? 'open' : 'close'); + } else { + status = + status.copyWith(code: event.category, value: event.functionOn); + } + final dateTime = DateTime.parse(event.time); final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), - function: Status(code: event.category, value: event.functionOn), + function: status, days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( @@ -396,7 +407,7 @@ class ScheduleBloc extends Bloc { final totalSeconds = Duration(hours: event.hours, minutes: event.minutes).inSeconds; final code = event.mode == ScheduleModes.countdown - ? 'countdown_1' + ? event.countDownCode : 'switch_inching'; final currentState = state as ScheduleLoaded; final duration = Duration(seconds: totalSeconds); @@ -423,7 +434,7 @@ class ScheduleBloc extends Bloc { ); if (success) { - if (code == 'countdown_1') { + if (code == event.countDownCode) { final countdownDuration = Duration(seconds: totalSeconds); emit( @@ -437,7 +448,7 @@ class ScheduleBloc extends Bloc { ); if (countdownDuration.inSeconds > 0) { - _startCountdownTimer(emit, countdownDuration); + _startCountdownTimer(emit, countdownDuration, event.countDownCode); } else { _countdownTimer?.cancel(); emit( @@ -467,9 +478,7 @@ class ScheduleBloc extends Bloc { } void _startCountdownTimer( - Emitter emit, - Duration duration, - ) { + Emitter emit, Duration duration, String countdownCode) { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_currentCountdown != null && _currentCountdown! > Duration.zero) { @@ -479,6 +488,7 @@ class ScheduleBloc extends Bloc { } else { timer.cancel(); add(StopScheduleEvent( + countdownCode: countdownCode, mode: _currentCountdown == null ? ScheduleModes.countdown : ScheduleModes.inching, @@ -515,70 +525,75 @@ class ScheduleBloc extends Bloc { try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - print(status.status); + int totalSeconds = 0; + final countdownItem = status.status.firstWhere( + (item) => item.code == event.countdownCode, + orElse: () => Status(code: '', value: 0), + ); + totalSeconds = (countdownItem.value as int?) ?? 0; + final countdownHours = totalSeconds ~/ 3600; + final countdownMinutes = (totalSeconds % 3600) ~/ 60; + final countdownSeconds = totalSeconds % 60; + final deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final isCountdownActive = totalSeconds > 0; + final isInchingActive = !isCountdownActive && + (deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0); - final scheduleMode = - deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0 - ? ScheduleModes.countdown - : deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0 - ? ScheduleModes.inching - : ScheduleModes.schedule; - final isCountdown = scheduleMode == ScheduleModes.countdown; - final isInching = scheduleMode == ScheduleModes.inching; + final newState = state is ScheduleLoaded + ? (state as ScheduleLoaded).copyWith( + scheduleMode: ScheduleModes.schedule, + countdownHours: countdownHours, + countdownMinutes: countdownMinutes, + countdownSeconds: countdownSeconds, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: isCountdownActive + ? Duration(seconds: totalSeconds) + : Duration.zero, + ) + : ScheduleLoaded( + scheduleMode: ScheduleModes.schedule, + schedules: const [], + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: event.deviceId, + countdownHours: countdownHours, + countdownMinutes: countdownMinutes, + countdownSeconds: countdownSeconds, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: isCountdownActive + ? Duration(seconds: totalSeconds) + : Duration.zero, + ); + emit(newState); - Duration? countdownRemaining; - var isCountdownActive = false; - var isInchingActive = false; + if (isCountdownActive) { + _countdownTimer?.cancel(); + _currentCountdown = Duration(seconds: totalSeconds); + countdownRemaining = _currentCountdown!; - if (isCountdown) { - countdownRemaining = Duration( - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - ); - isCountdownActive = countdownRemaining > Duration.zero; - } else if (isInching) { - isInchingActive = Duration( - hours: deviceStatus.inchingHours, - minutes: deviceStatus.inchingMinutes, - ) > - Duration.zero; - } - if (state is ScheduleLoaded) { - final currentState = state as ScheduleLoaded; - emit(currentState.copyWith( - scheduleMode: scheduleMode, - countdownHours: deviceStatus.countdownHours, - countdownMinutes: deviceStatus.countdownMinutes, - inchingHours: deviceStatus.inchingHours, - inchingMinutes: deviceStatus.inchingMinutes, - isCountdownActive: isCountdownActive, - isInchingActive: isInchingActive, - countdownRemaining: countdownRemaining ?? Duration.zero, - )); + if (totalSeconds > 0) { + _startCountdownTimer( + emit, Duration(seconds: totalSeconds), event.countdownCode); + } else { + add(StopScheduleEvent( + countdownCode: event.countdownCode, + mode: ScheduleModes.countdown, + deviceId: event.deviceId, + )); + } } else { - emit(ScheduleLoaded( - schedules: const [], - selectedTime: null, - selectedDays: List.filled(7, false), - functionOn: false, - isEditing: false, - deviceId: deviceId, - scheduleMode: scheduleMode, - countdownHours: deviceStatus.countdownHours, - countdownMinutes: deviceStatus.countdownMinutes, - inchingHours: deviceStatus.inchingHours, - inchingMinutes: deviceStatus.inchingMinutes, - isCountdownActive: isCountdownActive, - isInchingActive: isInchingActive, - countdownRemaining: countdownRemaining ?? Duration.zero, - )); + _countdownTimer?.cancel(); } - - // if (isCountdownActive && countdownRemaining != null) { - // _startCountdownTimer(emit, countdownRemaining); - // } } catch (e) { emit(ScheduleError('Failed to fetch device status: $e')); } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index a28b8757..6c79c8b6 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent { final String time; final List selectedDays; final bool functionOn; + final String deviceType; const ScheduleEditEvent({ required this.scheduleId, @@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent { required this.time, required this.selectedDays, required this.functionOn, + required this.deviceType, }); @override @@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent { time, selectedDays, functionOn, + deviceType, ]; } @@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent { class UpdateScheduleModeEvent extends ScheduleEvent { final ScheduleModes scheduleMode; + final String countdownCode; - const UpdateScheduleModeEvent({required this.scheduleMode}); + const UpdateScheduleModeEvent( + {required this.scheduleMode, required this.countdownCode}); @override - List get props => [scheduleMode]; + List get props => [scheduleMode, countdownCode!]; } class UpdateCountdownTimeEvent extends ScheduleEvent { @@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent { final ScheduleModes mode; final int hours; final int minutes; + final String countDownCode; const StartScheduleEvent({ required this.mode, required this.hours, required this.minutes, + required this.countDownCode, }); @override - List get props => [mode, hours, minutes]; + List get props => [mode, hours, minutes, countDownCode]; } class StopScheduleEvent extends ScheduleEvent { final ScheduleModes mode; final String deviceId; + final String countdownCode; const StopScheduleEvent({ required this.mode, required this.deviceId, + required this.countdownCode, }); @override - List get props => [mode, deviceId]; + List get props => [mode, deviceId, countdownCode]; } class ScheduleDecrementCountdownEvent extends ScheduleEvent { @@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent { class ScheduleFetchStatusEvent extends ScheduleEvent { final String deviceId; + final String countdownCode; - const ScheduleFetchStatusEvent(this.deviceId); + const ScheduleFetchStatusEvent( + {required this.deviceId, required this.countdownCode}); @override - List get props => [deviceId]; + List get props => [deviceId, countdownCode]; } class DeleteScheduleEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 63551c3a..66547788 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState { final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; - final Duration? countdownRemaining; + final Duration countdownRemaining; final int? countdownSeconds; const ScheduleLoaded({ @@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState { this.inchingMinutes = 0, this.isInchingActive = false, this.scheduleMode = ScheduleModes.countdown, - this.countdownRemaining, + this.countdownRemaining = Duration.zero, }); ScheduleLoaded copyWith({ diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index 4919018c..b28a6a23 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget { final String deviceId; final int hours; final int minutes; + final String countDownCode; const CountdownModeButtons({ super.key, @@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget { required this.deviceId, required this.hours, required this.minutes, + required this.countDownCode, }); @override @@ -43,6 +45,7 @@ class CountdownModeButtons extends StatelessWidget { StopScheduleEvent( mode: ScheduleModes.countdown, deviceId: deviceId, + countdownCode: countDownCode, ), ); }, @@ -54,10 +57,10 @@ class CountdownModeButtons extends StatelessWidget { onPressed: () { context.read().add( StartScheduleEvent( - mode: ScheduleModes.countdown, - hours: hours, - minutes: minutes, - ), + mode: ScheduleModes.countdown, + hours: hours, + minutes: minutes, + countDownCode: countDownCode), ); }, backgroundColor: ColorsManager.primaryColor, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart index 418bab6c..e64b7cf7 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State { final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isActive = isCountDown ? state.isCountdownActive : state.isInchingActive; - final displayHours = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inHours - : (isCountDown ? state.countdownHours : state.inchingHours); - final displayMinutes = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inMinutes.remainder(60) - : (isCountDown ? state.countdownMinutes : state.inchingMinutes); - final displaySeconds = isActive && state.countdownRemaining != null - ? state.countdownRemaining!.inSeconds.remainder(60) - : (isCountDown ? state.countdownSeconds : state.inchingSeconds); - _updateControllers(displayHours, displayMinutes, displaySeconds!); + final displayHours = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inHours + : (isCountDown ? state.countdownHours : state.inchingHours); - if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) { + final displayMinutes = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inMinutes.remainder(60) + : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + + final displaySeconds = + isActive && state.countdownRemaining != Duration.zero + ? state.countdownRemaining.inSeconds.remainder(60) + : (isCountDown ? (state.countdownSeconds ?? 0) : 0); + + _updateControllers(displayHours, displayMinutes, displaySeconds); + + if (isActive && + displayHours == 0 && + displayMinutes == 0 && + displaySeconds == 0) { context.read().add( StopScheduleEvent( mode: ScheduleModes.countdown, deviceId: widget.deviceId, + countdownCode: '', ), ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index e75c5d46..e8dc5e79 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget { onPressed: () { context.read().add( StopScheduleEvent( - deviceId: deviceId, mode: ScheduleModes.inching), + deviceId: deviceId, + mode: ScheduleModes.inching, + countdownCode: ''), ); }, backgroundColor: Colors.red, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index c511b8bd..b654698d 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -18,11 +18,15 @@ class BuildScheduleView extends StatelessWidget { super.key, required this.deviceUuid, required this.category, + required this.countdownCode, this.code, + required this.deviceType, }); final String deviceUuid; final String category; final String? code; + final String? countdownCode; + final String deviceType; @override Widget build(BuildContext context) { @@ -31,7 +35,8 @@ class BuildScheduleView extends StatelessWidget { deviceId: deviceUuid, ) ..add(ScheduleGetEvent(category: category)) - ..add(ScheduleFetchStatusEvent(deviceUuid)), + ..add(ScheduleFetchStatusEvent( + deviceId: deviceUuid, countdownCode: countdownCode ?? '')), child: Dialog( backgroundColor: Colors.white, insetPadding: const EdgeInsets.all(20), @@ -52,28 +57,32 @@ class BuildScheduleView extends StatelessWidget { children: [ const ScheduleHeader(), const SizedBox(height: 20), - ScheduleModeSelector( - currentMode: state.scheduleMode, - ), + if (deviceType == 'CUR_2') + const SizedBox() + else + ScheduleModeSelector( + countdownCode: countdownCode ?? '', + currentMode: state.scheduleMode, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) ScheduleManagementUI( + deviceType: deviceType, category: category, deviceUuid: deviceUuid, onAddSchedule: () async { final entry = await ScheduleDialogHelper - .showAddScheduleDialog( - context, - schedule: ScheduleEntry( - category: category, - time: '', - function: Status( - code: code.toString(), value: null), - days: [], - ), - isEdit: false, - code: code, - ); + .showAddScheduleDialog(context, + schedule: ScheduleEntry( + category: category, + time: '', + function: Status( + code: code.toString(), value: null), + days: [], + ), + isEdit: false, + code: code, + deviceType: deviceType); if (entry != null) { context.read().add( ScheduleAddEvent( @@ -87,14 +96,16 @@ class BuildScheduleView extends StatelessWidget { } }, ), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) - CountdownInchingView( - deviceId: deviceUuid, - ), + if (deviceType != 'CUR_2') + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( + countDownCode: countdownCode ?? '', isActive: state.isCountdownActive, deviceId: deviceUuid, hours: state.countdownHours, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 8f871ce4..1a89c1ee 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -5,14 +5,16 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { - final String deviceUuid; + final String deviceUuid; final VoidCallback onAddSchedule; final String category; + final String deviceType; const ScheduleManagementUI({ super.key, required this.deviceUuid, required this.onAddSchedule, + required this.deviceType, this.category = 'switch_1', }); @@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(deviceUuid: deviceUuid, category: category), + ScheduleTableWidget( + deviceUuid: deviceUuid, + category: category, + deviceType: deviceType, + ), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart index 25bf7f2c..200d8c66 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleModeSelector extends StatelessWidget { final ScheduleModes currentMode; + final String countdownCode; const ScheduleModeSelector({ super.key, required this.currentMode, + required this.countdownCode, }); @override @@ -71,7 +73,8 @@ class ScheduleModeSelector extends StatelessWidget { onChanged: (ScheduleModes? value) { if (value != null) { context.read().add( - UpdateScheduleModeEvent(scheduleMode: value), + UpdateScheduleModeEvent( + scheduleMode: value, countdownCode: countdownCode), ); if (value == ScheduleModes.schedule) { context.read().add( diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index b23e48df..c1771c1b 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart'; class ScheduleTableWidget extends StatelessWidget { final String deviceUuid; final String category; + final String deviceType; const ScheduleTableWidget({ super.key, required this.deviceUuid, this.category = 'switch_1', + required this.deviceType, }); @override @@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget { create: (_) => ScheduleBloc( deviceId: deviceUuid, )..add(ScheduleGetEvent(category: category)), - child: _ScheduleTableView(), + child: _ScheduleTableView(deviceType), ); } } class _ScheduleTableView extends StatelessWidget { - const _ScheduleTableView(); + final String deviceType; + const _ScheduleTableView(this.deviceType); @override Widget build(BuildContext context) { @@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), - child: _buildTableBody(state.schedules, context)); + child: _buildTableBody(state.schedules, context, deviceType)); } if (state is ScheduleError) { return Center(child: Text(state.error)); @@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget { ); } - Widget _buildTableBody(List schedules, BuildContext context) { + Widget _buildTableBody( + List schedules, BuildContext context, String deviceType) { return SizedBox( height: 200, child: SingleChildScrollView( @@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget { defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ for (int i = 0; i < schedules.length; i++) - _buildScheduleRow(schedules[i], i, context), + _buildScheduleRow(schedules[i], i, context, + deviceType: deviceType), ], ), ), @@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget { } TableRow _buildScheduleRow( - ScheduleModel schedule, int index, BuildContext context) { + ScheduleModel schedule, int index, BuildContext context, + {required String deviceType}) { return TableRow( children: [ Center( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - bool temp; - if (schedule.category == 'CUR_2') { - temp = schedule.function.value == 'open' ? true : false; - } else { - temp = schedule.function.value as bool; - } context.read().add( ScheduleUpdateEntryEvent( category: schedule.category, scheduleId: schedule.scheduleId, - functionOn: temp, - // schedule.function.value, + functionOn: schedule.function.value, enable: !schedule.enable, ), ); @@ -195,10 +194,11 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - schedule.category == 'CUR_2' - ? Center( - child: Text(schedule.function.value == true ? 'open' : 'close')) - : Center(child: Text(schedule.function.value ? 'On' : 'Off')), + if (deviceType == 'CUR_2') + Center( + child: Text(schedule.function.value == true ? 'open' : 'close')) + else + Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, @@ -206,18 +206,27 @@ class _ScheduleTableView extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - ScheduleDialogHelper.showAddScheduleDialog( - context, - schedule: ScheduleEntry.fromScheduleModel(schedule), - isEdit: true, - ).then((updatedSchedule) { + ScheduleDialogHelper.showAddScheduleDialog(context, + schedule: ScheduleEntry.fromScheduleModel(schedule), + isEdit: true, + deviceType: deviceType) + .then((updatedSchedule) { if (updatedSchedule != null) { + bool temp; + if (deviceType == 'CUR_2') { + updatedSchedule.function.value == 'open' + ? temp = true + : temp = false; + } else { + temp = updatedSchedule.function.value; + } context.read().add( ScheduleEditEvent( + deviceType: deviceType, scheduleId: schedule.scheduleId, category: schedule.category, time: updatedSchedule.time, - functionOn: updatedSchedule.function.value, + functionOn: temp, selectedDays: updatedSchedule.days), ); } diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart index 766c3163..4a122345 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc emit(ThreeGangGlassSwitchLoading()); try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId, emit); + _listenToChanges(event.deviceId); deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); @@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc } } - void _listenToChanges( - String deviceId, - Emitter emit, - ) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; - stream.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + final usersMap = event.snapshot.value! as Map; final statusList = []; - if (data['status'] != null) { - for (var element in data['status']) { - statusList.add( - Status( - code: element['code'].toString(), - value: element['value'].toString(), - ), - ); - } - } - if (statusList.isNotEmpty) { - final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList); - if (newStatus != deviceStatus) { - deviceStatus = newStatus; - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - } - } + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e')); - } + } catch (_) {} } void _onStatusUpdated( @@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc break; } } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); + } } diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 72435b74..8878a159 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceId, + countdownCode: 'countdown_1', + deviceType: '3GT', ), )); }, @@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_2', deviceUuid: deviceId, + countdownCode: 'countdown_2', + deviceType: '3GT', ), )); }, @@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_3', deviceUuid: deviceId, + countdownCode: 'countdown_3', + deviceType: '3GT', ), )); }, diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 66784bd5..79b843a3 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: '3G', ), )); }, @@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_2', + countdownCode: 'countdown_2', + deviceType: '3G', ), )); }, @@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_3', + countdownCode: 'countdown_3', + deviceType: '3G', ), )); }, diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart index 8f82c198..08b40362 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -1,173 +1,177 @@ -import 'dart:async'; -import 'dart:developer'; + import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:firebase_database/firebase_database.dart'; -import 'package:flutter/foundation.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; -import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; -import 'package:syncrow_web/services/batch_control_devices_service.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; -import 'package:syncrow_web/services/devices_mang_api.dart'; + import 'package:bloc/bloc.dart'; + import 'package:equatable/equatable.dart'; + import 'package:firebase_database/firebase_database.dart'; + import 'package:flutter/foundation.dart'; + import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; + import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; + import 'package:syncrow_web/services/batch_control_devices_service.dart'; + import 'package:syncrow_web/services/control_device_service.dart'; + import 'package:syncrow_web/services/devices_mang_api.dart'; -part 'two_gang_glass_switch_event.dart'; -part 'two_gang_glass_switch_state.dart'; + part 'two_gang_glass_switch_event.dart'; + part 'two_gang_glass_switch_state.dart'; -class TwoGangGlassSwitchBloc - extends Bloc { - final String deviceId; - final ControlDeviceService controlDeviceService; - final BatchControlDevicesService batchControlDevicesService; + class TwoGangGlassSwitchBloc + extends Bloc { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - late TwoGangGlassStatusModel deviceStatus; + late TwoGangGlassStatusModel deviceStatus; - TwoGangGlassSwitchBloc({ - required this.deviceId, - required this.controlDeviceService, - required this.batchControlDevicesService, - }) : super(TwoGangGlassSwitchInitial()) { - on(_onFetchDeviceStatus); - on(_onControl); - on(_onBatchControl); - on(_onFetchBatchStatus); - on(_onFactoryReset); - on(_onStatusUpdated); - } - - Future _onFetchDeviceStatus( - TwoGangGlassSwitchFetchDeviceEvent event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); + TwoGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangGlassSwitchInitial()) { + on(_onFetchDeviceStatus); + on(_onControl); + on(_onBatchControl); + on(_onFetchBatchStatus); + on(_onFactoryReset); + on(_onStatusUpdated); } - } - void _listenToChanges(String deviceId) { - try { - final ref = FirebaseDatabase.instance.ref( - 'device-status/$deviceId', - ); - - ref.onValue.listen((event) { - final eventsMap = event.snapshot.value as Map; - - List statusList = []; - eventsMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList); - add(StatusUpdated(deviceStatus)); - }); - } catch (_) { - log( - 'Error listening to changes', - name: 'TwoGangGlassSwitchBloc._listenToChanges', - ); - } - } - - Future _onControl( - TwoGangGlassSwitchControl event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - _updateLocalValue(event.code, event.value); - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - - try { - await controlDeviceService.controlDevice( - deviceUuid: event.deviceId, - status: Status(code: event.code, value: event.value), - ); - } catch (e) { - _updateLocalValue(event.code, !event.value); - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onBatchControl( - TwoGangGlassSwitchBatchControl event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - _updateLocalValue(event.code, event.value); - emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - - try { - await batchControlDevicesService.batchControlDevices( - uuids: event.deviceIds, - code: event.code, - value: event.value, - ); - } catch (e) { - _updateLocalValue(event.code, !event.value); - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onFetchBatchStatus( - TwoGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = TwoGangGlassStatusModel.fromJson( - event.deviceIds.first, - status.status, - ); - emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); - } - } - - Future _onFactoryReset( - TwoGangGlassFactoryReset event, - Emitter emit, - ) async { - emit(TwoGangGlassSwitchLoading()); - try { - final response = await DevicesManagementApi().factoryReset( - event.factoryReset, - event.deviceId, - ); - if (!response) { - emit(TwoGangGlassSwitchError('Failed to reset device')); - } else { - add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); + Future _onFetchDeviceStatus( + TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + _listenToChanges(event.deviceId); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); } - } catch (e) { - emit(TwoGangGlassSwitchError(e.toString())); } - } - void _onStatusUpdated( - StatusUpdated event, - Emitter emit, - ) { - deviceStatus = event.deviceStatus; - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } + StreamSubscription? _deviceStatusSubscription; - void _updateLocalValue(String code, bool value) { - switch (code) { - case 'switch_1': - deviceStatus = deviceStatus.copyWith(switch1: value); - break; - case 'switch_2': - deviceStatus = deviceStatus.copyWith(switch2: value); - break; + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async { + if (event.snapshot.value == null) return; + + final usersMap = event.snapshot.value! as Map; + + final statusList = []; + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + deviceStatus = + TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList); + + add(StatusUpdated(deviceStatus)); + }); + } catch (_) {} } + + Future _onControl( + TwoGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onBatchControl( + TwoGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + _updateLocalValue(event.code, event.value); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onFetchBatchStatus( + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = TwoGangGlassStatusModel.fromJson( + event.deviceIds.first, + status.status, + ); + emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + Future _onFactoryReset( + TwoGangGlassFactoryReset event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(TwoGangGlassSwitchError('Failed to reset device')); + } else { + add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); + } + } catch (e) { + emit(TwoGangGlassSwitchError(e.toString())); + } + } + + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; + emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + } + } + + @override + Future close() { + _deviceStatusSubscription?.cancel(); + return super.close(); } } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 34b30dd3..90328896 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + deviceType: '2GT', + countdownCode: 'countdown_1', deviceUuid: deviceId, category: 'switch_1', ), @@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + deviceType: '2GT', + countdownCode: 'countdown_2', deviceUuid: deviceId, category: 'switch_2', ), diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index 849412f2..10909e8f 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_1', deviceUuid: deviceIds.first, + countdownCode: 'countdown_1', + deviceType: '2G', ), )); }, @@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget child: BuildScheduleView( category: 'switch_2', deviceUuid: deviceIds.first, + countdownCode: 'countdown_2', + deviceType: '2G', ), )); }, @@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget subtitle: 'Scheduling', iconPath: Assets.scheduling, ), - // FirmwareUpdateWidget( - // deviceId: deviceIds.first, - // version: 12, - // ), + FactoryResetWidget(callFactoryReset: () { context.read().add( TwoGangFactoryReset( diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index ac3fe579..0ff7d964 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: '2G', ), )); }, @@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: deviceId, category: 'switch_2', + countdownCode: 'countdown_2', + deviceType: '2G', ), )); }, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 389eac3f..0a65595e 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -18,14 +18,21 @@ class ScheduleDialogHelper { ScheduleEntry? schedule, bool isEdit = false, String? code, + required String deviceType, }) { + bool temp; + if (deviceType == 'CUR_2') { + temp = schedule!.function.value == 'open' ? true : false; + } else { + temp = schedule!.function.value; + } final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) : TimeOfDay.now(); final initialDays = schedule != null ? _convertDaysStringToBooleans(schedule.days) : List.filled(7, false); - bool? functionOn = schedule?.function.value ?? true; + bool? functionOn = temp; TimeOfDay selectedTime = initialTime; List selectedDays = List.of(initialDays); @@ -97,8 +104,7 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(schedule!.category, ctx, functionOn!, - (v) { + _buildFunctionSwitch(deviceType, ctx, functionOn!, (v) { setState(() => functionOn = v); }), ], @@ -114,32 +120,29 @@ class ScheduleDialogHelper { ), ), SizedBox( - width: 100, - child: ElevatedButton( - onPressed: () { - dynamic temp; - if (schedule?.category == 'CUR_2') { - temp = functionOn! ? 'open' : 'close'; - } else { - temp = functionOn; - } - print(temp); - final entry = ScheduleEntry( - category: schedule?.category ?? 'switch_1', - time: _formatTimeOfDayToISO(selectedTime), - function: Status( - code: code ?? 'switch_1', - value: temp, - // functionOn, - ), - days: _convertSelectedDaysToStrings(selectedDays), - scheduleId: schedule?.scheduleId, - ); - Navigator.pop(ctx, entry); - }, - child: const Text('Save'), - ), - ), + 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'), + )), ], ); }, diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 16eff86a..a0e39bfa 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget child: BuildScheduleView( deviceUuid: device.uuid ?? '', category: 'switch_1', + countdownCode: 'countdown_1', + deviceType: 'WH', ), )); }, diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index c1bcba6a..aa642e25 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -105,7 +105,7 @@ class HomeBloc extends Bloc { color: const Color(0xFF0026A2), ), HomeItemModel( - title: 'Devices Management', + title: 'Device Management', icon: Assets.devicesIcon, active: true, onPress: (context) { diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart index 54187152..72c4501c 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart @@ -455,7 +455,7 @@ class UsersBloc extends Bloc { Future checkEmail( CheckEmailEvent event, Emitter emit) async { emit(UsersLoadingState()); - String? res = await UserPermissionApi().checkEmail( + String? res = await UserPermissionApi().checkEmail( emailController.text, ); checkEmailValid = res!; diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart index 44ba81ff..501cd02e 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/add_user_dialog.dart @@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State { ), InkWell( onTap: () { + final isBasicsStep = currentStep == 1; + + if (isBasicsStep) { + // Validate the form first + final isValid = _blocRole.formKey.currentState + ?.validate() ?? + false; + + if (!isValid) + return; // Stop if form is not valid + } _blocRole.add(const CheckEmailEvent()); setState(() { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(const CheckStepStatus(isEditUser: false)); + _blocRole.add(const CheckStepStatus( + isEditUser: false)); } else if (currentStep == 3) { - _blocRole.add(const CheckSpacesStepStatus()); + _blocRole + .add(const CheckSpacesStepStatus()); } } else { - _blocRole.add(SendInviteUsers(context: context)); + _blocRole + .add(SendInviteUsers(context: context)); } }); }, @@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State { currentStep < 3 ? "Next" : "Save", style: TextStyle( color: (_blocRole.isCompleteSpaces == false || - _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteBasics == + false || + _blocRole + .isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -143,7 +162,7 @@ class _AddNewUserDialogState extends State { Widget _getFormContent() { switch (currentStep) { case 1: - return const BasicsView( + return BasicsView( userId: '', ); case 2: @@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart index fa04c051..14022cab 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/basics_view.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl_phone_field/countries.dart'; import 'package:intl_phone_field/country_picker_dialog.dart'; import 'package:intl_phone_field/intl_phone_field.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; +import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart'; class BasicsView extends StatelessWidget { final String? userId; - const BasicsView({super.key, this.userId = ''}); + Timer? _debounce; + + BasicsView({super.key, this.userId = ''}); @override Widget build(BuildContext context) { return BlocBuilder(builder: (context, state) { @@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget { } return Form( key: _blocRole.formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, child: ListView( shrinkWrap: true, children: [ @@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget { fontSize: 12, color: ColorsManager.textGray), ), + + onChanged: (value) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 800), () { + _blocRole.add(const CheckEmailEvent()); + }); + }, + validator: (value) { if (value == null || value.isEmpty) { return 'Enter Email Address'; diff --git a/lib/pages/routines/create_new_routines/space_dropdown.dart b/lib/pages/routines/create_new_routines/space_dropdown.dart index 0d2dc075..0605b7fc 100644 --- a/lib/pages/routines/create_new_routines/space_dropdown.dart +++ b/lib/pages/routines/create_new_routines/space_dropdown.dart @@ -32,113 +32,114 @@ class SpaceDropdown extends StatelessWidget { color: ColorsManager.blackColor, ), ), - SizedBox( - child: Container( + DropdownButton2( + underline: const SizedBox(), + buttonStyleData: ButtonStyleData( + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(12)), + ), + value: selectedValue, + items: spaces.map((space) { + return DropdownMenuItem( + value: space.uuid, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + ' ${space.name}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + color: selectedValue == space.uuid + ? ColorsManager.dialogBlueTitle + : ColorsManager.blackColor, + ), + ), + Text( + ' ${space.lastThreeParents}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 12, + color: selectedValue == space.uuid + ? ColorsManager.dialogBlueTitle + : ColorsManager.blackColor, + ), + ), + ], + ), + ); + }).toList(), + onChanged: onChanged, + style: TextStyle( + color: Colors.black, + fontSize: 13, + ), + hint: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + hintMessage, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ColorsManager.textGray, + ), + ), + ), + customButton: Container( height: 40, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.textGray, width: 1.0), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 8, + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedValue != null + ? spaces + .firstWhere((e) => e.uuid == selectedValue) + .name + : hintMessage, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 13, + color: selectedValue != null + ? Colors.black + : ColorsManager.textGray, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + height: 45, + child: const Icon( + Icons.keyboard_arrow_down, + color: ColorsManager.textGray, + ), + ), + ), + ], + ), + ), + dropdownStyleData: DropdownStyleData( + maxHeight: MediaQuery.of(context).size.height * 0.4, decoration: BoxDecoration( color: ColorsManager.whiteColors, borderRadius: BorderRadius.circular(10), ), - child: DropdownButton2( - underline: const SizedBox(), - value: selectedValue, - items: spaces.map((space) { - return DropdownMenuItem( - value: space.uuid, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - ' ${space.name}', - style: - Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - ' ${space.lastThreeParents}', - style: - Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 12, - ), - ), - ], - ), - ); - }).toList(), - onChanged: onChanged, - style: TextStyle(color: Colors.black), - hint: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - hintMessage, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.textGray, - ), - ), - ), - customButton: Container( - height: 45, - decoration: BoxDecoration( - border: - Border.all(color: ColorsManager.textGray, width: 1.0), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 8, - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - selectedValue != null - ? spaces - .firstWhere((e) => e.uuid == selectedValue) - .name - : hintMessage, - style: - Theme.of(context).textTheme.bodySmall!.copyWith( - color: selectedValue != null - ? Colors.black - : ColorsManager.textGray, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: const BorderRadius.only( - topRight: Radius.circular(10), - bottomRight: Radius.circular(10), - ), - ), - height: 45, - child: const Icon( - Icons.keyboard_arrow_down, - color: ColorsManager.textGray, - ), - ), - ), - ], - ), - ), - dropdownStyleData: DropdownStyleData( - maxHeight: MediaQuery.of(context).size.height * 0.4, - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(10), - ), - ), - menuItemStyleData: const MenuItemStyleData( - height: 60, - ), - ), + ), + menuItemStyleData: const MenuItemStyleData( + height: 60, ), ), ], diff --git a/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart b/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart index 4fc4bd0f..df21c93e 100644 --- a/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart +++ b/lib/pages/routines/widgets/main_routine_view/routine_view_card.dart @@ -121,7 +121,8 @@ class _RoutineViewCardState extends State { child: SizedBox( width: 16, height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + child: + CircularProgressIndicator(strokeWidth: 2), ), ), ) @@ -159,8 +160,9 @@ class _RoutineViewCardState extends State { height: iconSize, width: iconSize, fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => - Image.asset( + errorBuilder: + (context, error, stackTrace) => + Image.asset( Assets.logo, height: iconSize, width: iconSize, @@ -203,7 +205,8 @@ class _RoutineViewCardState extends State { maxLines: 1, style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: widget.isSmallScreenSize(context) ? 10 : 12, + fontSize: + widget.isSmallScreenSize(context) ? 10 : 12, ), ), if (widget.spaceName != '') @@ -222,8 +225,9 @@ class _RoutineViewCardState extends State { maxLines: 1, style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: - widget.isSmallScreenSize(context) ? 10 : 12, + fontSize: widget.isSmallScreenSize(context) + ? 10 + : 12, ), ), ], diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart index bdf8660d..64295e2a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -58,7 +58,9 @@ class CurtainHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('AC Functions'), + DialogHeader(dialogType == 'THEN' + ? 'Curtain Functions' + : 'Curtain Conditions'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart index 5322c3ea..e1981208 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -1,24 +1,39 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.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/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart'; abstract final class SpaceManagementCommunityDialogHelper { - static void showCreateDialog(BuildContext context) { + static void showCreateDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => const CreateCommunityDialog(), + ); + + static void showEditDialog( + BuildContext context, + CommunityModel community, + ) { showDialog( context: context, - builder: (_) => CreateCommunityDialog( - title: const SelectableText('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, + builder: (_) => EditCommunityDialog( + community: community, + parentContext: context, ), ); } + + static void showLoadingDialog(BuildContext context) => showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + + static void showSuccessSnackBar(BuildContext context, String message) => + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + ), + ); } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart similarity index 55% rename from lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart rename to lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart index ab9f7b9a..32f6f39c 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart +++ b/lib/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart @@ -1,28 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class CreateCommunityDialogWidget extends StatefulWidget { +class CommunityDialog extends StatefulWidget { final String? initialName; final Widget title; + final void Function(String name) onSubmit; + final String? errorMessage; - const CreateCommunityDialogWidget({ - super.key, + const CommunityDialog({ required this.title, + required this.onSubmit, this.initialName, + this.errorMessage, + super.key, }); @override - State createState() => - _CreateCommunityDialogWidgetState(); + State createState() => _CommunityDialogState(); } -class _CreateCommunityDialogWidgetState extends State { +class _CommunityDialogState extends State { late final TextEditingController _nameController; @override @@ -63,35 +64,20 @@ class _CreateCommunityDialogWidgetState extends State( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context).textTheme.headlineMedium!, - child: widget.title, - ), - const SizedBox(height: 18), - CreateCommunityNameTextField( - nameController: _nameController, - ), - if (state case CreateCommunityFailure(:final message)) - Padding( - padding: const EdgeInsets.only(top: 18), - child: SelectableText( - '* $message', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ), - ), - const SizedBox(height: 24), - _buildActionButtons(context), - ], - ); - }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField(nameController: _nameController), + _buildErrorMessage(), + const SizedBox(height: 24), + _buildActionButtons(context), + ], ), ), ), @@ -132,13 +118,22 @@ class _CreateCommunityDialogWidgetState extends State().add( - CreateCommunity( - CreateCommunityParam( - name: _nameController.text.trim(), - ), - ), - ); + widget.onSubmit.call(_nameController.text.trim()); } } + + Widget _buildErrorMessage() { + return Visibility( + visible: widget.errorMessage != null, + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 18), + child: SelectableText( + '* ${widget.errorMessage}', + style: context.textTheme.bodyMedium?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + ); + } } diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 957be65a..106b9a3a 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -7,6 +7,11 @@ 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'; 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'; @@ -26,6 +31,18 @@ class SpaceManagementPage extends StatelessWidget { )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + BlocProvider( + create: (context) => SpaceDetailsBloc( + UniqueSubspacesDecorator( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + ), + ), + BlocProvider( + create: (context) => ProductsBloc( + RemoteProductsService(HTTPService()), + ), + ), ], child: WebScaffold( appBarTitle: Text( diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart new file mode 100644 index 00000000..4f71075b --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeader extends StatelessWidget { + const CommunityStructureHeader({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenWidth = MediaQuery.of(context).size.width; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + boxShadow: [ + BoxShadow( + color: ColorsManager.shadowBlackColor, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildCommunityInfo(context, theme, screenWidth), + ), + const SizedBox(width: 16), + ], + ), + ], + ), + ); + } + + Widget _buildCommunityInfo( + BuildContext context, ThemeData theme, double screenWidth) { + final selectedCommunity = + context.watch().state.selectedCommunity; + final selectedSpace = + context.watch().state.selectedSpace; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Community Structure', + style: theme.textTheme.headlineLarge + ?.copyWith(color: ColorsManager.blackColor), + ), + if (selectedCommunity != null) + Row( + children: [ + Expanded( + child: Row( + children: [ + Flexible( + child: SelectableText( + selectedCommunity.name, + style: theme.textTheme.bodyLarge + ?.copyWith(color: ColorsManager.blackColor), + maxLines: 1, + ), + ), + const SizedBox(width: 2), + GestureDetector( + onTap: () { + SpaceManagementCommunityDialogHelper.showEditDialog( + context, + selectedCommunity, + ); + }, + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + CommunityStructureHeaderActionButtons( + onDelete: (space) {}, + onDuplicate: (space) {}, + onEdit: (space) { + SpaceDetailsDialogHelper.showEdit( + context, + spaceModel: selectedSpace!, + ); + }, + selectedSpace: selectedSpace, + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart new file mode 100644 index 00000000..a965c866 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CommunityStructureHeaderActionButtons extends StatelessWidget { + const CommunityStructureHeaderActionButtons({ + super.key, + required this.onDelete, + required this.selectedSpace, + required this.onDuplicate, + required this.onEdit, + }); + + final void Function(SpaceModel space) onDelete; + final void Function(SpaceModel space) onDuplicate; + final void Function(SpaceModel space) onEdit; + final SpaceModel? selectedSpace; + + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.end, + spacing: 10, + children: [ + if (selectedSpace != null) ...[ + CommunityStructureHeaderButton( + label: 'Edit', + svgAsset: Assets.editSpace, + onPressed: () => onEdit(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Duplicate', + svgAsset: Assets.duplicate, + onPressed: () => onDuplicate(selectedSpace!), + ), + CommunityStructureHeaderButton( + label: 'Delete', + svgAsset: Assets.spaceDelete, + onPressed: () => onDelete(selectedSpace!), + ), + ], + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart new file mode 100644 index 00000000..4c0285e3 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityStructureHeaderButton extends StatelessWidget { + const CommunityStructureHeaderButton({ + super.key, + required this.label, + required this.onPressed, + this.svgAsset, + }); + + final String label; + final VoidCallback onPressed; + final String? svgAsset; + + @override + Widget build(BuildContext context) { + const double buttonHeight = 40; + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 130, + minHeight: buttonHeight, + ), + child: DefaultButton( + onPressed: onPressed, + borderWidth: 2, + backgroundColor: ColorsManager.textFieldGreyColor, + foregroundColor: ColorsManager.blackColor, + borderRadius: 12.0, + padding: 2.0, + height: buttonHeight, + elevation: 0, + borderColor: ColorsManager.lightGrayColor, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (svgAsset != null) + SvgPicture.asset( + svgAsset!, + width: 20, + height: 20, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + label, + style: context.textTheme.bodySmall + ?.copyWith(color: ColorsManager.blackColor, fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 99d0668a..e1f1fc00 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; @@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget { replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer], ), - child: CommunityStructureCanvas( - community: selectedCommunity, - selectedSpace: selectedSpace, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CommunityStructureHeader(), + Expanded( + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index 37f131b3..c2489bf6 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -39,6 +39,26 @@ class CommunityModel extends Equatable { .toList(); } + CommunityModel copyWith({ + String? uuid, + String? name, + DateTime? createdAt, + DateTime? updatedAt, + String? description, + String? externalId, + List? spaces, + }) { + return CommunityModel( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + description: description ?? this.description, + externalId: externalId ?? this.externalId, + spaces: spaces ?? this.spaces, + ); + } + @override List get props => [uuid, name, spaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 36943adb..ddcc6a86 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -19,6 +19,16 @@ class SpaceModel extends Equatable { required this.parent, }); + factory SpaceModel.empty() => const SpaceModel( + uuid: '', + createdAt: null, + updatedAt: null, + spaceName: '', + icon: '', + children: [], + parent: null, + ); + factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String? ?? '', diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 9094a632..245448ea 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -16,6 +16,7 @@ class CommunitiesBloc extends Bloc { on(_onLoadCommunities); on(_onLoadMoreCommunities); on(_onInsertCommunity); + on(_onCommunitiesUpdateCommunity); } final CommunitiesService _communitiesService; @@ -114,4 +115,18 @@ class CommunitiesBloc extends Bloc { ) { emit(state.copyWith(communities: [event.community, ...state.communities])); } + + void _onCommunitiesUpdateCommunity( + CommunitiesUpdateCommunity event, + Emitter emit, + ) { + final updatedCommunities = state.communities + .map((e) => e.uuid == event.community.uuid ? event.community : e) + .toList(); + emit( + state.copyWith( + communities: updatedCommunities, + ), + ); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index cd14fa3d..9f8d1126 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -31,3 +31,12 @@ final class InsertCommunity extends CommunitiesEvent { @override List get props => [community]; } + +final class CommunitiesUpdateCommunity extends CommunitiesEvent { + const CommunitiesUpdateCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index a9af44d6..299c0078 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -1,57 +1,58 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.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/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class CreateCommunityDialog extends StatelessWidget { - final void Function(CommunityModel community) onCreateCommunity; - final String? initialName; - final Widget title; - - const CreateCommunityDialog({ - super.key, - required this.onCreateCommunity, - required this.title, - this.initialName, - }); + const CreateCommunityDialog({super.key}); @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), - child: BlocListener( + create: (_) => CreateCommunityBloc( + RemoteCreateCommunityService(HTTPService()), + ), + child: BlocConsumer( listener: (context, state) { switch (state) { - case CreateCommunityLoading(): - showDialog( - context: context, - builder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); + case CreateCommunityLoading() || CreateCommunityInitial(): + SpaceManagementCommunityDialogHelper.showLoadingDialog(context); break; case CreateCommunitySuccess(:final community): Navigator.of(context).pop(); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Community created successfully')), + SpaceManagementCommunityDialogHelper.showSuccessSnackBar( + context, + '${community.name} community created successfully', ); - onCreateCommunity.call(community); + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); break; case CreateCommunityFailure(): Navigator.of(context).pop(); break; - default: - break; } }, - child: CreateCommunityDialogWidget( - title: title, - initialName: initialName, - ), + builder: (BuildContext context, CreateCommunityState state) { + return CommunityDialog( + title: const Text('Create Community'), + initialName: null, + onSubmit: (name) => context.read().add( + CreateCommunity(CreateCommunityParam(name: name)), + ), + errorMessage: state is CreateCommunityFailure ? state.message : null, + ); + }, ), ); } diff --git a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart index 6e501b44..a01419fe 100644 --- a/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart +++ b/lib/pages/space_management_v2/modules/products/data/services/remote_products_service.dart @@ -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> getProducts(LoadProductsParam param) async { + Future> 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; + final products = json['data'] as List; + return products .map((e) => Product.fromJson(e as Map)) .toList(); }, diff --git a/lib/pages/space_management_v2/modules/products/domain/models/product.dart b/lib/pages/space_management_v2/modules/products/domain/models/product.dart index cd837121..1a505bc5 100644 --- a/lib/pages/space_management_v2/modules/products/domain/models/product.dart +++ b/lib/pages/space_management_v2/modules/products/domain/models/product.dart @@ -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 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 get props => [uuid, name]; + List get props => [uuid, name, productType]; } diff --git a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart b/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart deleted file mode 100644 index 87194ae7..00000000 --- a/lib/pages/space_management_v2/modules/products/domain/params/load_products_param.dart +++ /dev/null @@ -1,11 +0,0 @@ -class LoadProductsParam { - final String spaceUuid; - final String? type; - final String? status; - - const LoadProductsParam({ - required this.spaceUuid, - this.type, - this.status, - }); -} diff --git a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart index 18554382..f6d41d0c 100644 --- a/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart +++ b/lib/pages/space_management_v2/modules/products/domain/services/products_service.dart @@ -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> getProducts(LoadProductsParam param); + Future> getProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart index 1ce6ae89..0e85f1c7 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart @@ -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 { - final ProductsService _deviceService; - - ProductsBloc(this._deviceService) : super(ProductsInitial()) { + ProductsBloc(this._productsService) : super(ProductsInitial()) { on(_onLoadProducts); } + final ProductsService _productsService; + Future _onLoadProducts( LoadProducts event, Emitter 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) { diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart index 971b6d27..7bc14795 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_event.dart @@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable { } final class LoadProducts extends ProductsEvent { - const LoadProducts(this.param); - - final LoadProductsParam param; - - @override - List get props => [param]; + const LoadProducts(); } diff --git a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart index d5622cd3..78cee7ab 100644 --- a/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart +++ b/lib/pages/space_management_v2/modules/products/presentation/bloc/products_state.dart @@ -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 get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart index 2e999361..8b40cbfb 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart @@ -1,6 +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/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -15,12 +16,15 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { static const _defaultErrorMessage = 'Failed to load space details'; @override - Future getSpaceDetails(LoadSpacesParam param) async { + Future getSpaceDetails(LoadSpaceDetailsParam param) async { try { final response = await _httpService.get( - path: 'endpoint', + path: await _makeEndpoint(param), expectedResponseModel: (data) { - return SpaceDetailsModel.fromJson(data as Map); + final response = data as Map; + return SpaceDetailsModel.fromJson( + response['data'] as Map, + ); }, ); return response; @@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService { throw APIException(formattedErrorMessage); } } + + Future _makeEndpoint(LoadSpaceDetailsParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}'; + } } diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart b/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart new file mode 100644 index 00000000..8309c545 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart @@ -0,0 +1,27 @@ +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/domain/services/space_details_service.dart'; + +class UniqueSubspacesDecorator implements SpaceDetailsService { + final SpaceDetailsService _decoratee; + + const UniqueSubspacesDecorator(this._decoratee); + + @override + Future getSpaceDetails(LoadSpaceDetailsParam param) async { + final response = await _decoratee.getSpaceDetails(param); + + final uniqueSubspaces = {}; + + for (final subspace in response.subspaces) { + final normalizedName = subspace.name.trim().toLowerCase(); + if (!uniqueSubspaces.containsKey(normalizedName)) { + uniqueSubspaces[normalizedName] = subspace; + } + } + + return response.copyWith( + subspaces: uniqueSubspaces.values.toList(), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index 891e7eb2..b3e436b1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -1,6 +1,8 @@ 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; @@ -17,6 +19,13 @@ class SpaceDetailsModel extends Equatable { required this.subspaces, }); + factory SpaceDetailsModel.empty() => const SpaceDetailsModel( + uuid: '', + spaceName: '', + icon: Assets.location, + productAllocations: [], + subspaces: [], + ); factory SpaceDetailsModel.fromJson(Map json) { return SpaceDetailsModel( uuid: json['uuid'] as String, @@ -41,23 +50,40 @@ class SpaceDetailsModel extends Equatable { }; } + SpaceDetailsModel copyWith({ + String? uuid, + String? spaceName, + String? icon, + List? productAllocations, + List? subspaces, + }) { + return SpaceDetailsModel( + uuid: uuid ?? this.uuid, + spaceName: spaceName ?? this.spaceName, + icon: icon ?? this.icon, + productAllocations: productAllocations ?? this.productAllocations, + subspaces: subspaces ?? this.subspaces, + ); + } + @override List get props => [uuid, spaceName, icon, productAllocations, subspaces]; } class ProductAllocation extends Equatable { + final String uuid; final Product product; final Tag tag; - final String? location; const ProductAllocation({ + required this.uuid, required this.product, required this.tag, - this.location, }); factory ProductAllocation.fromJson(Map json) { return ProductAllocation( + uuid: json['uuid'] as String? ?? const Uuid().v4(), product: Product.fromJson(json['product'] as Map), tag: Tag.fromJson(json['tag'] as Map), ); @@ -65,13 +91,26 @@ class ProductAllocation extends Equatable { Map toJson() { return { + 'uuid': uuid, 'product': product.toJson(), 'tag': tag.toJson(), }; } + ProductAllocation copyWith({ + String? uuid, + Product? product, + Tag? tag, + }) { + return ProductAllocation( + uuid: uuid ?? this.uuid, + product: product ?? this.product, + tag: tag ?? this.tag, + ); + } + @override - List get props => [product, tag]; + List get props => [uuid, product, tag]; } class Subspace extends Equatable { @@ -88,7 +127,7 @@ class Subspace extends Equatable { factory Subspace.fromJson(Map json) { return Subspace( uuid: json['uuid'] as String, - name: json['name'] as String, + name: json['subspaceName'] as String, productAllocations: (json['productAllocations'] as List) .map((e) => ProductAllocation.fromJson(e as Map)) .toList(), @@ -103,6 +142,18 @@ class Subspace extends Equatable { }; } + Subspace copyWith({ + String? uuid, + String? name, + List? productAllocations, + }) { + return Subspace( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + productAllocations: productAllocations ?? this.productAllocations, + ); + } + @override List get props => [uuid, name, productAllocations]; } diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart new file mode 100644 index 00000000..7242e62e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart @@ -0,0 +1,9 @@ +class LoadSpaceDetailsParam { + const LoadSpaceDetailsParam({ + required this.spaceUuid, + required this.communityUuid, + }); + + final String spaceUuid; + final String communityUuid; +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart b/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart deleted file mode 100644 index 5324ed98..00000000 --- a/lib/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart +++ /dev/null @@ -1,3 +0,0 @@ -class LoadSpacesParam { - const LoadSpacesParam(); -} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart index b032560b..16b09ff1 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart @@ -1,6 +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/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; abstract class SpaceDetailsService { - Future getSpaceDetails(LoadSpacesParam param); + Future getSpaceDetails(LoadSpaceDetailsParam param); } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart index 59c1a06d..d397daf9 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart @@ -1,7 +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/space_details/domain/params/load_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -9,12 +9,13 @@ part 'space_details_event.dart'; part 'space_details_state.dart'; class SpaceDetailsBloc extends Bloc { - final SpaceDetailsService _spaceDetailsService; - SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) { on(_onLoadSpaceDetails); + on(_onClearSpaceDetails); } + final SpaceDetailsService _spaceDetailsService; + Future _onLoadSpaceDetails( LoadSpaceDetails event, Emitter emit, @@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc { emit(SpaceDetailsFailure(e.toString())); } } + + void _onClearSpaceDetails( + ClearSpaceDetails event, + Emitter emit, + ) { + emit(SpaceDetailsInitial()); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart index fe559e26..9dd40fba 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_event.dart @@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable { List get props => []; } -class LoadSpaceDetails extends SpaceDetailsEvent { +final class LoadSpaceDetails extends SpaceDetailsEvent { const LoadSpaceDetails(this.param); - final LoadSpacesParam param; + final LoadSpaceDetailsParam param; @override List get props => [param]; } + +final class ClearSpaceDetails extends SpaceDetailsEvent { + const ClearSpaceDetails(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart index c7378f89..53c4cf77 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_state.dart @@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState { } final class SpaceDetailsFailure extends SpaceDetailsState { - final String message; + final String errorMessage; - const SpaceDetailsFailure(this.message); + const SpaceDetailsFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index e871f4d0..de2c40f0 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,11 +1,46 @@ 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/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/services/api/http_service.dart'; abstract final class SpaceDetailsDialogHelper { static void showCreate(BuildContext context) { showDialog( context: context, - builder: (context) => const SpaceDetailsDialog(), + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: (space) {}, + ), + ), + ); + } + + static void showEdit( + BuildContext context, { + required SpaceModel spaceModel, + }) { + showDialog( + context: context, + builder: (_) => BlocProvider( + create: (context) => SpaceDetailsBloc( + RemoteSpaceDetailsService(httpService: HTTPService()), + ), + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Edit Space'), + spaceModel: spaceModel, + onSave: (space) {}, + ), + ), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart new file mode 100644 index 00000000..4c95634e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ButtonContentWidget extends StatelessWidget { + final String label; + final String? svgAssets; + final bool disabled; + + const ButtonContentWidget({ + required this.label, + this.svgAssets, + this.disabled = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Opacity( + opacity: disabled ? 0.5 : 1.0, + child: Container( + width: screenWidth * 0.25, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + border: Border.all( + color: ColorsManager.neutralGray, + width: 3.0, + ), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Row( + children: [ + if (svgAssets != null) + Padding( + padding: const EdgeInsets.only(left: 6.0), + child: SvgPicture.asset( + svgAssets!, + width: screenWidth * 0.015, + height: screenWidth * 0.015, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle( + color: ColorsManager.blackColor, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart new file mode 100644 index 00000000..8d7d2e29 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceDetailsActionButtons extends StatelessWidget { + const SpaceDetailsActionButtons({ + 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) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 10, + children: [ + Expanded(child: _buildCancelButton(context)), + Expanded(child: _buildSaveButton()), + ], + ); + } + + Widget _buildCancelButton(BuildContext context) { + return CancelButton(onPressed: onCancel, label: cancelButtonLabel); + } + + Widget _buildSaveButton() { + return DefaultButton( + onPressed: onSave, + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: ColorsManager.whiteColors, + child: Text(saveButtonLabel), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart new file mode 100644 index 00000000..cf65dbb6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart @@ -0,0 +1,139 @@ +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({ + required this.space, + super.key, + }); + + final SpaceDetailsModel space; + + @override + Widget build(BuildContext context) { + final allAllocations = [ + ...space.productAllocations, + ...space.subspaces.expand((s) => s.productAllocations), + ]; + + if (allAllocations.isNotEmpty) { + final productCounts = {}; + 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), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...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: () => _showAssignTagsDialog(context), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Add Devices', + ), + ), + ); + } + } + + void _showAssignTagsDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AssignTagsDialog(space: space), + ).then((resultSpace) { + if (resultSpace != null) { + if (context.mounted) { + context.read().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, + }; +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart index 7213c99e..ae772036 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -1,12 +1,105 @@ 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'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class SpaceDetailsDialog extends StatelessWidget { - const SpaceDetailsDialog({super.key}); +class SpaceDetailsDialog extends StatefulWidget { + const SpaceDetailsDialog({ + required this.title, + required this.spaceModel, + required this.onSave, + required this.context, + super.key, + }); + + final Widget title; + final SpaceModel spaceModel; + final void Function(SpaceDetailsModel space) onSave; + final BuildContext context; + + @override + State createState() => _SpaceDetailsDialogState(); +} + +class _SpaceDetailsDialogState extends State { + @override + void initState() { + final isCreateMode = widget.spaceModel.uuid.isEmpty; + + if (!isCreateMode) { + final param = LoadSpaceDetailsParam( + spaceUuid: widget.spaceModel.uuid, + communityUuid: widget.context + .read() + .state + .selectedCommunity! + .uuid, + ); + widget.context.read().add(LoadSpaceDetails(param)); + } + super.initState(); + } @override Widget build(BuildContext context) { - return const Dialog( - child: Text('Create Space'), + final isCreateMode = widget.spaceModel.uuid.isEmpty; + if (isCreateMode) { + return SpaceDetailsForm( + title: widget.title, + space: SpaceDetailsModel.empty(), + onSave: widget.onSave, + ); + } + + return BlocBuilder( + bloc: widget.context.read(), + builder: (context, state) => switch (state) { + SpaceDetailsInitial() => _buildLoadingDialog(), + SpaceDetailsLoading() => _buildLoadingDialog(), + SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm( + title: widget.title, + space: spaceDetails, + onSave: widget.onSave, + ), + SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog( + errorMessage, + ), + }, + ); + } + + Widget _buildLoadingDialog() { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.3, + width: context.screenWidth * 0.5, + child: const Center(child: CircularProgressIndicator()), + ), + ); + } + + Widget _buildErrorDialog(String errorMessage) { + return AlertDialog( + title: widget.title, + backgroundColor: ColorsManager.whiteColors, + content: Center( + child: SelectableText( + errorMessage, + style: context.textTheme.bodyLarge?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), ); } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart new file mode 100644 index 00000000..e4007511 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceDetailsForm extends StatelessWidget { + const SpaceDetailsForm({ + required this.title, + required this.space, + required this.onSave, + super.key, + }); + + final Widget title; + final SpaceDetailsModel space; + final void Function(SpaceDetailsModel space) onSave; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SpaceDetailsModelBloc(initialState: space), + child: BlocBuilder( + buildWhen: (previous, current) => previous != current, + builder: (context, space) { + return AlertDialog( + title: title, + backgroundColor: ColorsManager.whiteColors, + content: SizedBox( + height: context.screenHeight * 0.3, + width: context.screenWidth * 0.5, + child: Row( + spacing: 20, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: SpaceIconPicker(iconPath: space.icon)), + Expanded( + flex: 2, + child: ListView( + shrinkWrap: true, + children: [ + SpaceNameTextField( + initialValue: space.spaceName, + isNameFieldExist: (value) => space.subspaces.any( + (subspace) => subspace.name == value, + ), + ), + const SizedBox(height: 32), + SpaceSubSpacesBox( + subspaces: space.subspaces, + ), + const SizedBox(height: 16), + SpaceDetailsDevicesBox(space: space), + ], + ), + ), + ], + ), + ), + actions: [ + SpaceDetailsActionButtons( + onSave: () => onSave(space), + onCancel: Navigator.of(context).pop, + ), + ], + ); + }), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart new file mode 100644 index 00000000..68bfdefc --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_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/extension/build_context_x.dart'; + +class SpaceIconPicker extends StatelessWidget { + const SpaceIconPicker({ + required this.iconPath, + super.key, + }); + + final String iconPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Stack( + + alignment: Alignment.center, + children: [ + Container( + width: context.screenWidth * 0.175, + height: context.screenHeight * 0.175, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(24), + child: SvgPicture.asset( + iconPath, + width: context.screenWidth * 0.08, + height: context.screenHeight * 0.08, + ), + ), + Positioned.directional( + top: 12, + start: context.screenHeight * 0.06, + textDirection: Directionality.of(context), + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) => SpaceIconSelectionDialog( + selectedIcon: iconPath, + ), + ).then((value) { + if (value != null) { + if (context.mounted) { + context.read().add( + UpdateSpaceDetailsIcon(value), + ); + } + } + }); + }, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart new file mode 100644 index 00000000..5fe5b463 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceIconSelectionDialog extends StatelessWidget { + const SpaceIconSelectionDialog({super.key, required this.selectedIcon}); + final String selectedIcon; + + static const List _icons = [ + Assets.location, + Assets.villa, + Assets.gym, + Assets.sauna, + Assets.bbq, + Assets.building, + Assets.desk, + Assets.door, + Assets.parking, + Assets.pool, + Assets.stair, + Assets.steamRoom, + Assets.street, + Assets.unit, + ]; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: SelectableText( + 'Space Icon', + style: context.textTheme.headlineMedium, + ), + backgroundColor: ColorsManager.whiteColors, + content: Container( + width: context.screenWidth * 0.45, + height: context.screenHeight * 0.275, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(12), + ), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + crossAxisSpacing: 8, + mainAxisSpacing: 16, + ), + itemCount: _icons.length, + itemBuilder: (context, index) { + final isSelected = selectedIcon == _icons[index]; + return Container( + padding: const EdgeInsetsDirectional.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isSelected + ? Border.all(color: ColorsManager.vividBlue, width: 2) + : null, + ), + child: IconButton( + onPressed: () => Navigator.of(context).pop(_icons[index]), + icon: SvgPicture.asset( + _icons[index], + width: context.screenWidth * 0.03, + height: context.screenHeight * 0.08, + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart new file mode 100644 index 00000000..0c62490f --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceNameTextField extends StatefulWidget { + const SpaceNameTextField({ + required this.initialValue, + required this.isNameFieldExist, + super.key, + }); + + final String? initialValue; + final bool Function(String value) isNameFieldExist; + + @override + State createState() => _SpaceNameTextFieldState(); +} + +class _SpaceNameTextFieldState extends State { + late final TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(text: widget.initialValue); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return '*Space name should not be empty.'; + } + if (widget.isNameFieldExist(value)) { + return '*Name already exists'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + controller: _controller, + onChanged: (value) => context.read().add( + UpdateSpaceDetailsName(value), + ), + validator: _validateName, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.lightGrayColor, + ), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(context, ColorsManager.vividBlue), + focusedBorder: _buildBorder(context, ColorsManager.primaryColor), + errorBorder: _buildBorder(context, context.theme.colorScheme.error), + focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error), + errorStyle: context.textTheme.bodySmall?.copyWith( + color: context.theme.colorScheme.error, + ), + ), + ), + ); + } + + OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart new file mode 100644 index 00000000..68bf68bd --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/edit_chip.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; +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'; + +class SpaceSubSpacesBox extends StatelessWidget { + const SpaceSubSpacesBox({super.key, required this.subspaces}); + + final List subspaces; + + @override + Widget build(BuildContext context) { + if (subspaces.isEmpty) { + return TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: () => _showSubSpacesDialog(context), + child: const SizedBox( + width: double.infinity, + child: ButtonContentWidget( + svgAssets: Assets.addIcon, + label: 'Create Sub Spaces', + ), + ), + ); + } else { + return Container( + padding: const EdgeInsets.all(8.0), + width: double.infinity, + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)), + EditChip( + onTap: () => _showSubSpacesDialog(context), + ), + ], + ), + ); + } + } + + void _showSubSpacesDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => SpaceSubSpacesDialog( + subspaces: subspaces, + onSave: (subspaces) { + context.read().add( + UpdateSpaceDetailsSubspaces(subspaces), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart new file mode 100644 index 00000000..9e81c323 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart'; +import 'package:uuid/uuid.dart'; + +class SpaceSubSpacesDialog extends StatefulWidget { + const SpaceSubSpacesDialog({ + required this.subspaces, + required this.onSave, + super.key, + }); + + final List subspaces; + final void Function(List subspaces) onSave; + + @override + State createState() => _SpaceSubSpacesDialogState(); +} + +class _SpaceSubSpacesDialogState extends State { + late List _subspaces; + + bool get _hasDuplicateNames => + _subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length != + _subspaces.length; + + @override + void initState() { + super.initState(); + _subspaces = List.from(widget.subspaces); + } + + void _handleSubspaceAdded(String name) { + setState(() { + _subspaces = [ + ..._subspaces, + Subspace( + name: name, + uuid: const Uuid().v4(), + productAllocations: const [], + ), + ]; + }); + } + + void _handleSubspaceDeleted(String uuid) => setState( + () => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(), + ); + + void _handleSave() { + widget.onSave(_subspaces); + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const SelectableText('Create Sub Spaces'), + content: Column( + spacing: 12, + mainAxisSize: MainAxisSize.min, + children: [ + SubSpacesInput( + subSpaces: _subspaces, + onSubspaceAdded: _handleSubspaceAdded, + onSubspaceDeleted: _handleSubspaceDeleted, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: Visibility( + key: ValueKey(_hasDuplicateNames), + visible: _hasDuplicateNames, + child: const SelectableText( + 'Error: Duplicate subspace names are not allowed.', + style: TextStyle(color: Colors.red), + ), + ), + ), + ], + ), + actions: [ + SpaceDetailsActionButtons( + onSave: _hasDuplicateNames ? null : _handleSave, + onCancel: Navigator.of(context).pop, + ) + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart new file mode 100644 index 00000000..854b79bc --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpacesInput extends StatefulWidget { + const SubSpacesInput({ + super.key, + required this.subSpaces, + required this.onSubspaceAdded, + required this.onSubspaceDeleted, + }); + + final List subSpaces; + final void Function(String name) onSubspaceAdded; + final void Function(String uuid) onSubspaceDeleted; + + @override + State createState() => _SubSpacesInputState(); +} + +class _SubSpacesInputState extends State { + late final TextEditingController _subspaceNameController; + late final FocusNode _focusNode; + @override + void initState() { + super.initState(); + _subspaceNameController = TextEditingController(); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.name.toLowerCase(); + + final duplicateIndices = widget.subSpaces + .asMap() + .entries + .where((e) => e.value.name.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: subSpace, + isDuplicate: isDuplicate, + onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + focusNode: _focusNode, + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + widget.onSubspaceAdded(trimmedValue); + _subspaceNameController.clear(); + _focusNode.requestFocus(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart new file mode 100644 index 00000000..a80ddd15 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceChip extends StatelessWidget { + const SubspaceChip({ + required this.subSpace, + required this.isDuplicate, + required this.onDeleted, + super.key, + }); + + final Subspace subSpace; + final bool isDuplicate; + final void Function() onDeleted; + + @override + Widget build(BuildContext context) { + return Chip( + label: Text( + subSpace.name, + style: context.textTheme.bodySmall?.copyWith( + color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor, + ), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide( + color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + fit: BoxFit.scaleDown, + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + onDeleted: onDeleted, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart new file mode 100644 index 00000000..bf13ffd3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubspaceNameDisplayWidget extends StatefulWidget { + const SubspaceNameDisplayWidget({super.key, required this.subSpace}); + + final Subspace subSpace; + + @override + State createState() => + _SubspaceNameDisplayWidgetState(); +} + +class _SubspaceNameDisplayWidgetState extends State { + late final TextEditingController _controller; + late final FocusNode _focusNode; + bool _isEditing = false; + bool _hasDuplicateName = false; + + @override + void initState() { + _controller = TextEditingController(text: widget.subSpace.name); + _focusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + bool _checkForDuplicateName(String name) { + final bloc = context.read(); + return bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .any((s) => s.name.toLowerCase() == name.toLowerCase()); + } + + void _handleNameChange(String value) { + setState(() { + _hasDuplicateName = _checkForDuplicateName(value); + }); + } + + void _tryToFinishEditing() { + if (!_hasDuplicateName) { + _onFinishEditing(); + } + } + + void _tryToSubmit(String value) { + if (_hasDuplicateName) return; + + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .map( + (e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e, + ) + .toList(), + ), + ); + _onFinishEditing(); + } + + @override + Widget build(BuildContext context) { + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.spaceColor, + ); + return InkWell( + onTap: () { + setState(() => _isEditing = true); + _focusNode.requestFocus(); + }, + child: Chip( + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: ColorsManager.transparentColor), + ), + onDeleted: () { + final bloc = context.read(); + bloc.add( + UpdateSpaceDetailsSubspaces( + bloc.state.subspaces + .where((s) => s.uuid != widget.subSpace.uuid) + .toList(), + ), + ); + }, + deleteIcon: Container( + padding: const EdgeInsetsDirectional.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const FittedBox( + child: Icon( + Icons.close, + color: ColorsManager.lightGrayColor, + ), + ), + ), + label: Visibility( + visible: _isEditing, + replacement: Text( + widget.subSpace.name, + style: textStyle, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: context.screenWidth * 0.065, + height: context.screenHeight * 0.025, + child: TextField( + focusNode: _focusNode, + controller: _controller, + style: textStyle?.copyWith( + color: _hasDuplicateName ? Colors.red : null, + ), + decoration: const InputDecoration.collapsed( + hintText: '', + ), + onChanged: _handleNameChange, + onTapOutside: (_) => _tryToFinishEditing(), + onSubmitted: _tryToSubmit, + ), + ), + if (_hasDuplicateName) + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Visibility( + key: ValueKey(_hasDuplicateName), + visible: _hasDuplicateName, + child: Text( + 'Name already exists', + style: textStyle?.copyWith( + color: Colors.red, + fontSize: 8, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _onFinishEditing() { + setState(() { + _isEditing = false; + _hasDuplicateName = false; + }); + _focusNode.unfocus(); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart index b5545bd3..76ceec71 100644 --- a/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart @@ -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> loadTags(LoadTagsParam param) async { - if (param.projectUuid == null) { - throw Exception('Project UUID is required'); - } - + Future> 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; final data = result['data'] as List; @@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is required'); + } + return '/projects/$projectUuid/tags'; + } } diff --git a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart index 1044d888..370bdf47 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/models/tag.dart @@ -13,6 +13,13 @@ class Tag extends Equatable { required this.updatedAt, }); + factory Tag.empty() => const Tag( + uuid: '', + name: '', + createdAt: '', + updatedAt: '', + ); + factory Tag.fromJson(Map json) { return Tag( uuid: json['uuid'] as String, diff --git a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart index ae097020..cf36527a 100644 --- a/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart +++ b/lib/pages/space_management_v2/modules/tags/domain/services/tags_service.dart @@ -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> loadTags(LoadTagsParam param); + Future> loadTags(); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart index e51884cb..b81fcb76 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart @@ -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 { ) 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)); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart index 99134cab..8965b7b0 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/bloc/tags_event.dart @@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable { } class LoadTags extends TagsEvent { - final LoadTagsParam param; - - const LoadTags(this.param); - - @override - List get props => [param]; + const LoadTags(); } diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart new file mode 100644 index 00000000..4c9990ae --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -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 createState() => _AddDeviceTypeWidgetState(); +} + +class _AddDeviceTypeWidgetState extends State { + final Map _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( + 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, + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart new file mode 100644 index 00000000..3cab4abe --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -0,0 +1,231 @@ +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 createState() => _AssignTagsDialogState(); +} + +class _AssignTagsDialogState extends State { + late SpaceDetailsModel _space; + final Map _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 = {}; + final allAllocations = [ + ..._space.productAllocations, + ..._space.subspaces.expand((s) => s.productAllocations), + ]; + + final allocationsByProductType = >{}; + for (final allocation in allAllocations) { + (allocationsByProductType[allocation.product.productType] ??= []) + .add(allocation); + } + + for (final productType in allocationsByProductType.keys) { + final allocations = allocationsByProductType[productType]!; + final tagCounts = {}; + + 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 = {}; + 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>( + 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(), + product: product, + tag: Tag.empty(), + ), + ); + } + }); + _validateAllTags(); + }, + cancelButtonLabel: 'Add New Device', + ) + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart new file mode 100644 index 00000000..9b0fd478 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart @@ -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 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(), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart new file mode 100644 index 00000000..6e7e2097 --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -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 productAllocations; + final List subspaces; + final Map 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( + create: (BuildContext context) => TagsBloc( + RemoteTagsService(HTTPService()), + )..add(const LoadTags()), + child: BlocBuilder( + 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(), + }; + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart new file mode 100644 index 00000000..8bbf379d --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart @@ -0,0 +1,186 @@ +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'; + +class ProductTagField extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final Tag? initialValue; + final String productName; + + const ProductTagField({ + super.key, + required this.items, + required this.onSelected, + this.initialValue, + required this.productName, + }); + + @override + State createState() => _ProductTagFieldState(); +} + +class _ProductTagFieldState extends State { + 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( + (tag) => tag.name.toLowerCase() == lowerCaseValue, + orElse: () => Tag( + name: value, + uuid: '', + createdAt: '', + updatedAt: '', + ), + ); + 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(); + }, + ), + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart new file mode 100644 index 00000000..c3910aca --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart @@ -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, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart new file mode 100644 index 00000000..605fde2f --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart @@ -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, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart new file mode 100644 index 00000000..53e59bde --- /dev/null +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart @@ -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 products; + final Map 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), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart index 6c550673..d6451a00 100644 --- a/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart +++ b/lib/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -13,15 +13,15 @@ class RemoteUpdateCommunityService implements UpdateCommunityService { static const _defaultErrorMessage = 'Failed to update community'; @override - Future updateCommunity(UpdateCommunityParam param) async { + Future updateCommunity(CommunityModel param) async { + final endpoint = await _makeUrl(param.uuid); try { - final response = await _httpService.put( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + await _httpService.put( + path: endpoint, + body: {'name': param.name}, + expectedResponseModel: (data) => null, ); - return response; + return param; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -36,4 +36,12 @@ class RemoteUpdateCommunityService implements UpdateCommunityService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl(String communityUuid) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities/$communityUuid'; + } } diff --git a/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart b/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart deleted file mode 100644 index 69dfc4e2..00000000 --- a/lib/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class UpdateCommunityParam extends Equatable { - const UpdateCommunityParam({required this.name}); - - final String name; - - @override - List get props => [name]; -} diff --git a/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart b/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart index 9703fdc6..d32e79b6 100644 --- a/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart +++ b/lib/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart @@ -1,6 +1,5 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart'; abstract class UpdateCommunityService { - Future updateCommunity(UpdateCommunityParam param); + Future updateCommunity(CommunityModel community); } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart index 4e913c22..6a4c2051 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart @@ -1,7 +1,6 @@ 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/update_community/domain/params/update_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -24,7 +23,7 @@ class UpdateCommunityBloc extends Bloc get props => [param]; + List get props => [communityModel]; } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart index 9126be0a..14ca7f6e 100644 --- a/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart +++ b/lib/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_state.dart @@ -21,10 +21,10 @@ final class UpdateCommunitySuccess extends UpdateCommunityState { } final class UpdateCommunityFailure extends UpdateCommunityState { - final String message; + final String errorMessage; - const UpdateCommunityFailure(this.message); + const UpdateCommunityFailure(this.errorMessage); @override - List get props => [message]; + List get props => [errorMessage]; } diff --git a/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart new file mode 100644 index 00000000..b566c02a --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/shared/widgets/community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/data/services/remote_update_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class EditCommunityDialog extends StatelessWidget { + const EditCommunityDialog({ + required this.community, + required this.parentContext, + super.key, + }); + + final CommunityModel community; + final BuildContext parentContext; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => UpdateCommunityBloc( + RemoteUpdateCommunityService(HTTPService()), + ), + child: BlocConsumer( + listener: (context, state) { + switch (state) { + case UpdateCommunityInitial() || UpdateCommunityLoading(): + SpaceManagementCommunityDialogHelper.showLoadingDialog(context); + break; + case UpdateCommunitySuccess(:final community): + _onUpdateCommunitySuccess(context, community); + break; + case UpdateCommunityFailure(): + Navigator.of(context).pop(); + break; + } + }, + builder: (context, state) => CommunityDialog( + title: const Text('Edit Community'), + initialName: community.name, + errorMessage: state is UpdateCommunityFailure ? state.errorMessage : null, + onSubmit: (name) => context.read().add( + UpdateCommunity(community.copyWith(name: name)), + ), + ), + ), + ); + } + + void _onUpdateCommunitySuccess( + BuildContext context, + CommunityModel community, + ) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + SpaceManagementCommunityDialogHelper.showSuccessSnackBar( + context, + '${community.name} community updated successfully', + ); + parentContext.read().add( + CommunitiesUpdateCommunity(community), + ); + parentContext.read().add( + SelectCommunityEvent(community: community), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart new file mode 100644 index 00000000..15a22fda --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -0,0 +1,53 @@ +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'; + +part 'space_details_model_event.dart'; + +class SpaceDetailsModelBloc extends Bloc { + SpaceDetailsModelBloc({ + required SpaceDetailsModel initialState, + }) : super(initialState) { + on(_onUpdateSpaceDetailsIcon); + on(_onUpdateSpaceDetailsName); + on(_onUpdateSpaceDetailsSubspaces); + on( + _onUpdateSpaceDetailsProductAllocations); + on(_onUpdateSpaceDetails); + } + + void _onUpdateSpaceDetailsIcon( + UpdateSpaceDetailsIcon event, + Emitter emit, + ) { + emit(state.copyWith(icon: event.icon)); + } + + void _onUpdateSpaceDetailsName( + UpdateSpaceDetailsName event, + Emitter emit, + ) { + emit(state.copyWith(spaceName: event.name)); + } + + void _onUpdateSpaceDetailsSubspaces( + UpdateSpaceDetailsSubspaces event, + Emitter emit, + ) { + emit(state.copyWith(subspaces: event.subspaces)); + } + + void _onUpdateSpaceDetailsProductAllocations( + UpdateSpaceDetailsProductAllocations event, + Emitter emit, + ) { + emit(state.copyWith(productAllocations: event.productAllocations)); + } + + void _onUpdateSpaceDetails( + UpdateSpaceDetails event, + Emitter emit, + ) { + emit(event.space); + } +} diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart new file mode 100644 index 00000000..abf9cd98 --- /dev/null +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_event.dart @@ -0,0 +1,53 @@ +part of 'space_details_model_bloc.dart'; + +sealed class SpaceDetailsModelEvent extends Equatable { + const SpaceDetailsModelEvent(); + + @override + List get props => []; +} + +final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsIcon(this.icon); + + final String icon; + + @override + List get props => [icon]; +} + +final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsName(this.name); + + final String name; + + @override + List get props => [name]; +} + +final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsSubspaces(this.subspaces); + + final List subspaces; + + @override + List get props => [subspaces]; +} + +final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent { + const UpdateSpaceDetailsProductAllocations(this.productAllocations); + + final List productAllocations; + + @override + List get props => [productAllocations]; +} + +final class UpdateSpaceDetails extends SpaceDetailsModelEvent { + const UpdateSpaceDetails(this.space); + + final SpaceDetailsModel space; + + @override + List get props => [space]; +} diff --git a/lib/pages/spaces_management/all_spaces/model/product_model.dart b/lib/pages/spaces_management/all_spaces/model/product_model.dart index 8f905032..a7b23d0f 100644 --- a/lib/pages/spaces_management/all_spaces/model/product_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/product_model.dart @@ -58,11 +58,14 @@ class ProductModel { '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, diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 6dc20cfd..c5ee3259 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -21,7 +21,6 @@ import 'package:syncrow_web/utils/snack_bar.dart'; class VisitorPasswordBloc extends Bloc { - VisitorPasswordBloc() : super(VisitorPasswordInitial()) { on(selectUsageFrequency); on(_onFetchDevice); @@ -87,6 +86,9 @@ class VisitorPasswordBloc SelectTimeVisitorPassword event, Emitter emit, ) async { + // Ensure expirationTimeTimeStamp has a value + effectiveTimeTimeStamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000; + final DateTime? picked = await showDatePicker( context: event.context, initialDate: DateTime.now(), @@ -94,86 +96,124 @@ class VisitorPasswordBloc lastDate: DateTime.now().add(const Duration(days: 5095)), ); - if (picked != null) { - final TimeOfDay? timePicked = await showTimePicker( - context: event.context, - initialTime: TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: ThemeData.light().copyWith( - colorScheme: const ColorScheme.light( - primary: ColorsManager.primaryColor, - onSurface: Colors.black, - ), - buttonTheme: const ButtonThemeData( - colorScheme: ColorScheme.light( - primary: Colors.green, - ), - ), + if (picked == null) return; + + final TimeOfDay? timePicked = await showTimePicker( + context: event.context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: ThemeData.light().copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + onSurface: Colors.black, ), - child: child!, - ); - }, - ); - - if (timePicked != null) { - final selectedDateTime = DateTime( - picked.year, - picked.month, - picked.day, - timePicked.hour, - timePicked.minute, + ), + child: child!, ); + }, + ); - final selectedTimestamp = - selectedDateTime.millisecondsSinceEpoch ~/ 1000; + if (timePicked == null) return; - if (event.isStart) { - if (expirationTimeTimeStamp != null && - selectedTimestamp > expirationTimeTimeStamp!) { - CustomSnackBar.displaySnackBar( + final selectedDateTime = DateTime( + picked.year, + picked.month, + picked.day, + timePicked.hour, + timePicked.minute, + ); + final selectedTimestamp = selectedDateTime.millisecondsSinceEpoch ~/ 1000; + final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + + if (event.isStart) { + // START TIME VALIDATION + if (expirationTimeTimeStamp != null && + selectedTimestamp > expirationTimeTimeStamp!) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( 'Effective Time cannot be later than Expiration Time.', - ); - return; - } - if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { - if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { - await showDialog( - context: event.context, - builder: (context) => AlertDialog( - title: const Text('Effective Time cannot be earlier than current time.'), - actionsAlignment: MainAxisAlignment.center, - content: - FilledButton( - onPressed: () { - Navigator.of(event.context).pop(); - add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); - }, - child: const Text('OK'), - ), - - ), - ); - } - return; - } - effectiveTimeTimeStamp = selectedTimestamp; - startTimeAccess = selectedDateTime.toString().split('.').first; - } else { - if (effectiveTimeTimeStamp != null && - selectedTimestamp < effectiveTimeTimeStamp!) { - CustomSnackBar.displaySnackBar( - 'Expiration Time cannot be earlier than Effective Time.', - ); - return; - } - expirationTimeTimeStamp = selectedTimestamp; - endTimeAccess = selectedDateTime.toString().split('.').first; - } - emit(ChangeTimeState()); - emit(VisitorPasswordInitial()); + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: true, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; } + + if (selectedTimestamp < currentTimestamp) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( + 'Effective Time cannot be earlier than current time.', + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: true, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; + } + + // Save effective time + effectiveTimeTimeStamp = selectedTimestamp; + startTimeAccess = selectedDateTime.toString().split('.').first; + } else { + // END TIME VALIDATION + if (effectiveTimeTimeStamp != null && + selectedTimestamp < effectiveTimeTimeStamp!) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text( + 'Expiration Time cannot be earlier than Effective Time.', + ), + actionsAlignment: MainAxisAlignment.center, + content: FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword( + context: event.context, + isStart: false, + isRepeat: false, + )); + }, + child: const Text('OK'), + ), + ), + ); + return; + } + + // Save expiration time + expirationTimeTimeStamp = selectedTimestamp; + endTimeAccess = selectedDateTime.toString().split('.').first; } + + emit(ChangeTimeState()); + emit(VisitorPasswordInitial()); } bool toggleRepeat( @@ -213,7 +253,7 @@ class VisitorPasswordBloc FetchDevice event, Emitter emit) async { try { emit(DeviceLoaded()); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; data = await AccessMangApi().fetchDoorLockDeviceList(projectUuid); emit(TableLoaded(data)); diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index d1fb172a..5362f448 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -2,10 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/date_time_widget.dart'; -import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; @@ -23,8 +21,8 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { - Size size = MediaQuery.of(context).size; - var text = Theme.of(context) + final size = MediaQuery.of(context).size; + final text = Theme.of(context) .textTheme .bodySmall! .copyWith(color: Colors.black, fontSize: 13); @@ -41,8 +39,7 @@ class VisitorPasswordDialog extends StatelessWidget { title: 'Sent Successfully', widgeta: Column( children: [ - if (visitorBloc - .passwordStatus!.failedOperations.isNotEmpty) + if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) Column( children: [ const Text('Failed Devices'), @@ -56,22 +53,19 @@ class VisitorPasswordDialog extends StatelessWidget { .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( - margin: EdgeInsets.all(5), + margin: const EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc - .passwordStatus! - .failedOperations[index] - .deviceName)), + child: Text(visitorBloc.passwordStatus! + .failedOperations[index].deviceName)), ); }, ), ), ], ), - if (visitorBloc - .passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -85,14 +79,12 @@ class VisitorPasswordDialog extends StatelessWidget { .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( - margin: EdgeInsets.all(5), + margin: const EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc - .passwordStatus! - .successOperations[index] - .deviceName)), + child: Text(visitorBloc.passwordStatus! + .successOperations[index].deviceName)), ); }, ), @@ -115,16 +107,14 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = + final isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; return AlertDialog( backgroundColor: Colors.white, title: Text( 'Create visitor password', style: Theme.of(context).textTheme.headlineLarge!.copyWith( - fontWeight: FontWeight.w400, - fontSize: 24, - color: Colors.black), + fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -310,14 +300,12 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(SelectTimeEvent( - context: context, - isEffective: false)); + context: context, isEffective: false)); } else { - visitorBloc.add( - SelectTimeVisitorPassword( - context: context, - isStart: false, - isRepeat: false)); + visitorBloc.add(SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { @@ -326,31 +314,28 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(SelectTimeEvent( - context: context, - isEffective: true)); + context: context, isEffective: true)); } else { - visitorBloc.add( - SelectTimeVisitorPassword( - context: context, - isStart: true, - isRepeat: false)); + visitorBloc.add(SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); } }, - firstString: (visitorBloc - .usageFrequencySelected == - 'Periodic' && - visitorBloc.accessTypeSelected == - 'Offline Password') - ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess - .toString(), + firstString: + (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + ? visitorBloc.effectiveTime + : visitorBloc.startTimeAccess, secondString: (visitorBloc .usageFrequencySelected == 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') ? visitorBloc.expirationTime - : visitorBloc.endTimeAccess.toString(), + : visitorBloc.endTimeAccess, icon: Assets.calendarIcon), const SizedBox( height: 10, @@ -410,8 +395,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc - .add(ToggleRepeatEvent()); + visitorBloc.add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -442,8 +426,7 @@ class VisitorPasswordDialog extends StatelessWidget { }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = - listDevice; + visitorBloc.selectedDevices = listDevice; } }); }, @@ -455,8 +438,7 @@ class VisitorPasswordDialog extends StatelessWidget { .bodySmall! .copyWith( fontWeight: FontWeight.w400, - color: - ColorsManager.whiteColors, + color: ColorsManager.whiteColors, fontSize: 12), ), ), @@ -495,37 +477,30 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.selectedDevices.isNotEmpty) { - if (visitorBloc.usageFrequencySelected == - 'One-Time' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + if (visitorBloc.usageFrequencySelected == 'One-Time' && + visitorBloc.accessTypeSelected == 'Offline Password') { setPasswordFunction(context, size, visitorBloc); } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + visitorBloc.accessTypeSelected == 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, - message: - 'Please select Access Period to continue', + message: 'Please select Access Period to continue', title: 'Access Period'); } - } else if (visitorBloc.endTimeAccess.toString() != - 'End Time' && - visitorBloc.startTimeAccess.toString() != - 'Start Time') { + } else if (visitorBloc.endTimeAccess != 'End Time' && + visitorBloc.startTimeAccess != 'Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time' && visitorBloc.selectedDays.isNotEmpty) { - setPasswordFunction( - context, size, visitorBloc); + setPasswordFunction(context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, @@ -539,15 +514,13 @@ class VisitorPasswordDialog extends StatelessWidget { } else { visitorBloc.stateDialog( context: context, - message: - 'Please select Access Period to continue', + message: 'Please select Access Period to continue', title: 'Access Period'); } } else { visitorBloc.stateDialog( context: context, - message: - 'Please select Access Period to continue', + message: 'Please select Access Period to continue', title: 'Access Period'); } } else { @@ -593,17 +566,17 @@ class VisitorPasswordDialog extends StatelessWidget { alignment: Alignment.center, content: SizedBox( height: size.height * 0.25, - child: Center( - child: - CircularProgressIndicator(), // Display a loading spinner + child: const Center( + child: CircularProgressIndicator(), // Display a loading spinner ), ), ); } else { return AlertDialog( alignment: Alignment.center, + backgroundColor: Colors.white, content: SizedBox( - height: size.height * 0.25, + height: size.height * 0.13, child: Column( children: [ Column( @@ -617,13 +590,16 @@ class VisitorPasswordDialog extends StatelessWidget { width: 35, ), ), + const SizedBox( + height: 20, + ), Text( 'Set Password', style: Theme.of(context) .textTheme .headlineLarge! .copyWith( - fontSize: 30, + fontSize: 24, fontWeight: FontWeight.w400, color: Colors.black, ), @@ -631,15 +607,6 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), const SizedBox(width: 15), - Text( - 'This action will update all of the selected\n door locks passwords in the property.\n\nAre you sure you want to continue?', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.grayColor, - fontWeight: FontWeight.w400, - fontSize: 18, - ), - ), ], ), ), @@ -668,12 +635,12 @@ class VisitorPasswordDialog extends StatelessWidget { decoration: containerDecoration, width: size.width * 0.1, child: DefaultButton( + backgroundColor: Color(0xff023DFE), borderRadius: 8, onPressed: () { Navigator.pop(context); if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == - 'Online Password') { + visitorBloc.accessTypeSelected == 'Online Password') { visitorBloc.add(OnlineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, @@ -681,8 +648,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == - 'Online Password') { + visitorBloc.accessTypeSelected == 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, @@ -693,8 +659,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, @@ -702,8 +667,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, @@ -715,7 +679,7 @@ class VisitorPasswordDialog extends StatelessWidget { } }, child: Text( - 'Ok', + 'Confirm', style: Theme.of(context).textTheme.bodySmall!.copyWith( fontWeight: FontWeight.w400, color: ColorsManager.whiteColors, diff --git a/lib/services/access_mang_api.dart b/lib/services/access_mang_api.dart index a780a12b..db3cb554 100644 --- a/lib/services/access_mang_api.dart +++ b/lib/services/access_mang_api.dart @@ -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'; diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 50170ed9..a36d1193 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -83,7 +83,6 @@ abstract class ColorsManager { static const Color maxPurpleDot = Color(0xFF5F00BD); static const Color minBlue = Color(0xFF93AAFD); static const Color minBlueDot = Color(0xFF023DFE); - static const Color grey25 = Color(0xFFF9F9F9); - - + static const Color grey25 = Color(0xFFF9F9F9); + static const Color grey50 = Color(0xFF718096); } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index c3e8b1d2..72fff557 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -138,4 +138,6 @@ 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'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 8979c446..f92975f3 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -394,6 +394,7 @@ class Assets { static const String emptyBox = 'assets/icons/empty_box.png'; static const String completeProcessIcon = 'assets/icons/compleate_process_icon.svg'; + static const String completedDoneIcon = 'assets/images/completed_done.svg'; static const String currentProcessIcon = 'assets/icons/current_process_icon.svg'; static const String uncomplete_ProcessIcon = @@ -505,5 +506,15 @@ class Assets { static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; - static const String autocadOccupancyImage = 'assets/images/autocad_occupancy_image.png'; + static const String autocadOccupancyImage = + 'assets/images/autocad_occupancy_image.png'; + static const String emptyBarredChart = 'assets/icons/empty_barred_chart.svg'; + static const String emptyEnergyManagementChart = + 'assets/icons/empty_energy_management_chart.svg'; + static const String emptyEnergyManagementPerDevice = + 'assets/icons/empty_energy_management_per_device.svg'; + static const String emptyHeatmap = 'assets/icons/empty_heatmap.svg'; + static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; + static const String homeIcon = 'assets/icons/home_icon.svg'; + static const String groupIcon = 'assets/icons/group_icon.svg'; } diff --git a/lib/utils/theme/theme.dart b/lib/utils/theme/theme.dart index 5ac61afa..5c036762 100644 --- a/lib/utils/theme/theme.dart +++ b/lib/utils/theme/theme.dart @@ -52,4 +52,7 @@ final myTheme = ThemeData( borderRadius: BorderRadius.circular(4), ), ), + dialogTheme: const DialogThemeData( + backgroundColor: ColorsManager.whiteColors, + ), ); diff --git a/pubspec.yaml b/pubspec.yaml index c4692ac4..67bf5328 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: