Merge pull request #51 from SyncrowIOT/routines_1

Routines 1
This commit is contained in:
Abdullah
2024-11-26 12:24:31 +03:00
committed by GitHub
18 changed files with 862 additions and 197 deletions

View File

@ -88,31 +88,32 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
);
}),
rightBody: const NavigateHomeGridView(),
scaffoldBody: BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
builder: (context, state) {
if (state is SelectedTabState && state.selectedTab) {
return const RoutinesView();
}
if (state is ShowCreateRoutineState && state.showCreateRoutine) {
return const CreateNewRoutineView();
}
scaffoldBody: CreateNewRoutineView(),
// BlocBuilder<SwitchTabsBloc, SwitchTabsState>(
// builder: (context, state) {
// if (state is SelectedTabState && state.selectedTab) {
// return const RoutinesView();
// }
// if (state is ShowCreateRoutineState && state.showCreateRoutine) {
// return const CreateNewRoutineView();
// }
return BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
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<DeviceManagementBloc, DeviceManagementState>(
// 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'));
// }
// },
// );
// }),
),
);
}

View File

@ -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<EffectPeriodEvent, EffectPeriodState> {
final daysMap = {
@ -49,9 +52,9 @@ class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
break;
}
// BlocProvider.of<CreateSceneBloc>(NavigationService.navigatorKey.currentContext!).add(
// EffectiveTimePeriodEvent(
// EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary)));
BlocProvider.of<RoutineBloc>(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<EffectPeriodEvent, EffectPeriodState> {
final newDaysBinary = daysList.join();
emit(state.copyWith(selectedDaysBinary: newDaysBinary));
// BlocProvider.of<CreateSceneBloc>(NavigationService.navigatorKey.currentContext!).add(
// EffectiveTimePeriodEvent(EffectiveTime(
// start: state.customStartTime ?? '00:00',
// end: state.customEndTime ?? '23:59',
// loops: newDaysBinary)));
BlocProvider.of<RoutineBloc>(NavigationService.navigatorKey.currentContext!).add(
EffectiveTimePeriodEvent(EffectiveTime(
start: state.customStartTime ?? '00:00',
end: state.customEndTime ?? '23:59',
loops: newDaysBinary)));
}
void _onSetCustomTime(SetCustomTime event, Emitter<EffectPeriodState> emit) {
@ -94,9 +97,9 @@ class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
emit(
state.copyWith(customStartTime: startTime, customEndTime: endTime, selectedPeriod: period));
// BlocProvider.of<CreateSceneBloc>(NavigationService.navigatorKey.currentContext!).add(
// EffectiveTimePeriodEvent(
// EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary)));
BlocProvider.of<RoutineBloc>(NavigationService.navigatorKey.currentContext!).add(
EffectiveTimePeriodEvent(
EffectiveTime(start: startTime, end: endTime, loops: state.selectedDaysBinary)));
}
void _onResetEffectivePeriod(ResetEffectivePeriod event, Emitter<EffectPeriodState> emit) {
@ -106,8 +109,8 @@ class EffectPeriodBloc extends Bloc<EffectPeriodEvent, EffectPeriodState> {
customEndTime: '23:59',
selectedDaysBinary: '1111111'));
// BlocProvider.of<CreateSceneBloc>(NavigationService.navigatorKey.currentContext!).add(
// EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '1111111')));
BlocProvider.of<RoutineBloc>(NavigationService.navigatorKey.currentContext!).add(
EffectiveTimePeriodEvent(EffectiveTime(start: '00:00', end: '23:59', loops: '1111111')));
}
void _onResetDays(ResetDays event, Emitter<EffectPeriodState> emit) {

View File

@ -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<RoutineEvent, RoutineState> {
bool isAutomation = false;
bool isTabToRun = false;
// bool isAutomation = false;
// bool isTabToRun = false;
RoutineBloc() : super(const RoutineState()) {
on<AddToIfContainer>(_onAddToIfContainer);
@ -27,28 +28,45 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
on<AddSelectedIcon>(_onAddSelectedIcon);
on<CreateSceneEvent>(_onCreateScene);
on<RemoveDragCard>(_onRemoveDragCard);
on<ChangeAutomationOperator>(_changeOperatorOperator);
on<EffectiveTimePeriodEvent>(_onEffectiveTimeEvent);
on<CreateAutomationEvent>(_onCreateAutomation);
on<SetRoutineName>(_onSetRoutineName);
// on<RemoveFunction>(_onRemoveFunction);
// on<ClearFunctions>(_onClearFunctions);
}
void _onAddToIfContainer(AddToIfContainer event, Emitter<RoutineState> emit) {
final updatedIfItems = List<Map<String, dynamic>>.from(state.ifItems)..add(event.item);
final updatedIfItems = List<Map<String, dynamic>>.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<RoutineState> emit) {
final updatedThenItems = List<Map<String, dynamic>>.from(state.thenItems)..add(event.item);
emit(state.copyWith(thenItems: updatedThenItems));
void _onAddToThenContainer(
AddToThenContainer event, Emitter<RoutineState> emit) {
final currentItems = List<Map<String, dynamic>>.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<RoutineState> emit) {
void _onAddFunctionsToRoutine(
AddFunctionToRoutine event, Emitter<RoutineState> emit) {
try {
if (event.functions.isEmpty) return;
@ -57,9 +75,11 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
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<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(LoadScenes event, Emitter<RoutineState> emit) async {
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
@ -87,7 +108,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadAutomation(LoadAutomation event, Emitter<RoutineState> emit) async {
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
@ -106,13 +128,15 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onSearchRoutines(SearchRoutines event, Emitter<RoutineState> emit) async {
FutureOr<void> _onSearchRoutines(
SearchRoutines event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(searchText: event.query));
}
FutureOr<void> _onAddSelectedIcon(AddSelectedIcon event, Emitter<RoutineState> emit) {
FutureOr<void> _onAddSelectedIcon(
AddSelectedIcon event, Emitter<RoutineState> emit) {
emit(state.copyWith(selectedIcon: event.icon));
}
@ -121,7 +145,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
return actions.first['deviceId'] == 'delay';
}
Future<void> _onCreateScene(CreateSceneEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateScene(
CreateSceneEvent event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
if (_isFirstActionDelay(state.thenItems)) {
@ -136,7 +161,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
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<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onRemoveDragCard(RemoveDragCard event, Emitter<RoutineState> emit) {
Future<void> _onCreateAutomation(
CreateAutomationEvent event, Emitter<RoutineState> 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<CreateCondition>()
.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<CreateSceneAction>()
.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<void> _onRemoveDragCard(
RemoveDragCard event, Emitter<RoutineState> emit) {
if (event.isFromThen) {
/// remove element from thenItems at specific index
final thenItems = List<Map<String, dynamic>>.from(state.thenItems);
thenItems.removeAt(event.index);
emit(state.copyWith(thenItems: thenItems));
@ -203,4 +339,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
emit(state.copyWith(ifItems: ifItems));
}
}
FutureOr<void> _changeOperatorOperator(
ChangeAutomationOperator event, Emitter<RoutineState> emit) {
emit(state.copyWith(
selectedAutomationOperator: event.operator,
));
}
FutureOr<void> _onEffectiveTimeEvent(
EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
emit(state.copyWith(effectiveTime: event.effectiveTime));
}
FutureOr<void> _onSetRoutineName(
SetRoutineName event, Emitter<RoutineState> emit) {
emit(state.copyWith(routineName: event.name));
}
}

View File

@ -87,4 +87,38 @@ class RemoveDragCard extends RoutineEvent {
List<Object> get props => [index];
}
class ChangeAutomationOperator extends RoutineEvent {
final String operator;
const ChangeAutomationOperator({required this.operator});
@override
List<Object> get props => [operator];
}
class EffectiveTimePeriodEvent extends RoutineEvent {
final EffectiveTime effectiveTime;
const EffectiveTimePeriodEvent(this.effectiveTime);
@override
List<Object> 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<Object> get props => [];
}
class SetRoutineName extends RoutineEvent {
final String name;
const SetRoutineName(this.name);
@override
List<Object> get props => [name];
}
class ClearFunctions extends RoutineEvent {}

View File

@ -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<Map<String, dynamic>>? ifItems,
List<Map<String, dynamic>>? thenItems,
List<ScenesModel>? scenes,
List<ScenesModel>? automations,
Map<String, List<DeviceFunctionData>>? selectedFunctions,
bool? isLoading,
String? errorMessage,
String? routineName,
String? selectedIcon,
String? loadAutomationErrorMessage,
String? loadScenesErrorMessage,
String? searchText}) {
RoutineState copyWith({
List<Map<String, dynamic>>? ifItems,
List<Map<String, dynamic>>? thenItems,
List<ScenesModel>? scenes,
List<ScenesModel>? automations,
Map<String, List<DeviceFunctionData>>? 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
];
}

View File

@ -43,7 +43,7 @@ class SaveRoutineHelper {
),
),
const SizedBox(height: 8),
if (context.read<RoutineBloc>().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(),
),
);
}),
],
),
),

View File

@ -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<CreateCondition> conditions;
List<CreateSceneAction> 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<CreateCondition>? conditions,
List<CreateSceneAction>? 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<String, dynamic> 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<String, dynamic> map) {
return CreateAutomationModel(
unitUuid: map['spaceUuid'] ?? '',
automationName: map['automationName'] ?? '',
decisionExpr: map['decisionExpr'] ?? '',
effectiveTime: EffectiveTime.fromMap(map['effectiveTime']),
conditions: List<CreateCondition>.from(
map['conditions']?.map((x) => CreateCondition.fromMap(x))),
actions: List<CreateSceneAction>.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<String, dynamic> toMap() {
return {
'start': start,
'end': end,
'loops': loops,
};
}
factory EffectiveTime.fromMap(Map<String, dynamic> 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<String, dynamic> toMap() {
return {
'code': code,
'entityId': entityId,
'entityType': entityType,
'expr': expr.toMap(),
};
}
factory CreateCondition.fromMap(Map<String, dynamic> 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<String, dynamic> toMap() {
return {
'statusCode': statusCode,
'comparator': comparator,
'statusValue': statusValue,
};
}
factory ConditionExpr.fromMap(Map<String, dynamic> map) {
return ConditionExpr(
statusCode: map['statusCode'] ?? '',
comparator: map['comparator'] ?? '',
statusValue: map['statusValue'],
);
}
@override
String toString() =>
'ConditionExpr(statusCode: $statusCode, comparator: $comparator, statusValue: $statusValue)';
}

View File

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

View File

@ -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<RoutineBloc>().isTabToRun)
if (state.isTabToRun)
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -36,7 +46,7 @@ class IfContainer extends StatelessWidget {
),
],
),
if (!context.read<RoutineBloc>().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<String, dynamic>.from(data);
// mutableData['uniqueCustomId'] = uniqueCustomId;
// if (!context.read<RoutineBloc>().isTabToRun) {
// if (data['deviceId'] == 'tab_to_run') {
// context.read<RoutineBloc>().add(AddToIfContainer(data, true));
// } else {
// final result =
// await DeviceDialogHelper.showDeviceDialog(context, mutableData);
// if (result != null) {
// context
// .read<RoutineBloc>()
// .add(AddToIfContainer(mutableData, false));
// } else if (!['AC', '1G', '2G', '3G']
// .contains(data['productType'])) {
// context
// .read<RoutineBloc>()
// .add(AddToIfContainer(mutableData, false));
// }
// }
//}
final uniqueCustomId = const Uuid().v4();
final mutableData = Map<String, dynamic>.from(data);
mutableData['uniqueCustomId'] = uniqueCustomId;
if (!context.read<RoutineBloc>().isTabToRun) {
if (!state.isTabToRun) {
if (mutableData['deviceId'] == 'tab_to_run') {
context
.read<RoutineBloc>()
@ -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<RoutineBloc>()
.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<RoutineBloc>()
.add(const ChangeAutomationOperator(operator: 'and'));
},
),
],
),
);
}
}

View File

@ -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<AutomationDialog> {
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<bool>(
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<bool>(
value: false,
groupValue: _isEnabled,
onChanged: (bool? value) {
setState(() {
_isEnabled = value!;
});
},
),
),
const SizedBox(height: 16),
DialogFooter(
onConfirm: () {
context.read<RoutineBloc>().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,
),
],
),
),
);
}
}

