diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index ffc57131..966d5361 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -88,31 +88,32 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { ); }), rightBody: const NavigateHomeGridView(), - scaffoldBody: BlocBuilder( - builder: (context, state) { - if (state is SelectedTabState && state.selectedTab) { - return const RoutinesView(); - } - if (state is ShowCreateRoutineState && state.showCreateRoutine) { - return const CreateNewRoutineView(); - } + scaffoldBody: CreateNewRoutineView(), + // BlocBuilder( + // builder: (context, state) { + // if (state is SelectedTabState && state.selectedTab) { + // return const RoutinesView(); + // } + // if (state is ShowCreateRoutineState && state.showCreateRoutine) { + // return const CreateNewRoutineView(); + // } - return BlocBuilder( - builder: (context, deviceState) { - if (deviceState is DeviceManagementLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (deviceState is DeviceManagementLoaded || - deviceState is DeviceManagementFiltered) { - final devices = (deviceState as dynamic).devices ?? - (deviceState as DeviceManagementFiltered).filteredDevices; + // return BlocBuilder( + // builder: (context, deviceState) { + // if (deviceState is DeviceManagementLoading) { + // return const Center(child: CircularProgressIndicator()); + // } else if (deviceState is DeviceManagementLoaded || + // deviceState is DeviceManagementFiltered) { + // final devices = (deviceState as dynamic).devices ?? + // (deviceState as DeviceManagementFiltered).filteredDevices; - return DeviceManagementBody(devices: devices); - } else { - return const Center(child: Text('Error fetching Devices')); - } - }, - ); - }), + // return DeviceManagementBody(devices: devices); + // } else { + // return const Center(child: Text('Error fetching Devices')); + // } + // }, + // ); + // }), ), ); } diff --git a/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart index 05f63123..b5a293a1 100644 --- a/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart +++ b/lib/pages/routiens/bloc/effective_period/effect_period_bloc.dart @@ -2,7 +2,10 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_event.dart'; import 'package:syncrow_web/pages/routiens/bloc/effective_period/effect_period_state.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/navigation_service.dart'; class EffectPeriodBloc extends Bloc { final daysMap = { @@ -49,9 +52,9 @@ class EffectPeriodBloc extends Bloc { break; } - // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( - // EffectiveTimePeriodEvent( - // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + EffectiveTimePeriodEvent( + EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); emit(state.copyWith( selectedPeriod: event.period, customStartTime: startTime, customEndTime: endTime)); @@ -68,11 +71,11 @@ class EffectPeriodBloc extends Bloc { final newDaysBinary = daysList.join(); emit(state.copyWith(selectedDaysBinary: newDaysBinary)); - // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( - // EffectiveTimePeriodEvent(EffectiveTime( - // start: state.customStartTime ?? '00:00', - // end: state.customEndTime ?? '23:59', - // loops: newDaysBinary))); + BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + EffectiveTimePeriodEvent(EffectiveTime( + start: state.customStartTime ?? '00:00', + end: state.customEndTime ?? '23:59', + loops: newDaysBinary))); } void _onSetCustomTime(SetCustomTime event, Emitter emit) { @@ -94,9 +97,9 @@ class EffectPeriodBloc extends Bloc { emit( state.copyWith(customStartTime: startTime, customEndTime: endTime, selectedPeriod: period)); - // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( - // EffectiveTimePeriodEvent( - // EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); + BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + EffectiveTimePeriodEvent( + EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary))); } void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter emit) { @@ -106,8 +109,8 @@ class EffectPeriodBloc extends Bloc { customEndTime: '23:59', selectedDaysBinary: '1111111')); - // BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( - // EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '1111111'))); + BlocProvider.of(NavigationService.navigatorKey.currentContext!).add( + EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '1111111'))); } void _onResetDays(ResetDays event, Emitter emit) { diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart index 88414196..7694e8ec 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_bloc.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/routines_api.dart'; @@ -14,8 +15,8 @@ part 'routine_state.dart'; const spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; class RoutineBloc extends Bloc { - bool isAutomation = false; - bool isTabToRun = false; + // bool isAutomation = false; + // bool isTabToRun = false; RoutineBloc() : super(const RoutineState()) { on(_onAddToIfContainer); @@ -27,28 +28,45 @@ class RoutineBloc extends Bloc { on(_onAddSelectedIcon); on(_onCreateScene); on(_onRemoveDragCard); + on(_changeOperatorOperator); + on(_onEffectiveTimeEvent); + on(_onCreateAutomation); + on(_onSetRoutineName); // on(_onRemoveFunction); // on(_onClearFunctions); } void _onAddToIfContainer(AddToIfContainer event, Emitter emit) { - final updatedIfItems = List>.from(state.ifItems)..add(event.item); + final updatedIfItems = List>.from(state.ifItems) + ..add(event.item); if (event.isTabToRun) { - isTabToRun = true; - isAutomation = false; + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: true, isAutomation: false)); } else { - isTabToRun = false; - isAutomation = true; + emit(state.copyWith( + ifItems: updatedIfItems, isTabToRun: false, isAutomation: true)); } - emit(state.copyWith(ifItems: updatedIfItems)); } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { - final updatedThenItems = List>.from(state.thenItems)..add(event.item); - emit(state.copyWith(thenItems: updatedThenItems)); + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { + final currentItems = List>.from(state.thenItems); + + // Find the index of the item in teh current itemsList + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + // Replace the map if the index is valid + if (index != -1) { + currentItems[index] = event.item; + } else { + currentItems.add(event.item); + } + + emit(state.copyWith(thenItems: currentItems)); } - void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter emit) { + void _onAddFunctionsToRoutine( + AddFunctionToRoutine event, Emitter emit) { try { if (event.functions.isEmpty) return; @@ -57,9 +75,11 @@ class RoutineBloc extends Bloc { if (currentSelectedFunctions.containsKey(event.uniqueCustomId)) { currentSelectedFunctions[event.uniqueCustomId] = - List.from(currentSelectedFunctions[event.uniqueCustomId]!)..addAll(event.functions); + List.from(currentSelectedFunctions[event.uniqueCustomId]!) + ..addAll(event.functions); } else { - currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); } emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); @@ -68,7 +88,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -87,7 +108,8 @@ class RoutineBloc extends Bloc { } } - Future _onLoadAutomation(LoadAutomation event, Emitter emit) async { + Future _onLoadAutomation( + LoadAutomation event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); try { @@ -106,13 +128,15 @@ class RoutineBloc extends Bloc { } } - FutureOr _onSearchRoutines(SearchRoutines event, Emitter emit) async { + FutureOr _onSearchRoutines( + SearchRoutines event, Emitter emit) async { emit(state.copyWith(isLoading: true, errorMessage: null)); await Future.delayed(const Duration(seconds: 1)); emit(state.copyWith(searchText: event.query)); } - FutureOr _onAddSelectedIcon(AddSelectedIcon event, Emitter emit) { + FutureOr _onAddSelectedIcon( + AddSelectedIcon event, Emitter emit) { emit(state.copyWith(selectedIcon: event.icon)); } @@ -121,7 +145,8 @@ class RoutineBloc extends Bloc { return actions.first['deviceId'] == 'delay'; } - Future _onCreateScene(CreateSceneEvent event, Emitter emit) async { + Future _onCreateScene( + CreateSceneEvent event, Emitter emit) async { try { // Check if first action is delay if (_isFirstActionDelay(state.thenItems)) { @@ -136,7 +161,8 @@ class RoutineBloc extends Bloc { final actions = state.thenItems .map((item) { - final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; if (functions.isEmpty) return null; final function = functions.first; @@ -191,9 +217,119 @@ class RoutineBloc extends Bloc { } } - FutureOr _onRemoveDragCard(RemoveDragCard event, Emitter emit) { + Future _onCreateAutomation( + CreateAutomationEvent event, Emitter emit) async { + try { + if (state.routineName == null || state.routineName!.isEmpty) { + emit(state.copyWith( + errorMessage: 'Automation name is required', + )); + return; + } + + emit(state.copyWith(isLoading: true)); + + final conditions = state.ifItems + .map((item) { + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; + if (functions.isEmpty) return null; + + final function = functions.first; + return CreateCondition( + code: state.ifItems.indexOf(item) + 1, + entityId: function.entityId, + entityType: 'device_report', + expr: ConditionExpr( + statusCode: function.functionCode, + comparator: function.condition ?? '==', + statusValue: function.value, + ), + ); + }) + .whereType() + .toList(); + + if (conditions.isEmpty) { + emit(state.copyWith( + isLoading: false, + errorMessage: 'At least one condition is required', + )); + return; + } + + final createAutomationModel = CreateAutomationModel( + unitUuid: spaceId, + automationName: state.routineName!, + decisionExpr: state.selectedAutomationOperator, + effectiveTime: state.effectiveTime ?? + EffectiveTime( + start: '00:00', + end: '23:59', + loops: '1111111', + ), + conditions: conditions, + actions: state.thenItems + .map((item) { + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; + if (functions.isEmpty) return null; + + final function = functions.first; + if (function.functionCode == 'automation') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: function.value, + executorProperty: null, + ); + } + + if (item['deviceId'] == 'delay') { + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'delay', + executorProperty: CreateSceneExecutorProperty( + functionCode: '', + functionValue: '', + delaySeconds: function.value, + ), + ); + } + + return CreateSceneAction( + entityId: function.entityId, + actionExecutor: 'device_issue', + executorProperty: CreateSceneExecutorProperty( + functionCode: function.functionCode, + functionValue: function.value, + delaySeconds: 0, + ), + ); + }) + .whereType() + .toList(), + ); + + final result = await SceneApi.createAutomation(createAutomationModel); + if (result['success']) { + emit(const RoutineState()); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: result['message'], + )); + } + } catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.toString(), + )); + } + } + + FutureOr _onRemoveDragCard( + RemoveDragCard event, Emitter emit) { if (event.isFromThen) { - /// remove element from thenItems at specific index final thenItems = List>.from(state.thenItems); thenItems.removeAt(event.index); emit(state.copyWith(thenItems: thenItems)); @@ -203,4 +339,21 @@ class RoutineBloc extends Bloc { emit(state.copyWith(ifItems: ifItems)); } } + + FutureOr _changeOperatorOperator( + ChangeAutomationOperator event, Emitter emit) { + emit(state.copyWith( + selectedAutomationOperator: event.operator, + )); + } + + FutureOr _onEffectiveTimeEvent( + EffectiveTimePeriodEvent event, Emitter emit) { + emit(state.copyWith(effectiveTime: event.effectiveTime)); + } + + FutureOr _onSetRoutineName( + SetRoutineName event, Emitter emit) { + emit(state.copyWith(routineName: event.name)); + } } diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart index 9e154cf4..9337e88e 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_event.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_event.dart @@ -87,4 +87,38 @@ class RemoveDragCard extends RoutineEvent { List get props => [index]; } +class ChangeAutomationOperator extends RoutineEvent { + final String operator; + const ChangeAutomationOperator({required this.operator}); + @override + List get props => [operator]; +} + +class EffectiveTimePeriodEvent extends RoutineEvent { + final EffectiveTime effectiveTime; + const EffectiveTimePeriodEvent(this.effectiveTime); + @override + List get props => [effectiveTime]; +} + +class CreateAutomationEvent extends RoutineEvent { + // final CreateAutomationModel createAutomationModel; + final String? automationId; + final bool updateAutomation; + + const CreateAutomationEvent({ + //required this.createAutomationModel, + this.automationId, + this.updateAutomation = false, + }); + @override + List get props => []; +} + +class SetRoutineName extends RoutineEvent { + final String name; + const SetRoutineName(this.name); + @override + List get props => [name]; +} class ClearFunctions extends RoutineEvent {} diff --git a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart index f057c2c0..42f584ba 100644 --- a/lib/pages/routiens/bloc/routine_bloc/routine_state.dart +++ b/lib/pages/routiens/bloc/routine_bloc/routine_state.dart @@ -14,48 +14,70 @@ class RoutineState extends Equatable { final String? routineName; final String? selectedIcon; final String? searchText; + final bool isTabToRun; + final bool isAutomation; + final String selectedAutomationOperator; + final EffectiveTime? effectiveTime; - const RoutineState( - {this.ifItems = const [], - this.thenItems = const [], - this.availableCards = const [], - this.scenes = const [], - this.automations = const [], - this.selectedFunctions = const {}, - this.isLoading = false, - this.errorMessage, - this.routineName, - this.selectedIcon, - this.loadScenesErrorMessage, - this.loadAutomationErrorMessage, - this.searchText}); + const RoutineState({ + this.ifItems = const [], + this.thenItems = const [], + this.availableCards = const [], + this.scenes = const [], + this.automations = const [], + this.selectedFunctions = const {}, + this.isLoading = false, + this.errorMessage, + this.routineName, + this.selectedIcon, + this.loadScenesErrorMessage, + this.loadAutomationErrorMessage, + this.searchText, + this.isTabToRun = false, + this.isAutomation = false, + this.selectedAutomationOperator = 'or', + this.effectiveTime, + }); - RoutineState copyWith( - {List>? ifItems, - List>? thenItems, - List? scenes, - List? automations, - Map>? selectedFunctions, - bool? isLoading, - String? errorMessage, - String? routineName, - String? selectedIcon, - String? loadAutomationErrorMessage, - String? loadScenesErrorMessage, - String? searchText}) { + RoutineState copyWith({ + List>? ifItems, + List>? thenItems, + List? scenes, + List? automations, + Map>? selectedFunctions, + bool? isLoading, + String? errorMessage, + String? routineName, + String? selectedIcon, + String? loadAutomationErrorMessage, + String? loadScenesErrorMessage, + String? searchText, + bool? isTabToRun, + bool? isAutomation, + String? selectedAutomationOperator, + EffectiveTime? effectiveTime, + }) { return RoutineState( - ifItems: ifItems ?? this.ifItems, - thenItems: thenItems ?? this.thenItems, - scenes: scenes ?? this.scenes, - automations: automations ?? this.automations, - selectedFunctions: selectedFunctions ?? this.selectedFunctions, - isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage ?? this.errorMessage, - routineName: routineName ?? this.routineName, - selectedIcon: selectedIcon ?? this.selectedIcon, - loadScenesErrorMessage: loadScenesErrorMessage ?? this.loadScenesErrorMessage, - loadAutomationErrorMessage: loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, - searchText: searchText ?? this.searchText); + ifItems: ifItems ?? this.ifItems, + thenItems: thenItems ?? this.thenItems, + scenes: scenes ?? this.scenes, + automations: automations ?? this.automations, + selectedFunctions: selectedFunctions ?? this.selectedFunctions, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + routineName: routineName ?? this.routineName, + selectedIcon: selectedIcon ?? this.selectedIcon, + loadScenesErrorMessage: + loadScenesErrorMessage ?? this.loadScenesErrorMessage, + loadAutomationErrorMessage: + loadAutomationErrorMessage ?? this.loadAutomationErrorMessage, + searchText: searchText ?? this.searchText, + isTabToRun: isTabToRun ?? this.isTabToRun, + isAutomation: isAutomation ?? this.isAutomation, + selectedAutomationOperator: + selectedAutomationOperator ?? this.selectedAutomationOperator, + effectiveTime: effectiveTime ?? this.effectiveTime, + ); } @override @@ -71,6 +93,10 @@ class RoutineState extends Equatable { selectedIcon, loadScenesErrorMessage, loadAutomationErrorMessage, - searchText + searchText, + isTabToRun, + isAutomation, + selectedAutomationOperator, + effectiveTime ]; } diff --git a/lib/pages/routiens/helper/save_routine_helper.dart b/lib/pages/routiens/helper/save_routine_helper.dart index 69329992..c6a8b2aa 100644 --- a/lib/pages/routiens/helper/save_routine_helper.dart +++ b/lib/pages/routiens/helper/save_routine_helper.dart @@ -43,7 +43,7 @@ class SaveRoutineHelper { ), ), const SizedBox(height: 8), - if (context.read().isTabToRun) + if (state.isTabToRun) ListTile( leading: SvgPicture.asset( Assets.tabToRun, @@ -52,6 +52,35 @@ class SaveRoutineHelper { ), title: const Text('Tab to run'), ), + if (state.isAutomation) + ...state.ifItems.map((item) { + final functions = state.selectedFunctions[ + item['uniqueCustomId']] ?? + []; + return ListTile( + leading: SvgPicture.asset( + item['imagePath'], + width: 22, + height: 22, + ), + title: Text(item['title'], + style: const TextStyle(fontSize: 14)), + subtitle: Wrap( + children: functions + .map((f) => Text( + '${f.operationName}: ${f.value}, ', + style: const TextStyle( + color: ColorsManager + .grayColor, + fontSize: 8), + overflow: + TextOverflow.ellipsis, + maxLines: 3, + )) + .toList(), + ), + ); + }), ], ), ), diff --git a/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart new file mode 100644 index 00000000..f7bb1fb5 --- /dev/null +++ b/lib/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart @@ -0,0 +1,173 @@ + +import 'dart:convert'; + +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; + +class CreateAutomationModel { + String unitUuid; + String automationName; + String decisionExpr; + EffectiveTime effectiveTime; + List conditions; + List actions; + + CreateAutomationModel({ + required this.unitUuid, + required this.automationName, + required this.decisionExpr, + required this.effectiveTime, + required this.conditions, + required this.actions, + }); + + CreateAutomationModel copyWith({ + String? unitUuid, + String? automationName, + String? decisionExpr, + EffectiveTime? effectiveTime, + List? conditions, + List? actions, + }) { + return CreateAutomationModel( + unitUuid: unitUuid ?? this.unitUuid, + automationName: automationName ?? this.automationName, + decisionExpr: decisionExpr ?? this.decisionExpr, + effectiveTime: effectiveTime ?? this.effectiveTime, + conditions: conditions ?? this.conditions, + actions: actions ?? this.actions, + ); + } + + Map toMap([String? automationId]) { + return { + if (automationId == null) 'spaceUuid': unitUuid, + 'automationName': automationName, + 'decisionExpr': decisionExpr, + 'effectiveTime': effectiveTime.toMap(), + 'conditions': conditions.map((x) => x.toMap()).toList(), + 'actions': actions.map((x) => x.toMap()).toList(), + }; + } + + factory CreateAutomationModel.fromMap(Map map) { + return CreateAutomationModel( + unitUuid: map['spaceUuid'] ?? '', + automationName: map['automationName'] ?? '', + decisionExpr: map['decisionExpr'] ?? '', + effectiveTime: EffectiveTime.fromMap(map['effectiveTime']), + conditions: List.from( + map['conditions']?.map((x) => CreateCondition.fromMap(x))), + actions: List.from( + map['actions']?.map((x) => CreateSceneAction.fromMap(x))), + ); + } + + String toJson([String? automationId]) => json.encode(toMap(automationId)); + + factory CreateAutomationModel.fromJson(String source) => + CreateAutomationModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'CreateAutomationModel(unitUuid: $unitUuid, automationName: $automationName, decisionExpr: $decisionExpr, effectiveTime: $effectiveTime, conditions: $conditions, actions: $actions)'; + } +} + +class EffectiveTime { + String start; + String end; + String loops; + + EffectiveTime({ + required this.start, + required this.end, + required this.loops, + }); + + Map toMap() { + return { + 'start': start, + 'end': end, + 'loops': loops, + }; + } + + factory EffectiveTime.fromMap(Map map) { + return EffectiveTime( + start: map['start'] ?? '', + end: map['end'] ?? '', + loops: map['loops'] ?? '', + ); + } + + @override + String toString() => 'EffectiveTime(start: $start, end: $end, loops: $loops)'; +} + +class CreateCondition { + int code; + String entityId; + String entityType; + ConditionExpr expr; + + CreateCondition({ + required this.code, + required this.entityId, + required this.entityType, + required this.expr, + }); + + Map toMap() { + return { + 'code': code, + 'entityId': entityId, + 'entityType': entityType, + 'expr': expr.toMap(), + }; + } + + factory CreateCondition.fromMap(Map map) { + return CreateCondition( + code: map['code'] ?? 0, + entityId: map['entityId'] ?? '', + entityType: map['entityType'] ?? '', + expr: ConditionExpr.fromMap(map['expr']), + ); + } + + @override + String toString() => + 'CreateCondition(code: $code, entityId: $entityId, entityType: $entityType, expr: $expr)'; +} + +class ConditionExpr { + String statusCode; + String comparator; + dynamic statusValue; + + ConditionExpr({ + required this.statusCode, + required this.comparator, + required this.statusValue, + }); + + Map toMap() { + return { + 'statusCode': statusCode, + 'comparator': comparator, + 'statusValue': statusValue, + }; + } + + factory ConditionExpr.fromMap(Map map) { + return ConditionExpr( + statusCode: map['statusCode'] ?? '', + comparator: map['comparator'] ?? '', + statusValue: map['statusValue'], + ); + } + + @override + String toString() => + 'ConditionExpr(statusCode: $statusCode, comparator: $comparator, statusValue: $statusValue)'; +} diff --git a/lib/pages/routiens/models/create_scene/create_scene_model.dart b/lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart similarity index 100% rename from lib/pages/routiens/models/create_scene/create_scene_model.dart rename to lib/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart diff --git a/lib/pages/routiens/widgets/dialog_footer.dart b/lib/pages/routiens/widgets/dialog_footer.dart index 90c6baec..15db9732 100644 --- a/lib/pages/routiens/widgets/dialog_footer.dart +++ b/lib/pages/routiens/widgets/dialog_footer.dart @@ -5,12 +5,14 @@ class DialogFooter extends StatelessWidget { final VoidCallback onCancel; final VoidCallback? onConfirm; final bool isConfirmEnabled; + final int? dialogWidth; const DialogFooter({ Key? key, required this.onCancel, required this.onConfirm, required this.isConfirmEnabled, + this.dialogWidth, }) : super(key: key); @override @@ -26,24 +28,23 @@ class DialogFooter extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildFooterButton( - context, - 'Cancel', - onCancel, - width: isConfirmEnabled ? 299 : 179, - ), - if (isConfirmEnabled) - Row( - children: [ - Container(width: 1, height: 50, color: ColorsManager.greyColor), - _buildFooterButton( - context, - 'Confirm', - onConfirm, - width: 299, - ), - ], + Expanded( + child: _buildFooterButton( + context, + 'Cancel', + onCancel, ), + ), + if (isConfirmEnabled) ...[ + Container(width: 1, height: 50, color: ColorsManager.greyColor), + Expanded( + child: _buildFooterButton( + context, + 'Confirm', + onConfirm, + ), + ), + ], ], ), ); @@ -52,14 +53,12 @@ class DialogFooter extends StatelessWidget { Widget _buildFooterButton( BuildContext context, String text, - VoidCallback? onTap, { - required double width, - }) { + VoidCallback? onTap, + ) { return GestureDetector( onTap: onTap, child: SizedBox( height: 50, - width: width, child: Center( child: Text( text, diff --git a/lib/pages/routiens/widgets/if_container.dart b/lib/pages/routiens/widgets/if_container.dart index cfb1b27c..57577f64 100644 --- a/lib/pages/routiens/widgets/if_container.dart +++ b/lib/pages/routiens/widgets/if_container.dart @@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.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'; import 'package:uuid/uuid.dart'; class IfContainer extends StatelessWidget { @@ -21,11 +23,19 @@ class IfContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('IF', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('IF', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold)), + if (state.isAutomation) + AutomationOperatorSelector( + selectedOperator: state.selectedAutomationOperator), + ], + ), const SizedBox(height: 16), - if (context.read().isTabToRun) + if (state.isTabToRun) const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -36,7 +46,7 @@ class IfContainer extends StatelessWidget { ), ], ), - if (!context.read().isTabToRun) + if (!state.isTabToRun) Wrap( spacing: 8, runSpacing: 8, @@ -64,35 +74,12 @@ class IfContainer extends StatelessWidget { }, onWillAccept: (data) => data != null, onAccept: (data) async { - // final uniqueCustomId = const Uuid().v4(); - - // final mutableData = Map.from(data); - // mutableData['uniqueCustomId'] = uniqueCustomId; - - // if (!context.read().isTabToRun) { - // if (data['deviceId'] == 'tab_to_run') { - // context.read().add(AddToIfContainer(data, true)); - // } else { - // final result = - // await DeviceDialogHelper.showDeviceDialog(context, mutableData); - // if (result != null) { - // context - // .read() - // .add(AddToIfContainer(mutableData, false)); - // } else if (!['AC', '1G', '2G', '3G'] - // .contains(data['productType'])) { - // context - // .read() - // .add(AddToIfContainer(mutableData, false)); - // } - // } - //} final uniqueCustomId = const Uuid().v4(); final mutableData = Map.from(data); mutableData['uniqueCustomId'] = uniqueCustomId; - if (!context.read().isTabToRun) { + if (!state.isTabToRun) { if (mutableData['deviceId'] == 'tab_to_run') { context .read() @@ -119,3 +106,77 @@ class IfContainer extends StatelessWidget { ); } } + +class AutomationOperatorSelector extends StatelessWidget { + const AutomationOperatorSelector({ + super.key, + required this.selectedOperator, + }); + + final String selectedOperator; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: selectedOperator == 'or' + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: Text( + 'Any condition is met', + style: context.textTheme.bodyMedium?.copyWith( + color: selectedOperator == 'or' + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + ), + ), + onPressed: () { + context + .read() + .add(const ChangeAutomationOperator(operator: 'or')); + }, + ), + Container( + width: 3, + height: 24, + color: ColorsManager.dividerColor, + ), + TextButton( + style: TextButton.styleFrom( + backgroundColor: selectedOperator == 'and' + ? ColorsManager.dialogBlueTitle + : ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0), + ), + ), + child: Text( + 'All condition is met', + style: context.textTheme.bodyMedium?.copyWith( + color: selectedOperator == 'and' + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + ), + ), + onPressed: () { + context + .read() + .add(const ChangeAutomationOperator(operator: 'and')); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart new file mode 100644 index 00000000..c494622a --- /dev/null +++ b/lib/pages/routiens/widgets/routine_dialogs/automation_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/models/device_functions.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routiens/widgets/dialog_footer.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class AutomationDialog extends StatefulWidget { + final String automationName; + final String automationId; + final String uniqueCustomId; + + const AutomationDialog({ + super.key, + required this.automationName, + required this.automationId, + required this.uniqueCustomId, + }); + + @override + _AutomationDialogState createState() => _AutomationDialogState(); +} + +class _AutomationDialogState extends State { + bool _isEnabled = true; + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + width: 400, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader(widget.automationName), + const SizedBox(height: 16), + ListTile( + leading: SvgPicture.asset(Assets.acPower, width: 24, height: 24), + title: const Text('Enable'), + trailing: Radio( + value: true, + groupValue: _isEnabled, + onChanged: (bool? value) { + setState(() { + _isEnabled = value!; + }); + }, + ), + ), + ListTile( + leading: + SvgPicture.asset(Assets.acPowerOff, width: 24, height: 24), + title: const Text('Disable'), + trailing: Radio( + value: false, + groupValue: _isEnabled, + onChanged: (bool? value) { + setState(() { + _isEnabled = value!; + }); + }, + ), + ), + const SizedBox(height: 16), + DialogFooter( + onConfirm: () { + context.read().add( + AddFunctionToRoutine( + [ + DeviceFunctionData( + entityId: widget.automationId, + functionCode: 'automation', + value: _isEnabled, + operationName: 'Automation', + ), + ], + widget.uniqueCustomId, + ), + ); + Navigator.of(context).pop(true); + }, + onCancel: () => Navigator.of(context).pop(false), + isConfirmEnabled: true, + dialogWidth: 400, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart index 1191ac58..6195a931 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/delay_dialog.dart @@ -8,13 +8,25 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart'; class DelayHelper { static Future?> showDelayPickerDialog( - BuildContext context, String uniqueCustomId) async { + BuildContext context, Map data) async { int hours = 0; int minutes = 0; return showDialog?>( context: context, builder: (BuildContext context) { + final routineBloc = context.read(); + int totalSec = 0; + + final selectedFunctionData = + routineBloc.state.selectedFunctions[data['uniqueCustomId']] ?? []; + + if (selectedFunctionData.isNotEmpty) { + totalSec = selectedFunctionData[0].value; + // Convert seconds to hours and minutes + hours = totalSec ~/ 3600; + minutes = (totalSec % 3600) ~/ 60; + } return AlertDialog( contentPadding: EdgeInsets.zero, content: Container( @@ -31,8 +43,7 @@ class DelayHelper { Expanded( child: CupertinoTimerPicker( mode: CupertinoTimerPickerMode.hm, - initialTimerDuration: - Duration(hours: hours, minutes: minutes), + initialTimerDuration: Duration(hours: hours, minutes: minutes), onTimerDurationChanged: (Duration newDuration) { hours = newDuration.inHours; minutes = newDuration.inMinutes % 60; @@ -54,7 +65,7 @@ class DelayHelper { value: totalSeconds, ) ], - uniqueCustomId, + data['uniqueCustomId'], )); Navigator.pop(context, { diff --git a/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart index 621a5219..4e2bdde6 100644 --- a/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart +++ b/lib/pages/routiens/widgets/routine_dialogs/setting_dialog.dart @@ -18,7 +18,7 @@ class SettingHelper { return showDialog( context: context, builder: (BuildContext context) { - final isAutomation = context.read().isAutomation; + final isAutomation = context.read().state.isAutomation; return BlocProvider( create: (_) => SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')), diff --git a/lib/pages/routiens/widgets/routine_search_and_buttons.dart b/lib/pages/routiens/widgets/routine_search_and_buttons.dart index 01352bf8..5bf9db8e 100644 --- a/lib/pages/routiens/widgets/routine_search_and_buttons.dart +++ b/lib/pages/routiens/widgets/routine_search_and_buttons.dart @@ -45,7 +45,9 @@ class RoutineSearchAndButtons extends StatelessWidget { isRequired: true, width: 450, onChanged: (value) { - // context.read().add(SearchRoutines(value)); + context + .read() + .add(SetRoutineName(value)); }, ), ), diff --git a/lib/pages/routiens/widgets/scenes_and_automations.dart b/lib/pages/routiens/widgets/scenes_and_automations.dart index 7a26979c..746795af 100644 --- a/lib/pages/routiens/widgets/scenes_and_automations.dart +++ b/lib/pages/routiens/widgets/scenes_and_automations.dart @@ -34,11 +34,9 @@ class _ScenesAndAutomationsState extends State { children: scenes.asMap().entries.map((entry) { final scene = entry.value; if (state.searchText != null && state.searchText!.isNotEmpty) { - return scene.name - .toLowerCase() - .contains(state.searchText!.toLowerCase()) + return scene.name.toLowerCase().contains(state.searchText!.toLowerCase()) ? DraggableCard( - imagePath: Assets.logo, + imagePath: scene.icon ?? Assets.loginLogo, title: scene.name, deviceData: { 'deviceId': scene.id, diff --git a/lib/pages/routiens/widgets/then_container.dart b/lib/pages/routiens/widgets/then_container.dart index 2bc65b0e..9d729327 100644 --- a/lib/pages/routiens/widgets/then_container.dart +++ b/lib/pages/routiens/widgets/then_container.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/routiens/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/automation_dialog.dart'; import 'package:syncrow_web/pages/routiens/widgets/routine_dialogs/delay_dialog.dart'; import 'package:syncrow_web/pages/routiens/helper/dialog_helper/device_dialog_helper.dart'; import 'package:syncrow_web/pages/routiens/widgets/dragable_card.dart'; @@ -34,36 +35,113 @@ class ThenContainer extends StatelessWidget { runSpacing: 8, children: List.generate( state.thenItems.length, - (index) => DraggableCard( - imagePath: - state.thenItems[index]['imagePath'] ?? '', - title: state.thenItems[index]['title'] ?? '', - deviceData: state.thenItems[index], - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 8), - isFromThen: true, - isFromIf: false, - onRemove: () { - context.read().add( - RemoveDragCard( - index: index, isFromThen: true)); + (index) => GestureDetector( + onTap: () async { + if (state.thenItems[index]['deviceId'] == + 'delay') { + final result = await DelayHelper + .showDelayPickerDialog( + context, state.thenItems[index]); + + if (result != null) { + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.delay, + 'title': 'Delay', + })); + } + return; + } + + final result = await DeviceDialogHelper + .showDeviceDialog( + context, state.thenItems[index]); + + if (result != null) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } else if (!['AC', '1G', '2G', '3G'] + .contains(state.thenItems[index] + ['productType'])) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } }, + child: DraggableCard( + imagePath: state.thenItems[index] + ['imagePath'] ?? + '', + title: + state.thenItems[index]['title'] ?? '', + deviceData: state.thenItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: true, + isFromIf: false, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, isFromThen: true)); + }, + ), ))), ], ), ), ); }, - onWillAccept: (data) => data != null, - onAccept: (data) async { - final uniqueCustomId = const Uuid().v4(); + // onWillAcceptWithDetails: (data) { + // if (data == null) return false; + // return data.data; - final mutableData = Map.from(data); + // // if (state.isTabToRun) { + // // return data.data['type'] == 'automation'; + // // } + + // // if (state.isAutomation) { + // // return data.data['type'] == 'scene' || + // // data.data['type'] == 'automation'; + // // } + + // // return data.data['deviceId'] != null; + // }, + onAcceptWithDetails: (data) async { + final uniqueCustomId = const Uuid().v4(); + final mutableData = Map.from(data.data); mutableData['uniqueCustomId'] = uniqueCustomId; + if (mutableData['type'] == 'scene') { + context.read().add(AddToThenContainer(mutableData)); + return; + } + + if (mutableData['type'] == 'automation') { + final result = await showDialog( + context: context, + builder: (BuildContext context) => AutomationDialog( + automationName: mutableData['name'] ?? 'Automation', + automationId: mutableData['deviceId'] ?? '', + uniqueCustomId: uniqueCustomId, + ), + ); + + if (result != null) { + context.read().add(AddToThenContainer({ + ...mutableData, + 'imagePath': Assets.automation, + 'title': mutableData['name'], + })); + } + return; + } + if (mutableData['deviceId'] == 'delay') { - final result = await DelayHelper.showDelayPickerDialog( - context, mutableData['uniqueCustomId']); + final result = + await DelayHelper.showDelayPickerDialog(context, mutableData); if (result != null) { context.read().add(AddToThenContainer({ diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index 0ce106b2..f92f212f 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/routiens/models/create_scene/create_scene_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_automation_model.dart'; +import 'package:syncrow_web/pages/routiens/models/create_scene_and_autoamtion/create_scene_model.dart'; import 'package:syncrow_web/pages/routiens/models/icon_model.dart'; import 'package:syncrow_web/pages/routiens/models/routine_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -29,23 +30,23 @@ class SceneApi { } } // -// // create automation -// static Future> createAutomation( -// CreateAutomationModel createAutomationModel) async { -// try { -// final response = await _httpService.post( -// path: ApiEndpoints.createAutomation, -// body: createAutomationModel.toMap(), -// showServerMessage: false, -// expectedResponseModel: (json) { -// return json; -// }, -// ); -// return response; -// } catch (e) { -// rethrow; -// } -// } +// create automation + static Future> createAutomation( + CreateAutomationModel createAutomationModel) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.createAutomation, + body: createAutomationModel.toMap(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } static Future> getIcon() async { final response = await _httpService.get( diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index a5decc3b..4bc0e752 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -54,4 +54,5 @@ abstract class ApiEndpoints { static const String getSpaceAutomation = '/automation/{unitUuid}'; static const String getIconScene = '/scene/icon'; static const String createScene = '/scene/tap-to-run'; + static const String createAutomation = '/automation'; }