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 808a683f..0a1e5643 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_tag import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/flush/flush_functions.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/one_gang_switch.dart'; @@ -359,6 +360,14 @@ SOS uuid: uuid ?? '', name: name ?? '', ); + case 'CUR': + return [ + ControlFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'BOTH', + ) + ]; case 'NCPS': return [ FlushPresenceDelayFunction( @@ -441,15 +450,10 @@ SOS VoltageCStatusFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), CurrentCStatusFunction( - deviceId: uuid ?? '', - deviceName: name ?? '', - type: 'IF'), + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), PowerFactorCStatusFunction( - deviceId: uuid ?? '', - deviceName: name ?? '', - type: 'IF'), + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), ]; - default: return []; } diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart index df4683d8..e8aa4d37 100644 --- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/curtain_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart'; @@ -26,7 +27,7 @@ class DeviceDialogHelper { final result = await _getDialogForDeviceType( dialogType: dialogType, context: context, - productType: data['productType'], + productType: data['productType'] as String, data: data, functions: functions, removeComparetors: removeComparetors, @@ -65,7 +66,14 @@ class DeviceDialogHelper { removeComparetors: removeComparetors, dialogType: dialogType, ); - + case 'CUR': + return CurtainHelper.showControlDialog( + dialogType: dialogType, + context: context, + functions: functions, + uniqueCustomId: data['uniqueCustomId'], + device: data['device'], + ); case '1G': return OneGangSwitchHelper.showSwitchFunctionsDialog( dialogType: dialogType, diff --git a/lib/pages/routines/helper/save_routine_helper.dart b/lib/pages/routines/helper/save_routine_helper.dart index f8b52dab..2b506620 100644 --- a/lib/pages/routines/helper/save_routine_helper.dart +++ b/lib/pages/routines/helper/save_routine_helper.dart @@ -17,9 +17,10 @@ class SaveRoutineHelper { builder: (context) { return BlocBuilder( builder: (context, state) { - final selectedConditionLabel = state.selectedAutomationOperator == 'and' - ? 'All Conditions are met' - : 'Any Condition is met'; + final selectedConditionLabel = + state.selectedAutomationOperator == 'and' + ? 'All Conditions are met' + : 'Any Condition is met'; return AlertDialog( contentPadding: EdgeInsets.zero, @@ -37,10 +38,11 @@ class SaveRoutineHelper { Text( 'Create a scene: ${state.routineName ?? ""}', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - fontWeight: FontWeight.bold, - ), + style: + Theme.of(context).textTheme.headlineMedium!.copyWith( + color: ColorsManager.primaryColorWithOpacity, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 18), _buildDivider(), @@ -58,7 +60,8 @@ class SaveRoutineHelper { _buildIfConditions(state, context), Container( width: 1, - color: ColorsManager.greyColor.withValues(alpha: 0.8), + color: ColorsManager.greyColor + .withValues(alpha: 0.8), ), _buildThenActions(state, context), ], @@ -97,7 +100,8 @@ class SaveRoutineHelper { child: Row( spacing: 16, children: [ - Expanded(child: Text('IF: $selectedConditionLabel', style: textStyle)), + Expanded( + child: Text('IF: $selectedConditionLabel', style: textStyle)), const Expanded(child: Text('THEN:', style: textStyle)), ], ), @@ -109,7 +113,7 @@ class SaveRoutineHelper { spacing: 16, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - DialogFooterButton( + DialogFooterButton( text: 'Back', onTap: () => Navigator.pop(context), ), @@ -143,7 +147,8 @@ class SaveRoutineHelper { child: ListView( // shrinkWrap: true, children: state.thenItems.map((item) { - final functions = state.selectedFunctions[item['uniqueCustomId']] ?? []; + final functions = + state.selectedFunctions[item['uniqueCustomId']] ?? []; return functionRow(item, context, functions); }).toList(), ), @@ -203,19 +208,20 @@ class SaveRoutineHelper { ), ), child: Center( - child: item['type'] == 'tap_to_run' || item['type'] == 'scene' - ? Image.memory( - base64Decode(item['icon']), - width: 12, - height: 22, - fit: BoxFit.scaleDown, - ) - : SvgPicture.asset( - item['imagePath'], - width: 12, - height: 12, - fit: BoxFit.scaleDown, - ), + child: + item['type'] == 'tap_to_run' || item['type'] == 'scene' + ? Image.memory( + base64Decode(item['icon']), + width: 12, + height: 22, + fit: BoxFit.scaleDown, + ) + : SvgPicture.asset( + item['imagePath'], + width: 12, + height: 12, + fit: BoxFit.scaleDown, + ), ), ), Flexible( diff --git a/lib/pages/routines/models/curtain/curtain_function.dart b/lib/pages/routines/models/curtain/curtain_function.dart new file mode 100644 index 00000000..09e3b7e7 --- /dev/null +++ b/lib/pages/routines/models/curtain/curtain_function.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/device_managment/curtain/model/curtain_model.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart' + show DeviceFunction; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class CurtainFunction extends DeviceFunction { + final String type; + CurtainFunction({ + required super.deviceId, + required super.deviceName, + required this.type, + required super.code, + required super.operationName, + required super.icon, + }); + List getOperationalValues(); +} + +class ControlFunction extends CurtainFunction { + ControlFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + super.code = 'control', + super.operationName = 'Control', + super.icon = Assets.curtain, + }); + + @override + List getOperationalValues() => [ + CurtainOperationalValue( + icon: Assets.curtain, + description: 'OPEN', + value: 'open', + ), + CurtainOperationalValue( + icon: Assets.curtain, + description: 'STOP', + value: 'stop', + ), + CurtainOperationalValue( + icon: Assets.curtain, + description: 'CLOSE', + value: 'close', + ) + ]; +} diff --git a/lib/pages/routines/models/curtain/curtain_opertion_value.dart b/lib/pages/routines/models/curtain/curtain_opertion_value.dart new file mode 100644 index 00000000..faa81cfd --- /dev/null +++ b/lib/pages/routines/models/curtain/curtain_opertion_value.dart @@ -0,0 +1,11 @@ +class CurtainOperationalValue { + final String icon; + final String description; + final String value; + + CurtainOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart index da77c7c2..a85e25bc 100644 --- a/lib/pages/routines/widgets/if_container.dart +++ b/lib/pages/routines/widgets/if_container.dart @@ -148,6 +148,7 @@ class IfContainer extends StatelessWidget { 'NCPS', 'WH', 'PC', + 'CUR', ].contains(mutableData['productType'])) { context .read() diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart index f0b77467..f260b262 100644 --- a/lib/pages/routines/widgets/routine_devices.dart +++ b/lib/pages/routines/widgets/routine_devices.dart @@ -28,6 +28,7 @@ class _RoutineDevicesState extends State { 'NCPS', 'WH', 'PC', + 'CUR', }; @override diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart new file mode 100644 index 00000000..94a6f15e --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -0,0 +1,270 @@ +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/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CurtainHelper { + static Future?> showControlDialog({ + required String dialogType, + required BuildContext context, + required List functions, + required String uniqueCustomId, + required AllDevicesModel? device, + }) async { + List curtainFunctions = + functions.whereType().where((function) { + if (dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + return showDialog?>( + context: context, + builder: (context) => BlocProvider( + create: (_) => FunctionBloc()..add(const InitializeFunctions([])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('AC Functions'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Function list + SizedBox( + width: selectedFunction != null ? 320 : 360, + child: _buildFunctionsList( + context: context, + curtainFunctions: curtainFunctions, + onFunctionSelected: + (functionCode, operationName) { + RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: functionCode, + functionOperationName: operationName, + functionValueDescription: + selectedFunctionData.valueDescription, + deviceUuid: device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'temp_set', + 'temp_current', + ], + defaultValue: 0); + }), + ), + // Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + controlFunctions: curtainFunctions, + device: device, + operationName: selectedOperationName ?? '', + ), + ), + ], + ), + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + ), + ), + ).then((value) { + return value; + }); + } + + static Widget _buildFunctionsList({ + required BuildContext context, + required List curtainFunctions, + required Function(String, String) onFunctionSelected, + }) { + return ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: curtainFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider( + color: ColorsManager.dividerColor, + ), + ), + itemBuilder: (context, index) { + final function = curtainFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => onFunctionSelected( + function.code, + function.operationName, + ), + ); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List controlFunctions, + AllDevicesModel? device, + required String operationName, + }) { + final selectedFn = + controlFunctions.firstWhere((f) => f.code == selectedFunction); + + // Rest of your existing code for other value selectors + final values = selectedFn.getOperationalValues(); + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + + // required Function(dynamic) onValueChanged, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routines/widgets/then_container.dart b/lib/pages/routines/widgets/then_container.dart index d9eee4c4..d1f66733 100644 --- a/lib/pages/routines/widgets/then_container.dart +++ b/lib/pages/routines/widgets/then_container.dart @@ -30,123 +30,121 @@ class ThenContainer extends StatelessWidget { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), - state.isLoading && state.isUpdate == true - ? const Center( - child: CircularProgressIndicator(), - ) - : Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate( - state.thenItems.length, - (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; - } - - if (state.thenItems[index]['type'] == - 'automation') { - final result = await showDialog( - context: context, - builder: (BuildContext context) => - AutomationDialog( - automationName: - state.thenItems[index] - ['name'] ?? - 'Automation', - automationId: - state.thenItems[index] - ['deviceId'] ?? - '', - uniqueCustomId: - state.thenItems[index] - ['uniqueCustomId'], - ), - ); - - if (result != null) { - context - .read() - .add(AddToThenContainer({ - ...state.thenItems[index], - 'imagePath': - Assets.automation, - 'title': - state.thenItems[index] - ['name'] ?? - state.thenItems[index] - ['title'], - })); - } - return; - } - - final result = await DeviceDialogHelper - .showDeviceDialog( - context: context, - data: state.thenItems[index], - removeComparetors: true, - dialogType: "THEN"); + if (state.isLoading && state.isUpdate == true) + const Center( + child: CircularProgressIndicator(), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + state.thenItems.length, + (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])); - } else if (![ - 'AC', - '1G', - '2G', - '3G', - 'WPS', - 'CPS', - "GW", - "NCPS", - 'WH', - ].contains(state.thenItems[index] - ['productType'])) { - context.read().add( - AddToThenContainer( - state.thenItems[index])); + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.delay, + 'title': 'Delay', + })); } + return; + } + + if (state.thenItems[index]['type'] == + 'automation') { + final result = await showDialog( + context: context, + builder: (BuildContext context) => + AutomationDialog( + automationName: + state.thenItems[index]['name'] + as String? ?? + 'Automation', + automationId: state.thenItems[index] + ['deviceId'] as String? ?? + '', + uniqueCustomId: state + .thenItems[index] + ['uniqueCustomId'] as String, + ), + ); + + if (result != null) { + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.automation, + 'title': state.thenItems[index] + ['name'] ?? + state.thenItems[index] + ['title'], + })); + } + return; + } + + final result = await DeviceDialogHelper + .showDeviceDialog( + context: context, + data: state.thenItems[index], + removeComparetors: true, + dialogType: 'THEN'); + if (result != null) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } else if (![ + 'AC', + '1G', + '2G', + '3G', + 'WPS', + 'CPS', + 'GW', + 'NCPS', + 'WH', + 'CUR', + ].contains(state.thenItems[index] + ['productType'])) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } + }, + child: DraggableCard( + imagePath: state.thenItems[index] + ['imagePath'] as String? ?? + '', + title: state.thenItems[index]['title'] + as String? ?? + '', + 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, + key: state.thenItems[index] + ['uniqueCustomId'] + as String)); }, - 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, - key: state.thenItems[index] - ['uniqueCustomId'])); - }, - ), - ))), + ), + ))), ], ), ), @@ -230,7 +228,7 @@ class ThenContainer extends StatelessWidget { context: context, data: mutableData, removeComparetors: true, - dialogType: "THEN"); + dialogType: 'THEN'); if (result != null) { context.read().add(AddToThenContainer(mutableData)); } else if (![ @@ -241,9 +239,10 @@ class ThenContainer extends StatelessWidget { 'WPS', 'GW', 'CPS', - "NCPS", - "WH", + 'NCPS', + 'WH', 'PC', + 'CUR', ].contains(mutableData['productType'])) { context.read().add(AddToThenContainer(mutableData)); } diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index bdc46ac1..455de5ba 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -40,6 +40,7 @@ class SceneApi { static Future> createAutomation( CreateAutomationModel createAutomationModel, String projectId) async { try { + print(createAutomationModel.toMap()); final response = await _httpService.post( path: ApiEndpoints.createAutomation.replaceAll('{projectId}', projectId),