View File

@ -8,13 +8,25 @@ import 'package:syncrow_web/pages/routiens/widgets/dialog_header.dart';
class DelayHelper {
static Future<Map<String, dynamic>?> showDelayPickerDialog(
BuildContext context, String uniqueCustomId) async {
BuildContext context, Map<String, dynamic> data) async {
int hours = 0;
int minutes = 0;
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext context) {
final routineBloc = context.read<RoutineBloc>();
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, {

View File

@ -18,7 +18,7 @@ class SettingHelper {
return showDialog<String>(
context: context,
builder: (BuildContext context) {
final isAutomation = context.read<RoutineBloc>().isAutomation;
final isAutomation = context.read<RoutineBloc>().state.isAutomation;
return BlocProvider(
create: (_) =>
SettingBloc()..add(InitialEvent(selectedIcon: iconId ?? '')),

View File

@ -45,7 +45,9 @@ class RoutineSearchAndButtons extends StatelessWidget {
isRequired: true,
width: 450,
onChanged: (value) {
// context.read<RoutineBloc>().add(SearchRoutines(value));
context
.read<RoutineBloc>()
.add(SetRoutineName(value));
},
),
),

View File

@ -34,11 +34,9 @@ class _ScenesAndAutomationsState extends State<ScenesAndAutomations> {
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,

View File

@ -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<RoutineBloc>().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<RoutineBloc>()
.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<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
} else if (!['AC', '1G', '2G', '3G']
.contains(state.thenItems[index]
['productType'])) {
context.read<RoutineBloc>().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<RoutineBloc>().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<String, dynamic>.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<String, dynamic>.from(data.data);
mutableData['uniqueCustomId'] = uniqueCustomId;
if (mutableData['type'] == 'scene') {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
return;
}
if (mutableData['type'] == 'automation') {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) => AutomationDialog(
automationName: mutableData['name'] ?? 'Automation',
automationId: mutableData['deviceId'] ?? '',
uniqueCustomId: uniqueCustomId,
),
);
if (result != null) {
context.read<RoutineBloc>().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<RoutineBloc>().add(AddToThenContainer({

View File

@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<List<IconModel>> getIcon() async {
final response = await _httpService.get(

View File

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