From 584845ffdc380d3af5e0182a49c2be318d05188d Mon Sep 17 00:00:00 2001 From: Rafeek Alkhoudare Date: Thu, 22 May 2025 04:52:23 -0500 Subject: [PATCH 01/86] fix horizontal scroll bar --- lib/pages/common/custom_table.dart | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 60abc0d2..ac96bce8 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -63,7 +63,8 @@ class _DynamicTableState extends State { } } - bool _compareListOfLists(List> oldList, List> newList) { + bool _compareListOfLists( + List> oldList, List> newList) { // Check if the old and new lists are the same if (oldList.length != newList.length) return false; @@ -111,8 +112,8 @@ class _DynamicTableState extends State { trackVisibility: true, child: Scrollbar( controller: _horizontalScrollController, - thumbVisibility: false, - trackVisibility: false, + thumbVisibility: true, + trackVisibility: true, notificationPredicate: (notif) => notif.depth == 1, child: SingleChildScrollView( controller: _verticalScrollController, @@ -132,7 +133,8 @@ class _DynamicTableState extends State { children: [ if (widget.withCheckBox) _buildSelectAllCheckbox(), ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell(widget.headers[index], index); + return _buildTableHeaderCell( + widget.headers[index], index); }) //...widget.headers.map((header) => _buildTableHeaderCell(header)), ], @@ -153,11 +155,14 @@ class _DynamicTableState extends State { height: 15, ), Text( - widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', style: Theme.of(context) .textTheme .bodySmall! - .copyWith(color: ColorsManager.grayColor), + .copyWith( + color: ColorsManager.grayColor), ) ], ), @@ -166,12 +171,17 @@ class _DynamicTableState extends State { ], ) : Column( - children: List.generate(widget.data.length, (index) { + children: + List.generate(widget.data.length, (index) { final row = widget.data[index]; return Row( children: [ - if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), - ...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), + if (widget.withCheckBox) + _buildRowCheckbox( + index, widget.size.height * 0.08), + ...row.map((cell) => _buildTableCell( + cell.toString(), + widget.size.height * 0.08)), ], ); }), @@ -196,7 +206,9 @@ class _DynamicTableState extends State { ), child: Checkbox( value: _selectAll, - onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + onChanged: widget.withSelectAll && widget.data.isNotEmpty + ? _toggleSelectAll + : null, ), ); } @@ -238,7 +250,9 @@ class _DynamicTableState extends State { constraints: const BoxConstraints.expand(height: 40), alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + padding: EdgeInsets.symmetric( + horizontal: index == widget.headers.length - 1 ? 12 : 8.0, + vertical: 4), child: Text( title, style: context.textTheme.titleSmall!.copyWith( From 056a1daadccadbefff7e8d55c5ea40df118e4655 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Tue, 17 Jun 2025 13:34:23 +0300 Subject: [PATCH 02/86] show curtain in devices and implement dialog for if and then last integrate with backend --- .../all_devices/models/devices_model.dart | 18 +- .../dialog_helper/device_dialog_helper.dart | 12 +- .../routines/helper/save_routine_helper.dart | 54 ++-- .../models/curtain/curtain_function.dart | 49 ++++ .../curtain/curtain_opertion_value.dart | 11 + lib/pages/routines/widgets/if_container.dart | 1 + .../routines/widgets/routine_devices.dart | 1 + .../routine_dialogs/curtain_dialog.dart | 270 ++++++++++++++++++ .../routines/widgets/then_container.dart | 231 ++++++++------- lib/services/routines_api.dart | 1 + 10 files changed, 499 insertions(+), 149 deletions(-) create mode 100644 lib/pages/routines/models/curtain/curtain_function.dart create mode 100644 lib/pages/routines/models/curtain/curtain_opertion_value.dart create mode 100644 lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart 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), From 8caee328229308ca5e9664fac66b247d348f8068 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 18 Jun 2025 09:39:49 +0300 Subject: [PATCH 03/86] Initialized new `SpaceManagementPage`. --- .../views/space_management_page.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/views/space_management_page.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart new file mode 100644 index 00000000..03e17165 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatelessWidget { + const SpaceManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} From db513f916fa983e9b4f714e9c62f29c74484ed3c Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 18 Jun 2025 16:27:50 +0300 Subject: [PATCH 04/86] Refactor schedule components and update imports for garage door and water heater modules --- .../helper/garage_door_helper.dart | 4 +- .../opening_clsoing_time_dialog_body.dart | 5 +- .../schedule__garage_table.dart | 48 +- .../schedule_garage_header.dart | 0 .../schedule_garage_managment_ui.dart | 2 +- .../schedule_garage_mode_buttons.dart | 0 .../schedule_garage_mode_selector.dart | 0 .../schedule_garage_view.dart | 6 +- .../seconds_picker.dart | 0 .../time_out_alarm_dialog_body.dart | 0 .../view/garage_door_control_view.dart | 2 +- .../schedule_device/bloc/schedule_bloc.dart | 557 ++++++++++++++++++ .../schedule_device/bloc/schedule_event.dart | 221 +++++++ .../schedule_device/bloc/schedule_state.dart | 109 ++++ .../schedule_widgets}/count_down_button.dart | 26 +- .../count_down_inching_view.dart | 185 ++++++ .../inching_mode_buttons.dart | 19 +- .../schedule_widgets/schedual_view.dart | 107 ++++ .../schedule_widgets}/schedule_header.dart | 0 .../schedule_managment_ui.dart | 13 +- .../schedule_mode_buttons.dart | 0 .../schedule_mode_selector.dart | 87 +++ .../schedule_widgets}/schedule_table.dart | 138 +++-- .../helper/add_schedule_dialog_helper.dart | 336 +++++------ .../water_heater/models/schedule_entry.dart | 23 +- .../models/water_heater_status_model.dart | 2 +- .../view/water_heater_device_control.dart | 9 +- .../widgets/count_down_inching_view.dart | 223 ------- .../water_heater/widgets/schedual_view.dart | 117 ---- .../widgets/schedule_mode_selector.dart | 86 --- 30 files changed, 1603 insertions(+), 722 deletions(-) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/opening_clsoing_time_dialog_body.dart (88%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule__garage_table.dart (79%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_header.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_managment_ui.dart (93%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_mode_buttons.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_mode_selector.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/schedule_garage_view.dart (91%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/seconds_picker.dart (100%) rename lib/pages/device_managment/garage_door/{widgets => schedule_view}/time_out_alarm_dialog_body.dart (100%) create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_event.dart create mode 100644 lib/pages/device_managment/schedule_device/bloc/schedule_state.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/count_down_button.dart (72%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/inching_mode_buttons.dart (82%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_header.dart (100%) rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_managment_ui.dart (76%) rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_mode_buttons.dart (100%) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart rename lib/pages/device_managment/{water_heater/widgets => schedule_device/schedule_widgets}/schedule_table.dart (58%) delete mode 100644 lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart delete mode 100644 lib/pages/device_managment/water_heater/widgets/schedual_view.dart delete mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart diff --git a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart index 7b133d45..8c9820f9 100644 --- a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart +++ b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart @@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; diff --git a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart similarity index 88% rename from lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart index 843bac9b..0cc9485a 100644 --- a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart @@ -1,14 +1,13 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/seconds_picker.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/seconds_picker.dart'; class OpeningAndClosingTimeDialogBody extends StatefulWidget { final ValueChanged onDurationChanged; final GarageDoorBloc bloc; - OpeningAndClosingTimeDialogBody({ + const OpeningAndClosingTimeDialogBody({ required this.onDurationChanged, required this.bloc, }); diff --git a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart similarity index 79% rename from lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart index 07cd9c7a..0bd347ab 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart @@ -26,7 +26,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { Table( border: TableBorder.all( color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), ), children: [ TableRow( @@ -50,17 +51,21 @@ class ScheduleGarageTableWidget extends StatelessWidget { BlocBuilder( builder: (context, state) { if (state is ScheduleGarageLoadingState) { - return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); } - if (state is GarageDoorLoadedState && state.status.schedules?.isEmpty == true) { + if (state is GarageDoorLoadedState && + state.status.schedules?.isEmpty == true) { return _buildEmptyState(context); } else if (state is GarageDoorLoadedState) { return Container( height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: - const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), ), child: _buildTableBody(state, context)); } @@ -78,7 +83,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), child: Center( child: Column( @@ -112,7 +118,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { children: [ if (state.status.schedules != null) for (int i = 0; i < state.status.schedules!.length; i++) - _buildScheduleRow(state.status.schedules![i], i, context, state), + _buildScheduleRow( + state.status.schedules![i], i, context, state), ], ), ), @@ -134,7 +141,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { ); } - TableRow _buildScheduleRow(ScheduleModel schedule, int index, BuildContext context, GarageDoorLoadedState state) { + TableRow _buildScheduleRow(ScheduleModel schedule, int index, + BuildContext context, GarageDoorLoadedState state) { return TableRow( children: [ Center( @@ -152,7 +160,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { width: 24, height: 24, child: schedule.enable - ? const Icon(Icons.radio_button_checked, color: ColorsManager.blueColor) + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) : const Icon( Icons.radio_button_unchecked, color: ColorsManager.grayColor, @@ -160,7 +169,9 @@ class ScheduleGarageTableWidget extends StatelessWidget { ), ), ), - Center(child: Text(_getSelectedDays(ScheduleModel.parseSelectedDays(schedule.days)))), + Center( + child: Text(_getSelectedDays( + ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( @@ -170,18 +181,24 @@ class ScheduleGarageTableWidget extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - GarageDoorDialogHelper.showAddGarageDoorScheduleDialog(context, - schedule: schedule, index: index, isEdit: true); + GarageDoorDialogHelper.showAddGarageDoorScheduleDialog( + context, + schedule: schedule, + index: index, + isEdit: true); }, child: Text( 'Edit', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), ), ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - context.read().add(DeleteGarageDoorScheduleEvent( + context + .read() + .add(DeleteGarageDoorScheduleEvent( index: index, scheduleId: schedule.scheduleId, deviceId: state.status.uuid, @@ -189,7 +206,8 @@ class ScheduleGarageTableWidget extends StatelessWidget { }, child: Text( 'Delete', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor), + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), ), ), ], diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart similarity index 93% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart index e5819e89..9b2c2b3f 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule__garage_table.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart similarity index 91% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart index 107c8e0a..7b772135 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_header.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart'; class BuildGarageDoorScheduleView extends StatefulWidget { const BuildGarageDoorScheduleView({super.key, required this.status}); diff --git a/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart b/lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/seconds_picker.dart rename to lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart diff --git a/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart diff --git a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart index ae2fc9e4..30d9bf5d 100644 --- a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart +++ b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart'; import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart'; -import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_view.dart'; +import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart new file mode 100644 index 00000000..e6a2645d --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -0,0 +1,557 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +part 'schedule_event.dart'; +part 'schedule_state.dart'; + +class ScheduleBloc extends Bloc { + final String deviceId; + + ScheduleBloc({ + required this.deviceId, + }) : super(ScheduleInitial()) { + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + on(_getSchedule); + on(_onAddSchedule); + on(_onEditSchedule); + on(_onUpdateSchedule); + on(_onUpdateScheduleMode); + on(_onUpdateCountdownTime); + on(_onUpdateInchingTime); + on(_onStartScheduleEvent); + on(_onStopScheduleEvent); + on(_onDecrementCountdown); + on(_fetchStatus); + on(_onDeleteSchedule); + } + Timer? _countdownTimer; + Duration countdownRemaining = Duration.zero; + + void _onStopScheduleEvent( + StopScheduleEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + _countdownTimer?.cancel(); + + if (event.mode == ScheduleModes.countdown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } else if (event.mode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + countdownRemaining: Duration.zero, + )); + } + } + } + + void _onUpdateScheduleMode( + UpdateScheduleModeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + scheduleMode: event.scheduleMode, + countdownRemaining: Duration.zero, + )); + } + } + + void _onUpdateCountdownTime( + UpdateCountdownTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownHours: event.hours, + countdownMinutes: event.minutes, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _onUpdateInchingTime( + UpdateInchingTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + inchingHours: event.hours, + inchingMinutes: event.minutes, + countdownRemaining: Duration.zero, + )); + } + } + + void _initializeAddSchedule( + ScheduleInitializeAddEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + scheduleMode: event.scheduleMode, + countdownRemaining: Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: const [], + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + deviceId: deviceId, + scheduleMode: event.scheduleMode, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } + + void _updateSelectedTime( + ScheduleUpdateSelectedTimeEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + selectedTime: event.selectedTime, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _updateSelectedDay( + ScheduleUpdateSelectedDayEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final updatedDays = List.from(currentState.selectedDays); + updatedDays[event.index] = event.value; + emit(currentState.copyWith( + selectedDays: updatedDays, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + void _updateFunctionOn( + ScheduleUpdateFunctionOnEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + functionOn: event.isOn, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } + } + + Future _getSchedule( + ScheduleGetEvent event, + Emitter emit, + ) async { + try { + emit(ScheduleLoading()); + final schedules = await DevicesManagementApi().getDeviceSchedules( + deviceId, + event.category, + ); + + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + schedules: schedules, + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + countdownRemaining: Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: schedules, + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: deviceId, + scheduleMode: ScheduleModes.schedule, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } catch (e) { + emit(ScheduleError('Failed to load schedules: $e')); + } + } + + Future _onAddSchedule( + ScheduleAddEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final newSchedule = ScheduleEntry( + category: event.category, + time: event.time, + function: Status(code: 'switch_1', value: event.functionOn), + days: event.selectedDays); + final success = await DevicesManagementApi().addScheduleRecord( + newSchedule, + deviceId, + ); + + if (success) { + add(const ScheduleGetEvent(category: 'switch_1')); + } else { + emit(const ScheduleError('Failed to add schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to add schedule: $e')); + } + } + + Future _onEditSchedule( + ScheduleEditEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final updatedSchedule = ScheduleEntry( + scheduleId: event.scheduleId, + category: event.category, + time: event.time, + function: Status(code: 'switch_1', value: event.functionOn), + days: event.selectedDays, + ); + + final success = await DevicesManagementApi().editScheduleRecord( + deviceId, + updatedSchedule, + ); + + if (success) { + add(const ScheduleGetEvent(category: 'switch_1')); + } else { + emit(const ScheduleError('Failed to update schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to update schedule: $e')); + } + } + + Future _onUpdateSchedule( + ScheduleUpdateEntryEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final updatedSchedules = currentState.schedules.map((schedule) { + if (schedule.scheduleId == event.scheduleId) { + return schedule.copyWith( + function: Status(code: 'switch_1', value: event.functionOn), + ); + } + return schedule; + }).toList(); + + final success = await DevicesManagementApi().updateScheduleRecord( + enable: event.enable, + uuid: deviceId, + scheduleId: event.scheduleId, + ); + + if (success) { + emit(currentState.copyWith( + schedules: updatedSchedules, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } else { + emit(const ScheduleError('Failed to update schedule status')); + } + } + } catch (e) { + emit(ScheduleError('Failed to update schedule: $e')); + } + } + + Future _onDeleteSchedule( + ScheduleDeleteEvent event, + Emitter emit, + ) async { + try { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final success = await DevicesManagementApi().deleteScheduleRecord( + deviceId, + event.scheduleId, + ); + + if (success) { + final updatedSchedules = currentState.schedules + .where((s) => s.scheduleId != event.scheduleId) + .toList(); + emit(currentState.copyWith( + schedules: updatedSchedules, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + countdownRemaining: Duration.zero, + )); + } else { + emit(const ScheduleError('Failed to delete schedule')); + } + } + } catch (e) { + emit(ScheduleError('Failed to delete schedule: $e')); + } + } + + Duration? _currentCountdown; + + Future _onStartScheduleEvent( + StartScheduleEvent event, + Emitter emit, + ) async { + if (state is ScheduleLoaded) { + final totalSeconds = + Duration(hours: event.hours, minutes: event.minutes).inSeconds; + final code = event.mode == ScheduleModes.countdown + ? 'countdown_1' + : 'switch_inching'; + final currentState = state as ScheduleLoaded; + final duration = Duration(seconds: totalSeconds); + _currentCountdown = duration; + emit(currentState.copyWith( + countdownRemaining: duration, + schedules: currentState.schedules.map((schedule) { + if (schedule.function.code == code) { + return schedule.copyWith( + function: Status(code: code, value: totalSeconds), + ); + } + return schedule; + }).toList(), + countdownHours: event.mode == ScheduleModes.countdown ? event.hours : 0, + )); + + final success = await RemoteControlDeviceService().controlDevice( + deviceUuid: deviceId, + status: Status( + code: code, + value: totalSeconds, + ), + ); + + if (success) { + if (code == 'countdown_1') { + final countdownDuration = Duration(seconds: totalSeconds); + + emit( + currentState.copyWith( + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, + ), + ); + + if (countdownDuration.inSeconds > 0) { + _startCountdownTimer(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); + } + } else if (code == 'switch_inching') { + final inchingDuration = Duration(seconds: totalSeconds); + emit( + currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + countdownRemaining: inchingDuration, + ), + ); + } + } + } + } + + void _startCountdownTimer( + Emitter emit, + Duration duration, + ) { + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_currentCountdown != null && _currentCountdown! > Duration.zero) { + _currentCountdown = _currentCountdown! - const Duration(seconds: 1); + countdownRemaining = _currentCountdown!; + add(const ScheduleDecrementCountdownEvent()); + } else { + timer.cancel(); + add(StopScheduleEvent( + mode: _currentCountdown == null + ? ScheduleModes.countdown + : ScheduleModes.inching, + deviceId: deviceId, + )); + } + }); + } + + void _onDecrementCountdown( + ScheduleDecrementCountdownEvent event, + Emitter emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownRemaining: countdownRemaining, + )); + } + } + + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + Future _fetchStatus( + ScheduleFetchStatusEvent event, + Emitter emit, + ) async { + emit(ScheduleLoading()); + + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + final deviceStatus = + WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + + final scheduleMode = deviceStatus.scheduleMode; + final isCountdown = scheduleMode == ScheduleModes.countdown; + final isInching = scheduleMode == ScheduleModes.inching; + + Duration? countdownRemaining; + var isCountdownActive = false; + var isInchingActive = false; + + if (isCountdown) { + countdownRemaining = Duration( + hours: deviceStatus.countdownHours, + minutes: deviceStatus.countdownMinutes, + ); + isCountdownActive = countdownRemaining > Duration.zero; + } else if (isInching) { + isInchingActive = Duration( + hours: deviceStatus.inchingHours, + minutes: deviceStatus.inchingMinutes, + ) > + Duration.zero; + } + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + scheduleMode: scheduleMode, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: countdownRemaining ?? Duration.zero, + )); + } else { + emit(ScheduleLoaded( + schedules: const [], + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + deviceId: deviceId, + scheduleMode: scheduleMode, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isCountdownActive: isCountdownActive, + isInchingActive: isInchingActive, + countdownRemaining: countdownRemaining ?? Duration.zero, + )); + } + + if (isCountdownActive && countdownRemaining != null) { + _startCountdownTimer(emit, countdownRemaining); + } + } catch (e) { + emit(ScheduleError('Failed to fetch device status: $e')); + } + } + +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart new file mode 100644 index 00000000..369ca795 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -0,0 +1,221 @@ +part of 'schedule_bloc.dart'; + +abstract class ScheduleEvent extends Equatable { + const ScheduleEvent(); +} + +class ScheduleInitializeAddEvent extends ScheduleEvent { + final bool isEditing; + final ScheduleModes scheduleMode; + final TimeOfDay? selectedTime; + final List? selectedDays; + final bool? functionOn; + + const ScheduleInitializeAddEvent({ + required this.isEditing, + required this.scheduleMode, + this.selectedTime, + this.selectedDays, + this.functionOn, + }); + + @override + List get props => [ + isEditing, + scheduleMode, + selectedTime, + selectedDays, + functionOn, + ]; +} + +class ScheduleUpdateSelectedTimeEvent extends ScheduleEvent { + final TimeOfDay selectedTime; + + const ScheduleUpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class ScheduleUpdateSelectedDayEvent extends ScheduleEvent { + final int index; + final bool value; + + const ScheduleUpdateSelectedDayEvent(this.index, this.value); + + @override + List get props => [index, value]; +} + +class ScheduleUpdateFunctionOnEvent extends ScheduleEvent { + final bool isOn; + + const ScheduleUpdateFunctionOnEvent(this.isOn); + + @override + List get props => [isOn]; +} + +class ScheduleGetEvent extends ScheduleEvent { + final String category; + + const ScheduleGetEvent({required this.category}); + + @override + List get props => [category]; +} + +class ScheduleAddEvent extends ScheduleEvent { + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleAddEvent({ + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [category, time, selectedDays, functionOn]; +} + +class ScheduleEditEvent extends ScheduleEvent { + final String scheduleId; + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleEditEvent({ + required this.scheduleId, + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [ + scheduleId, + category, + time, + selectedDays, + functionOn, + ]; +} + +class ScheduleDeleteEvent extends ScheduleEvent { + final String scheduleId; + + const ScheduleDeleteEvent(this.scheduleId); + + @override + List get props => [scheduleId]; +} + +class ScheduleUpdateEntryEvent extends ScheduleEvent { + final String scheduleId; + final bool functionOn; + final bool enable; + + const ScheduleUpdateEntryEvent({ + required this.scheduleId, + required this.functionOn, + required this.enable, + }); + + @override + List get props => [scheduleId, functionOn, enable]; +} + +class UpdateScheduleModeEvent extends ScheduleEvent { + final ScheduleModes scheduleMode; + + const UpdateScheduleModeEvent({required this.scheduleMode}); + + @override + List get props => [scheduleMode]; +} + +class UpdateCountdownTimeEvent extends ScheduleEvent { + final int hours; + final int minutes; + + const UpdateCountdownTimeEvent({ + required this.hours, + required this.minutes, + }); + + @override + List get props => [hours, minutes]; +} + +class UpdateInchingTimeEvent extends ScheduleEvent { + final int hours; + final int minutes; + + const UpdateInchingTimeEvent({ + required this.hours, + required this.minutes, + }); + + @override + List get props => [hours, minutes]; +} + +class StartScheduleEvent extends ScheduleEvent { + final ScheduleModes mode; + final int hours; + final int minutes; + + const StartScheduleEvent({ + required this.mode, + required this.hours, + required this.minutes, + }); + + @override + List get props => [mode, hours, minutes]; +} + +class StopScheduleEvent extends ScheduleEvent { + final ScheduleModes mode; + final String deviceId; + + const StopScheduleEvent({ + required this.mode, + required this.deviceId, + }); + + @override + List get props => [mode, deviceId]; +} + +class ScheduleDecrementCountdownEvent extends ScheduleEvent { + const ScheduleDecrementCountdownEvent(); + + @override + List get props => []; +} + +class ScheduleFetchStatusEvent extends ScheduleEvent { + final String deviceId; + + const ScheduleFetchStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class DeleteScheduleEvent extends ScheduleEvent { + final String scheduleId; + + const DeleteScheduleEvent(this.scheduleId); + + @override + List get props => [scheduleId]; +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart new file mode 100644 index 00000000..10cd7611 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -0,0 +1,109 @@ +part of 'schedule_bloc.dart'; + +abstract class ScheduleState extends Equatable { + const ScheduleState(); +} + +class ScheduleInitial extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoading extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoaded extends ScheduleState { + final List schedules; + final TimeOfDay? selectedTime; + final List selectedDays; + final bool functionOn; + final bool isEditing; + final String deviceId; + final int countdownHours; + final int countdownMinutes; + final bool isCountdownActive; + final int inchingHours; + final int inchingMinutes; + final bool isInchingActive; + final ScheduleModes scheduleMode; + final Duration? countdownRemaining; + + const ScheduleLoaded({ + required this.schedules, + this.selectedTime, + required this.selectedDays, + required this.functionOn, + required this.isEditing, + required this.deviceId, + this.countdownHours = 0, + this.countdownMinutes = 0, + this.isCountdownActive = false, + this.inchingHours = 0, + this.inchingMinutes = 0, + this.isInchingActive = false, + this.scheduleMode = ScheduleModes.countdown, + this.countdownRemaining, + }); + + ScheduleLoaded copyWith({ + List? schedules, + TimeOfDay? selectedTime, + List? selectedDays, + bool? functionOn, + bool? isEditing, + int? countdownHours, + int? countdownMinutes, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + ScheduleModes? scheduleMode, + Duration? countdownRemaining, + }) { + return ScheduleLoaded( + schedules: schedules ?? this.schedules, + selectedTime: selectedTime ?? this.selectedTime, + selectedDays: selectedDays ?? this.selectedDays, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, + deviceId: deviceId, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, + isCountdownActive: isCountdownActive ?? this.isCountdownActive, + inchingHours: inchingHours ?? this.inchingHours, + inchingMinutes: inchingMinutes ?? this.inchingMinutes, + isInchingActive: isInchingActive ?? this.isInchingActive, + scheduleMode: scheduleMode ?? this.scheduleMode, + countdownRemaining: countdownRemaining ?? this.countdownRemaining, + ); + } + + @override + List get props => [ + schedules, + selectedTime, + selectedDays, + functionOn, + isEditing, + deviceId, + countdownHours, + countdownMinutes, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, + scheduleMode, + countdownRemaining, + ]; +} + +class ScheduleError extends ScheduleState { + final String error; + + const ScheduleError(this.error); + + @override + List get props => [error]; +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart similarity index 72% rename from lib/pages/device_managment/water_heater/widgets/count_down_button.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index e60c7def..4919018c 100644 --- a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownModeButtons extends StatelessWidget { @@ -38,14 +39,10 @@ class CountdownModeButtons extends StatelessWidget { ? DefaultButton( height: 40, onPressed: () { - context - .read() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( + context.read().add( + StopScheduleEvent( + mode: ScheduleModes.countdown, deviceId: deviceId, - code: 'countdown_1', - value: 0, ), ); }, @@ -55,12 +52,11 @@ class CountdownModeButtons extends StatelessWidget { : DefaultButton( height: 40, onPressed: () { - context.read().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'countdown_1', - value: Duration(hours: hours, minutes: minutes) - .inSeconds, + context.read().add( + StartScheduleEvent( + mode: ScheduleModes.countdown, + hours: hours, + minutes: minutes, ), ); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart new file mode 100644 index 00000000..d45073ec --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownInchingView extends StatefulWidget { + const CountdownInchingView({super.key}); + + @override + State createState() => _CountdownInchingViewState(); +} + +class _CountdownInchingViewState extends State { + late FixedExtentScrollController _hoursController; + late FixedExtentScrollController _minutesController; + + int _lastHours = -1; + int _lastMinutes = -1; + + @override + void initState() { + super.initState(); + _hoursController = FixedExtentScrollController(); + _minutesController = FixedExtentScrollController(); + } + + @override + void dispose() { + _hoursController.dispose(); + _minutesController.dispose(); + super.dispose(); + } + + void _updateControllers(int displayHours, int displayMinutes) { + if (_lastHours != displayHours) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_hoursController.hasClients) { + _hoursController.jumpToItem(displayHours); + } + }); + _lastHours = displayHours; + } + if (_lastMinutes != displayMinutes) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_minutesController.hasClients) { + _minutesController.jumpToItem(displayMinutes); + } + }); + _lastMinutes = displayMinutes; + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is! ScheduleLoaded) return const SizedBox.shrink(); + + final isCountDown = state.scheduleMode == ScheduleModes.countdown; + final isActive = + isCountDown ? state.isCountdownActive : state.isInchingActive; + final displayHours = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inHours + : (isCountDown ? state.countdownHours : state.inchingHours); + final displayMinutes = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inMinutes.remainder(60) + : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + + _updateControllers(displayHours, displayMinutes); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isCountDown ? 'Countdown:' : 'Inching:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, ' + 'it will automatically turn off after a preset time.', + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn( + context, + 'h', + displayHours, + 100, + _hoursController, + (value) { + if (!isActive) { + context.read().add(UpdateCountdownTimeEvent( + hours: value, minutes: displayMinutes)); + } + }, + isActive: isActive, + ), + const SizedBox(width: 10), + _buildPickerColumn( + context, + 'm', + displayMinutes, + 60, + _minutesController, + (value) { + if (!isActive) { + context.read().add(UpdateCountdownTimeEvent( + hours: displayHours, minutes: value)); + } + }, + isActive: isActive, + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + FixedExtentScrollController controller, + ValueChanged onSelected, { + required bool isActive, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + controller: controller, + itemExtent: 40.0, + physics: isActive + ? const NeverScrollableScrollPhysics() + : const FixedExtentScrollPhysics(), + onSelectedItemChanged: isActive ? null : onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart similarity index 82% rename from lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index 8eec5cca..e75c5d46 100644 --- a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart' + hide StopScheduleEvent; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class InchingModeButtons extends StatelessWidget { @@ -38,15 +41,9 @@ class InchingModeButtons extends StatelessWidget { ? DefaultButton( height: 40, onPressed: () { - context - .read() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'switch_inching', - value: 0, - ), + context.read().add( + StopScheduleEvent( + deviceId: deviceId, mode: ScheduleModes.inching), ); }, backgroundColor: Colors.red, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart new file mode 100644 index 00000000..591b114f --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +class BuildScheduleView extends StatelessWidget { + const BuildScheduleView({super.key, required this.deviceUuid}); + final String deviceUuid; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScheduleBloc( + deviceId: deviceUuid, + ) + ..add(const ScheduleGetEvent(category: "switch_1")) + ..add(ScheduleFetchStatusEvent(deviceUuid)), + child: Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 700, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoaded) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ScheduleHeader(), + const SizedBox(height: 20), + ScheduleModeSelector( + currentMode: state.scheduleMode, + ), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.schedule) + ScheduleManagementUI( + deviceUuid: deviceUuid, + onAddSchedule: () async { + final entry = await ScheduleDialogHelper + .showAddScheduleDialog( + context, + schedule: null, + isEdit: false, + ); + if (entry != null) { + context.read().add( + ScheduleAddEvent( + category: entry.category, + time: entry.time, + functionOn: entry.function.value, + selectedDays: entry.days, + ), + ); + } + }, + ), + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + const CountdownInchingView(), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive, + deviceId: deviceUuid, + hours: state.countdownHours, + minutes: state.countdownMinutes, + ), + if (state.scheduleMode == ScheduleModes.inching) + InchingModeButtons( + isActive: state.isInchingActive, + deviceId: deviceUuid, + hours: state.inchingHours, + minutes: state.inchingMinutes, + ), + if (state.scheduleMode != ScheduleModes.countdown && + state.scheduleMode != ScheduleModes.inching) + ScheduleModeButtons( + onSave: () => Navigator.pop(context), + ), + ], + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_header.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart similarity index 76% rename from lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 1710c439..b60f00b9 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - final Function onAddSchedule; + final String deviceUuid; + final VoidCallback onAddSchedule; const ScheduleManagementUI({ super.key, - required this.state, + required this.deviceUuid, required this.onAddSchedule, }); @@ -28,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget { padding: 2, backgroundColor: ColorsManager.graysColor, borderRadius: 15, - onPressed: () => onAddSchedule(), + onPressed: onAddSchedule, child: Row( children: [ const Icon(Icons.add, color: ColorsManager.primaryColor), @@ -43,7 +42,7 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(state: state), + ScheduleTableWidget(deviceUuid: deviceUuid), ], ); } diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..2bcc0957 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleModeSelector extends StatelessWidget { + final ScheduleModes currentMode; + + const ScheduleModeSelector({ + super.key, + required this.currentMode, + }); + + @override + Widget build(BuildContext context) { + final currentMode = context.select( + (bloc) => bloc.state is ScheduleLoaded && + (bloc.state as ScheduleLoaded).scheduleMode != null + ? (bloc.state as ScheduleLoaded).scheduleMode + : ScheduleModes.schedule, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, currentMode), + _buildRadioTile( + context, 'Schedule', ScheduleModes.schedule, currentMode), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, currentMode), + _buildRadioTile( + context, 'Inching', ScheduleModes.inching, currentMode), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String label, + ScheduleModes mode, + ScheduleModes currentMode, + ) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: currentMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + context.read().add( + UpdateScheduleModeEvent(scheduleMode: value), + ); + if (value == ScheduleModes.schedule) { + context.read().add( + const ScheduleGetEvent(category: 'switch_1'), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart similarity index 58% rename from lib/pages/device_managment/water_heater/widgets/schedule_table.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 18cbbe5a..4d36e0e2 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -1,23 +1,38 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.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:syncrow_web/utils/format_date_time.dart'; -import '../helper/add_schedule_dialog_helper.dart'; - class ScheduleTableWidget extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; + final String deviceUuid; + final String category; const ScheduleTableWidget({ super.key, - required this.state, + required this.deviceUuid, + this.category = 'switch_1', }); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ScheduleBloc( + deviceId: deviceUuid, + )..add(ScheduleGetEvent(category: category)), + child: _ScheduleTableView(), + ); + } +} + +class _ScheduleTableView extends StatelessWidget { + const _ScheduleTableView(); + @override Widget build(BuildContext context) { return Column( @@ -47,17 +62,17 @@ class ScheduleTableWidget extends StatelessWidget { ), ], ), - BlocBuilder( + BlocBuilder( builder: (context, state) { - if (state is ScheduleLoadingState) { + if (state is ScheduleLoading) { return const SizedBox( height: 200, child: Center(child: CircularProgressIndicator())); } - if (state is WaterHeaterDeviceStatusLoaded && - state.schedules.isEmpty) { + if (state is ScheduleLoaded && state.schedules.isEmpty) { return _buildEmptyState(context); - } else if (state is WaterHeaterDeviceStatusLoaded) { + } + if (state is ScheduleLoaded) { return Container( height: 200, decoration: BoxDecoration( @@ -66,11 +81,12 @@ class ScheduleTableWidget extends StatelessWidget { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), ), - child: _buildTableBody(state, context)); + child: _buildTableBody(state.schedules, context)); } - return const SizedBox( - height: 200, - ); + if (state is ScheduleError) { + return Center(child: Text(state.error)); + } + return const SizedBox(height: 200); }, ), ], @@ -95,10 +111,10 @@ class ScheduleTableWidget extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Text( 'No schedules added yet', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), ), ), ], @@ -107,8 +123,7 @@ class ScheduleTableWidget extends StatelessWidget { ); } - Widget _buildTableBody( - WaterHeaterDeviceStatusLoaded state, BuildContext context) { + Widget _buildTableBody(List schedules, BuildContext context) { return SizedBox( height: 200, child: SingleChildScrollView( @@ -116,8 +131,8 @@ class ScheduleTableWidget extends StatelessWidget { border: TableBorder.all(color: ColorsManager.graysColor), defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - for (int i = 0; i < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context, state), + for (int i = 0; i < schedules.length; i++) + _buildScheduleRow(schedules[i], i, context), ], ), ), @@ -139,20 +154,23 @@ class ScheduleTableWidget extends StatelessWidget { ); } - TableRow _buildScheduleRow(ScheduleModel schedule, int index, - BuildContext context, WaterHeaterDeviceStatusLoaded state) { + TableRow _buildScheduleRow( + ScheduleModel schedule, int index, BuildContext context) { return TableRow( children: [ Center( child: GestureDetector( onTap: () { - context.read().add(UpdateScheduleEntryEvent( - index: index, - enable: !schedule.enable, - scheduleId: schedule.scheduleId, - deviceId: state.status.uuid, - functionOn: schedule.function.value, - )); + ///TODO: Implement toggle functionality + + // Toggle enabled state using ScheduleBloc + // context.read().add( + // UpdateScheduleEvent( + // scheduleId: schedule.scheduleId, + // functionOn: schedule.function.value, + // enable: !schedule.enable, + // ), + // ); }, child: SizedBox( width: 24, @@ -179,26 +197,46 @@ class ScheduleTableWidget extends StatelessWidget { TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - ScheduleDialogHelper.showAddScheduleDialog(context, - schedule: schedule, index: index, isEdit: true); + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: ScheduleEntry.fromScheduleModel(schedule), + isEdit: true, + ).then((updatedSchedule) { + print('updatedSchedule : $updatedSchedule'); + if (updatedSchedule != null) { + context.read().add( + ScheduleEditEvent( + scheduleId: schedule.scheduleId, + category: schedule.category, + time: updatedSchedule.time, + functionOn: updatedSchedule.function.value, + selectedDays: updatedSchedule.days), + ); + } + }); }, child: Text( 'Edit', - style: context.textTheme.bodySmall! + style: Theme.of(context) + .textTheme + .bodySmall! .copyWith(color: ColorsManager.blueColor), ), ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - context.read().add(DeleteScheduleEvent( - index: index, - scheduleId: schedule.scheduleId, - )); + context.read().add( + DeleteScheduleEvent( + schedule.scheduleId, + ), + ); }, child: Text( 'Delete', - style: context.textTheme.bodySmall! + style: Theme.of(context) + .textTheme + .bodySmall! .copyWith(color: ColorsManager.blueColor), ), ), @@ -210,13 +248,15 @@ class ScheduleTableWidget extends StatelessWidget { } String _getSelectedDays(List selectedDays) { - final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr.join(', '); + // Use the same order as in ScheduleDialogHelper + const days = ScheduleDialogHelper.allDays; + return selectedDays + .asMap() + .entries + .where((entry) => entry.value) + .map((entry) => days[entry.key]) + .join(', '); } + + // Removed allDays from here as it is now in ScheduleDialogHelper } diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 9278e396..b09cb48c 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -1,240 +1,210 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; class ScheduleDialogHelper { - static void showAddScheduleDialog(BuildContext context, {ScheduleModel? schedule, int? index, bool? isEdit}) { - final bloc = context.read(); + static const List allDays = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat' + ]; - if (schedule == null) { - bloc.add((const UpdateSelectedTimeEvent(null))); - bloc.add(InitializeAddScheduleEvent( - selectedTime: null, - selectedDays: List.filled(7, false), - functionOn: false, - isEditing: false, - )); - } else { - final time = _convertStringToTimeOfDay(schedule.time); - final selectedDays = _convertDaysStringToBooleans(schedule.days); + static Future showAddScheduleDialog( + BuildContext context, { + ScheduleEntry? schedule, + bool isEdit = false, + }) { + final initialTime = schedule != null + ? _convertStringToTimeOfDay(schedule.time) + : TimeOfDay.now(); + final initialDays = schedule != null + ? _convertDaysStringToBooleans(schedule.days) + : List.filled(7, false); + bool? functionOn = schedule?.function.value ?? true; + TimeOfDay selectedTime = initialTime; + List selectedDays = List.of(initialDays); - bloc.add(InitializeAddScheduleEvent( - selectedTime: time, - selectedDays: selectedDays, - functionOn: schedule.function.value, - isEditing: true, - index: index, - )); - } - - showDialog( + return showDialog( context: context, builder: (ctx) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + return StatefulBuilder( + builder: (ctx, setState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, + const SizedBox(), + Text( + isEdit ? 'Edit Schedule' : 'Add Schedule', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Colors.blue, fontWeight: FontWeight.bold, ), - ), - const SizedBox(), - ], ), - const SizedBox(height: 24), - SizedBox( - width: 150, - height: 40, - child: DefaultButton( - padding: 8, - backgroundColor: ColorsManager.boxColor, - borderRadius: 15, - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: state.selectedTime ?? TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.light( - primary: ColorsManager.primaryColor, - ), - ), - child: child!, - ); - }, - ); - if (time != null) { - bloc.add(UpdateSelectedTimeEvent(time)); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - state.selectedTime == null ? 'Time' : state.selectedTime!.format(context), - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - ), - ), - const Icon( - Icons.access_time, - color: ColorsManager.grayColor, - size: 18, - ), - ], - ), - ), - ), - const SizedBox(height: 16), - _buildDayCheckboxes(context, state.selectedDays, isEdit: isEdit), - const SizedBox(height: 16), - _buildFunctionSwitch(context, state.functionOn, isEdit), + const SizedBox(), ], ), - actions: [ - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[200], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), ), ), - ), - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - if (state.selectedTime != null) { - if (state.isEditing && index != null) { - bloc.add(EditWaterHeaterScheduleEvent( - scheduleId: schedule?.scheduleId ?? '', - category: 'switch_1', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } else { - bloc.add(AddScheduleEvent( - category: 'switch_1', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } - Navigator.pop(context); - } - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: ctx, + initialTime: selectedTime, + ); + if (time != null) { + setState(() => selectedTime = time); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedTime.format(context), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.grey), + ), + const Icon(Icons.access_time, + color: Colors.grey, size: 18), + ], ), ), - ], - ); - } - return const SizedBox(); - }, - ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(ctx, selectedDays, (i, v) { + setState(() => selectedDays[i] = v); + }), + const SizedBox(height: 16), + _buildFunctionSwitch(ctx, functionOn, (v) { + setState(() => functionOn = v); + }), + ], + ), + actions: [ + SizedBox( + width: 100, + child: OutlinedButton( + onPressed: () { + Navigator.pop(ctx, null); + }, + child: const Text('Cancel'), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + final entry = ScheduleEntry( + category: schedule?.category ?? 'switch_1', + time: _formatTimeOfDayToISO(selectedTime), + function: Status(code: 'switch_1', value: functionOn), + days: _convertSelectedDaysToStrings(selectedDays), + scheduleId: schedule?.scheduleId, + ); + Navigator.pop(ctx, entry); + }, + child: const Text('Save'), + ), + ), + ], + ); + }, ); }, ); } - static TimeOfDay _convertStringToTimeOfDay(String timeString) { - final regex = RegExp(r'^(\d{2}):(\d{2})$'); - final match = regex.firstMatch(timeString); - if (match != null) { - final hour = int.parse(match.group(1)!); - final minute = int.parse(match.group(2)!); - return TimeOfDay(hour: hour, minute: minute); - } else { - throw const FormatException('Invalid time format'); - } + static TimeOfDay _convertStringToTimeOfDay(String iso) { + final dt = DateTime.tryParse(iso); + if (dt != null) return TimeOfDay(hour: dt.hour, minute: dt.minute); + return const TimeOfDay(hour: 9, minute: 0); } static List _convertDaysStringToBooleans(List selectedDays) { final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List daysBoolean = List.filled(7, false); - - for (int i = 0; i < daysOfWeek.length; i++) { - if (selectedDays.contains(daysOfWeek[i])) { - daysBoolean[i] = true; - } - } - - return daysBoolean; + return daysOfWeek + .map((d) => + selectedDays.map((e) => e.toLowerCase()).contains(d.toLowerCase())) + .toList(); } - static Widget _buildDayCheckboxes(BuildContext context, List selectedDays, {bool? isEdit}) { - final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + static String _formatTimeOfDayToISO(TimeOfDay t) { + final now = DateTime.now(); + final dt = DateTime(now.year, now.month, now.day, t.hour, t.minute); + return dt.toIso8601String(); + } + static List _convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) result.add(allDays[i]); + } + return result; + } + + static Widget _buildDayCheckboxes(BuildContext ctx, List selectedDays, + Function(int, bool) onChanged) { + final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return Row( - children: List.generate(7, (index) { - return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate( + 7, + (index) => Row( children: [ Checkbox( value: selectedDays[index], - onChanged: (bool? value) { - context.read().add(UpdateSelectedDayEvent(index, value!)); - }, + onChanged: (val) => onChanged(index, val!), ), Text(dayLabels[index]), ], - ); - }), + ), + ), ); } - static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) { + static Widget _buildFunctionSwitch( + BuildContext ctx, bool isOn, Function(bool) onChanged) { return Row( children: [ Text( 'Function:', - style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.grayColor), + style: + Theme.of(ctx).textTheme.bodySmall!.copyWith(color: Colors.grey), ), const SizedBox(width: 10), Radio( value: true, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(true)); - }, + onChanged: (val) => onChanged(true), ), const Text('On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(false)); - }, + onChanged: (val) => onChanged(false), ), const Text('Off'), ], diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart index a2a109af..d6a530bb 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_entry.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; class ScheduleEntry { final String category; @@ -58,7 +59,8 @@ class ScheduleEntry { String toJson() => json.encode(toMap()); - factory ScheduleEntry.fromJson(String source) => ScheduleEntry.fromMap(json.decode(source)); + factory ScheduleEntry.fromJson(String source) => + ScheduleEntry.fromMap(json.decode(source)); @override bool operator ==(Object other) { @@ -73,6 +75,23 @@ class ScheduleEntry { @override int get hashCode { - return category.hashCode ^ time.hashCode ^ function.hashCode ^ days.hashCode; + return category.hashCode ^ + time.hashCode ^ + function.hashCode ^ + days.hashCode; + } + + // Existing properties and methods + + // Add the fromScheduleModel method + + static ScheduleEntry fromScheduleModel(ScheduleModel scheduleModel) { + return ScheduleEntry( + days: scheduleModel.days, + time: scheduleModel.time, + function: scheduleModel.function, + category: scheduleModel.category, + scheduleId: scheduleModel.scheduleId, + ); } } diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index c535bda2..bf9ab2cd 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -16,7 +16,7 @@ class WaterHeaterStatusModel extends Equatable { final String cycleTiming; final List schedules; - const WaterHeaterStatusModel({ + const WaterHeaterStatusModel({ required this.uuid, required this.heaterSwitch, required this.countdownHours, diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index f1e56136..1d80fd9f 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -7,7 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.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'; @@ -35,7 +35,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const SizedBox(height: 200, child: Center(child: SizedBox())); + return const SizedBox( + height: 200, child: Center(child: SizedBox())); } }, )); @@ -79,7 +80,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), - child: BuildScheduleView(status: status), + child: BuildScheduleView( + deviceUuid: device.uuid ?? '', + ), )); }, child: DeviceControlsContainer( diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart deleted file mode 100644 index 9c28d4d6..00000000 --- a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; - -class CountdownInchingView extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const CountdownInchingView({ - super.key, - required this.state, - }); - - @override - Widget build(BuildContext context) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isCountDown ? 'Countdown:' : 'Inching:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 8), - Visibility( - visible: !isCountDown, - child: const Text( - 'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'), - ), - const SizedBox(height: 8), - _hourMinutesWheel(context, state), - ], - ); - } - - Row _hourMinutesWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Row _hourMinutesSecondWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'S', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Widget _buildPickerColumn( - BuildContext context, - String label, - int initialValue, - int itemCount, - ValueChanged onSelected, { - required bool isActive, - }) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('$label-$initialValue'), - controller: FixedExtentScrollController( - initialItem: initialValue, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: onSelected, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 24, - color: isActive ? ColorsManager.grayColor : Colors.black, - ), - ), - ); - }, - childCount: itemCount, - ), - ), - ), - const SizedBox(width: 8), - Text( - label, - style: const TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), - ], - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart deleted file mode 100644 index 9d4a2497..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; - -class BuildScheduleView extends StatefulWidget { - const BuildScheduleView({super.key, required this.status}); - - final WaterHeaterStatusModel status; - - @override - State createState() => _BuildScheduleViewState(); -} - -class _BuildScheduleViewState extends State { - @override - Widget build(BuildContext context) { - final bloc = BlocProvider.of(context); - - return BlocProvider.value( - value: bloc, - child: Dialog( - backgroundColor: Colors.white, - insetPadding: const EdgeInsets.all(20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SizedBox( - width: 700, - child: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ScheduleHeader(), - const SizedBox(height: 20), - ScheduleModeSelector(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.schedule) - ScheduleManagementUI( - state: state, - onAddSchedule: () { - ScheduleDialogHelper.showAddScheduleDialog( - context, - schedule: null, - index: null, - isEdit: false); - }, - ), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) - CountdownInchingView(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.countdown) - CountdownModeButtons( - isActive: state.isCountdownActive ?? false, - deviceId: widget.status.uuid, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - ), - if (state.scheduleMode == ScheduleModes.inching) - InchingModeButtons( - isActive: state.isInchingActive ?? false, - deviceId: widget.status.uuid, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - ), - if (state.scheduleMode != ScheduleModes.countdown && - state.scheduleMode != ScheduleModes.inching) - ScheduleModeButtons( - onSave: () { - Navigator.pop(context); - }, - ), - ], - ); - } - if (state is WaterHeaterLoadingState) { - return const SizedBox( - height: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ScheduleHeader(), - SizedBox( - height: 20, - ), - Center(child: CircularProgressIndicator()), - ], - )); - } - return const SizedBox( - height: 200, - child: ScheduleHeader(), - ); - }, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart deleted file mode 100644 index bb9ddc8f..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; - -class ScheduleModeSelector extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const ScheduleModeSelector({super.key, required this.state}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Type:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildRadioTile( - context, 'Countdown', ScheduleModes.countdown, state), - _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, state), - _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), - ], - ), - ], - ); - } - - Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, - WaterHeaterDeviceStatusLoaded state) { - return Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: mode, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - if (value == ScheduleModes.countdown) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - )); - } else if (value == ScheduleModes.inching) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - )); - } - - if (value == ScheduleModes.schedule) { - context.read().add( - GetSchedulesEvent( - category: 'switch_1', - uuid: state.status.uuid, - ), - ); - } - } - }, - ), - ), - ); - } -} From 3d95f2bef0ab4817e92f2fb40cd346ef2d878b6e Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 18 Jun 2025 16:40:13 +0300 Subject: [PATCH 05/86] Fix null safety issue by adding null check for functionOn in schedule dialog helper --- .../water_heater/helper/add_schedule_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index b09cb48c..ae7feac9 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -96,7 +96,7 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(ctx, functionOn, (v) { + _buildFunctionSwitch(ctx, functionOn!, (v) { setState(() => functionOn = v); }), ], From ce96afd7af97a0aa8f8a79173dc395abaf19b722 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Thu, 19 Jun 2025 09:03:24 +0300 Subject: [PATCH 06/86] PR fixes --- .../all_devices/models/devices_model.dart | 2 +- lib/pages/routines/models/curtain/curtain_function.dart | 4 ++-- .../routines/widgets/routine_dialogs/curtain_dialog.dart | 8 ++++---- lib/services/routines_api.dart | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) 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 0a1e5643..e491214d 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -362,7 +362,7 @@ SOS ); case 'CUR': return [ - ControlFunction( + ControlCurtainFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH', diff --git a/lib/pages/routines/models/curtain/curtain_function.dart b/lib/pages/routines/models/curtain/curtain_function.dart index 09e3b7e7..e0689e5e 100644 --- a/lib/pages/routines/models/curtain/curtain_function.dart +++ b/lib/pages/routines/models/curtain/curtain_function.dart @@ -18,8 +18,8 @@ abstract class CurtainFunction extends DeviceFunction { List getOperationalValues(); } -class ControlFunction extends CurtainFunction { - ControlFunction({ +class ControlCurtainFunction extends CurtainFunction { + ControlCurtainFunction({ required super.deviceId, required super.deviceName, required super.type, diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart index 94a6f15e..bdf8660d 100644 --- a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -21,8 +21,8 @@ class CurtainHelper { required String uniqueCustomId, required AllDevicesModel? device, }) async { - List curtainFunctions = - functions.whereType().where((function) { + List curtainFunctions = + functions.whereType().where((function) { if (dialogType == 'THEN') { return function.type == 'THEN' || function.type == 'BOTH'; } @@ -136,7 +136,7 @@ class CurtainHelper { static Widget _buildFunctionsList({ required BuildContext context, - required List curtainFunctions, + required List curtainFunctions, required Function(String, String) onFunctionSelected, }) { return ListView.separated( @@ -184,7 +184,7 @@ class CurtainHelper { required BuildContext context, required String selectedFunction, required DeviceFunctionData? selectedFunctionData, - required List controlFunctions, + required List controlFunctions, AllDevicesModel? device, required String operationName, }) { diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index 455de5ba..bdc46ac1 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -40,7 +40,6 @@ 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), From 09c44f8a5f85ab06eeeb6fd9f35bcb8262d6f29f Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Thu, 19 Jun 2025 09:33:45 +0300 Subject: [PATCH 07/86] add comment for problem solve --- lib/pages/common/custom_table.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index ac96bce8..d2923700 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -111,6 +111,7 @@ class _DynamicTableState extends State { thumbVisibility: true, trackVisibility: true, child: Scrollbar( + //fixed the horizontal scrollbar issue controller: _horizontalScrollController, thumbVisibility: true, trackVisibility: true, From d895ed74d2a5b02ab5c6e19e79131a0c16f7d89c Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 10:49:06 +0300 Subject: [PATCH 08/86] Add scheduling functionality to device control views with dialog integration --- .../one_gang_glass_switch_control_view.dart | 58 ++++++++++-- .../view/wall_light_device_control.dart | 52 ++++++++++- .../three_gang_glass_switch_control_view.dart | 88 ++++++++++++++++--- .../view/living_room_device_control.dart | 50 +++++++++++ .../two_gang_glass_switch_control_view.dart | 72 ++++++++++++--- .../view/wall_light_device_control.dart | 56 ++++++++++++ 6 files changed, 342 insertions(+), 34 deletions(-) diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 997be513..694c2f6e 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,10 +1,15 @@ 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/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { @@ -76,15 +81,50 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index f1861c55..e590adea 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -1,11 +1,17 @@ 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/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class WallLightDeviceControl extends StatelessWidget @@ -55,7 +61,6 @@ class WallLightDeviceControl extends StatelessWidget mainAxisSpacing: 12, ), children: [ - const SizedBox(), ToggleWidget( value: status.switch1, code: 'switch_1', @@ -69,7 +74,50 @@ class WallLightDeviceControl extends StatelessWidget )); }, ), - const SizedBox(), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 21a81df0..7d80f289 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -1,14 +1,20 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import '../models/three_gang_glass_switch.dart'; -class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { +class ThreeGangGlassSwitchControlView extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const ThreeGangGlassSwitchControlView({required this.deviceId, super.key}); @@ -17,7 +23,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons Widget build(BuildContext context) { return BlocProvider( create: (context) => - ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is ThreeGangGlassSwitchLoading) { @@ -34,7 +41,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons ); } - Widget _buildStatusControls(BuildContext context, ThreeGangGlassStatusModel status) { + Widget _buildStatusControls( + BuildContext context, ThreeGangGlassStatusModel status) { final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); @@ -107,15 +115,71 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) + // ToggleWidget( + // value: true, + // code: '', + // deviceId: deviceId, + // label: 'Scheduling', + // icon: Assets.scheduling, + // onChange: (value) { + // print('Scheduling clicked'); + // showDialog( + // context: context, + // builder: (ctx) => BlocProvider.value( + // value: BlocProvider.of(context), + // child: BuildScheduleView( + // deviceUuid: deviceId, + // ), + // ), + // ); + // }, + // showToggle: false, + // ), ], ); } diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 731b354c..f4739b88 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class LivingRoomDeviceControlsView extends StatelessWidget @@ -90,6 +96,50 @@ class LivingRoomDeviceControlsView extends StatelessWidget ); }, ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), ], ); } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 575deeac..cd80f528 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -1,10 +1,15 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangGlassSwitchControlView extends StatelessWidget @@ -16,8 +21,9 @@ class TwoGangGlassSwitchControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) - ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), + create: (context) => + TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is TwoGangGlassSwitchLoading) { @@ -92,15 +98,59 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Scheduling', - icon: Assets.scheduling, - onChange: (value) {}, - showToggle: false, - ), + // ToggleWidget( + // value: false, + // code: '', + // deviceId: deviceId, + // label: 'Scheduling', + // icon: Assets.scheduling, + // onChange: (value) {}, + // showToggle: false, + // ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ) ], ); } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 882aac3e..05a02a69 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -1,11 +1,17 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangDeviceControlView extends StatelessWidget @@ -44,6 +50,7 @@ class TwoGangDeviceControlView extends StatelessWidget children: [ SizedBox( width: 200, + height: 120, child: ToggleWidget( value: status.switch1, code: 'switch_1', @@ -60,6 +67,7 @@ class TwoGangDeviceControlView extends StatelessWidget ), SizedBox( width: 200, + height: 120, child: ToggleWidget( value: status.switch2, code: 'switch_2', @@ -74,6 +82,54 @@ class TwoGangDeviceControlView extends StatelessWidget }, ), ), + SizedBox( + width: 200, + height: 120, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + ) ], ), ); From ed2a8f6ba28e63951ef5fa469d69614292aac3d0 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 11:02:23 +0300 Subject: [PATCH 09/86] Refactor border radius implementation in ScheduleGarageTableWidget for consistency --- .../schedule_view/schedule__garage_table.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart index 0bd347ab..525e79c9 100644 --- a/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart @@ -26,8 +26,7 @@ class ScheduleGarageTableWidget extends StatelessWidget { Table( border: TableBorder.all( color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), topRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), children: [ TableRow( @@ -56,16 +55,15 @@ class ScheduleGarageTableWidget extends StatelessWidget { child: Center(child: CircularProgressIndicator())); } if (state is GarageDoorLoadedState && - state.status.schedules?.isEmpty == true) { + state.status.schedules!.isEmpty) { return _buildEmptyState(context); } else if (state is GarageDoorLoadedState) { return Container( height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20)), ), child: _buildTableBody(state, context)); } @@ -83,8 +81,7 @@ class ScheduleGarageTableWidget extends StatelessWidget { height: 200, decoration: BoxDecoration( border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Center( child: Column( From d45fa4c9570937456f789c632b46bf8912e1a44f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 11:12:12 +0300 Subject: [PATCH 10/86] analytics hotfixes. --- assets/images/autocad_occupancy_image.png | Bin 0 -> 297264 bytes .../analytics_energy_management_view.dart | 4 +- .../views/analytics_occupancy_view.dart | 6 +- .../widgets/occupancy_end_side_bar.dart | 62 ++-- lib/utils/constants/assets.dart | 291 +++++++++--------- 5 files changed, 186 insertions(+), 177 deletions(-) create mode 100644 assets/images/autocad_occupancy_image.png diff --git a/assets/images/autocad_occupancy_image.png b/assets/images/autocad_occupancy_image.png new file mode 100644 index 0000000000000000000000000000000000000000..39d9625b5b5cac6970b9e9ef520a0765e9cb9852 GIT binary patch literal 297264 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

rV=nW?D1sUN3o6C$P&Aae@8i?T9Rt!!)U$f z80xq343l3N1HLgt3Kck~=6>l>xmKQnr95J{J~3>5QYgFRD*C+k`%ghYTS@j!o|oM0 zY_I-Brf_NUZfI__I)VG+c3~yMAnwqH@$5)JENF>>`e9o>#CC|-91&k5EAmNgpE6tX zOjt{fhms&Nt0@lV+yj$ms-sl$?y4oz=*i7FtmkKBp-ms&*Dq>GX&{g?^-qInQu9}Wq#&5E<<)ghd;vR&Xh7n2 znq$Tppo#L`Myn}97}D{X{lV>mznba@zJ*X2FINQH2IDU6-?D{ z(UGoEEYDnile|f%rG~d3D`f0zpCVNq`BkeTW4X5=I5@PCgFMFK5;`dcBL8qKb z7#?FsN*eWFu&fQ?bOQn^J^aeD&`o}Wd&ol{PfysLaHb|pY_J)3e65Z|IAm-Fk+bos zsmE3_zKr3L8Yley52E|w*VEglCf#`{OKy#aON-VBryFxmw99Mcu}#@xK!QGcAg<{- zg}H)**`l7u*0NFLF`yNjCAya)Re$Y+5h(Rl_YiMGG|pBJ%!wPSOnDemIW;a^QRrNS z?BWBR$hAv>hFxvZo#Djo#$Z9|WX{AB4OCTh_^NqXX;yi(a!vL0d`P1UwB0}+M3z;U zwu6AwJPutuVFNk(-IG$;t$d>WLB_3NghfQl;NUmup{QKJB-d7|7N|aI?-17hj#X`b z6>rwcJtQ*CV=gAbO};|RKy9y$(pm<&<|CUQ0<3%%XuF6$y376T%J=UtkxkBAC*o*d zc28mw?blwFiSgBt?5Eb$IJ3@9!CQBBu}^;84Acj7v$WO3(GqB`I~al*3ZLoFo@yx& zirR^}eNgSbA2hQIe$Mi;)#Kb};<1a(M_Ibv)$#b>v5Lqkq@9*7>inoWL)b9|^2FAb z>GR{pr>;pc&)QAQx6Z6yo&Q$F8?@bWm(k42%L8kRE^4tXi7s~RE8TKYE}Tdl1z?xW zxIFLgIadTlUj0o_j4d}08r6ZVF^3u zzMd=noBgd{;;Qk_(k*clKbf=8wf05Mul2m{-Ardk6Ne(dSTTHy={SW}J{e@`ce$$5 zBCFu4bz7mgE#ib2G8lN?XWe3(nM!DvlOyQfB->7sfwS5xh1hagZdD9vgK{!=Y-}l= zT%v)3m)wO?J6o@JHaBSrF0HQ?c0s3_iGdP_-U1_;Y+GHpzr$7W(ZzCG&B6T|Q6>)QedJ`ePPt_}2XB&g};Y!-+B)Q|)W;!%)HkGA?3np@oXZB$fLgU2Ps z#qA7#E)-BfDlI5v_}s`R1;k>1?-??;<%uI2(Ad=zh5DkDveNqJsuhuVzR)9Inw(6I zGBa#hU%N6{Z1EXvP!>FU!N4k!W3ck=C$rj-0KS48vw^c7hs$Ae(?cWUA!cwW`yz>n zgoC1Y%_KvKKYI&e*wj2MEa~CE+p>@h%ey1*9BNO5(`NnN%k8kY8NhRtlqWcva!2$&Oqp$3=*VGvTKO*qj1i7(;O5On&>g#<`R zx{$rw-lyAVXD#}TZY9nlxBi4?L(;?%l0z7P}D zh8FeYNoPvS2@B9X5o>ovJhw%azOgUzSp0gK&>e#aqO^yLJiJj6nh%<_YaFO2;(0p9 zh>v%)A`c%t2fCa0U+yF>Cs#S6RDm|wZ*H$IH)6S(Pe!>UBu<8926w+@-Pm;d9Ob@* z)88D%`q64@YhUcz`$ZbvZ0P_*%sWXTjjS({$8|Y+^4`jugTtzWi59YLJftanb6KA;`Cq{Oo+du7M!~lGOrHZoTF{j_cmGq^{ryuAVykU^?0` zb%r=Pyc|uU-wq@B75^KXs!Qb~FDB4GFP~(zH}M3?^E!n32q`Q0dfxYHcaZkJlka0gAr45BqCUNNTefj<>KB;EnKfH(RCv51 zM{rF&^PPS3q76x&urR+3ouNU99ar5cqqGJ|1I}_rxaaEeF`cHuqY#W}Lb7+WVm3zM z*f9~b8M9Qs#*&UDT)%Q^C{&-rUbMzj(mMl6yQ;B`duHz;p&ygl9b*7}>jxLe@JL!W z1;MwyBv(_Phum7}GgZVbrcLF2#XW2!+4`Npq{i#l zTPNS+bY{PW(rZNRPz~M#{h4sQ6D(FJk~MT?PuIkc zF?0^;08~w&;{t|O{0A|%8MxpZKgi6IPHgUH!F{C|cu5x|QRU&WsIQR8CkIyQW3mo3 z0Z>?BWj+E3^C2?)vk$gp4{{p4=$K~(<*1Yfzida82?>Jgsfo9VXA_&(m6;!Ej*rZL ziCGbs3yM^&g+W_W@V0YJu1v)2rb*P2qU0F~g|zF%A4xavxkkZAj@X@u4TmL}0%35G z%nt8_`M}~BI4gD>64f7q4{jJ{@hgS2NQ{X411K4RVVyhxPK}e@J-k#9bp+-Z9wSkh zfEl!T&P;thS!~UWx#eOy3JjL!(F-zOejlzVI-J}_ID7mKDSO$FSSJVEb4vq@h+?Ij%FKYAz{@l1apT{vFtq=%*l zau$vnr00|!XK?xB4aqIds4uA>`kM~Uy-PIv$Z{f&q^pT&Mqdy}6mAWRbeE8t>mOVY z+s`74XF5%bLSB(V+Qf7RnX#*@OqosCi<~hMKu3d&a&{asV}+ z1B9IwRaE5osWI_s4a+F>YVBNGQgS|6rK$EnjIcwDg^kuqB+i!-vle-Kl5^_tgx0*I z6PjNx0{uc!KBL3K@9}8_-`{P>JJ{A`p(l|T9xy7Fy=7~O@Tc55J@H=Ke=<_=!`Ko) zDC65r4|CCTk0GQFp-er1rgStQ5`CX5#rnGuksNAr^JF-&Ot)SFel~4!II6_$k3|n} zVfONfp@kANTupVuJPUhq$&se5x2I)SK0O>+e`cz!D}1ip<~&xTRSO>b5mve{0DUGM z_nXgQ68-0tq4SRb()wSR8VXT^nOQ>A)c2Z>Rgp-f3veaI(TaKyt#-$DuMeany4ave zirU%}@87W{>*V#JILmC3Tlp@VZ3-G!V?;7YM1kb%xK2;;+S-R>Kn0$zw0=D;25wLJ zhzOwbx@uO?`af|wDpFGSBO)S%Q)ccx z`g40ktDZ?%U0W-1-aGmJ`V#2n3#0_M&&wXb7dX5&$N&v_nW5)L(Ed!X_E181@!7SZ zqozxsMZ+u94It+$?YfL8vX zr*P=Pcd(Jf(Zx%83J3#=b&mlj%jyWqIvBoIU+%qkBo5!<9>OOvMrGZeK(6=PThUJi z$a8aW@HCqY*G_FCunejK>jb_iQ8Cj>jfbUZ$WZ}?YO{fLHY(!Ai!|@JQb+cwkSIOA zn4usSa6LuLWff@TqoVM>$wOw{CvxGG(zlRQRz!Tol(8Uhvfa6v@r zti%@>@kk?3VFmD)B7q(Npjff9ROg9Z+uHecypIXOx``0GqYg0ol^DSzbsVk+$cCl<0_5Y^JrITzM?Y>-OqKbNQ`9u9OERz|?cbPVAv zX#SFA{XK~uP~)Okbaz`m4 zL{7Ba#<#&&VX zd>kAV=AiPe;O+^TH+elJ{X|?*VRJ1w3tB9O5R2Pqj$E5M8N2DZ;uWwWRk~=?Ew)HG zqL?ru^&lTEi>r_e3F6rnw$B~gl;o~euq{ja))?87(z}FF?lJi3$siz%eR${TtG+qC z>bBvpbx*buSk*D=W0pMZ)G5e$Nei@P(yQ!zc!Mwr5A-S%W;|nZ(;}d{W$WoPjhkHT znS@VgYJoOxVo8E}&O5?;7}W^$IJ|88IHn|SHq}JVBaeB4JU}7{Q=ney;+C)_c3}35 znQ~-=T#IoAPxb;23?sA|IrueBEf;r%4jXDSM$c~_Y<@sP0588}3$fr^zu!pk!fTiC(ji8O+g+XRQpo zOmh5KfAe<0Wg`#r&nV7sM7&W7|C8<2kgTCa*YK9nrr%jq#qApjQ^t-|zWdn}iPk)( z%2^mmrwy4{kh#ef7pU!{RQ@F5@es|Ldb$b1F2(bOtILW>zoeFp(Q)5!nd!F46-=u= zU`^kJmSe0sCPGs|KpZ3g;obfX^oURa$Z!*qAJP3c%n&=?9=$TwPN^!e#axX*XDE=* z0fB&$Gp7!w9-jUHCRDGcfx~vQf?f?BZZ9nF7g6`Gqm9?Qm5Mvq7L;lhHYz6>$&bNJ zB>o60K(seumOLk8Ho7!@|Fr@%g+a@`DF%QSJ6oUEMzalCK6AQC{`!^JsHGb=qq@-} z8@urnAa?xEz3sv}g_Y~{N*TiQEyI|{0IWXdiC?3f>f{Sk3f>DR)OgJ8y|+1IAxHLy zL-pRq335A{)oUOaL@tE@hoW{`9Cf?P3qILGw_mu%lAy6h=(=5-G}}e?ju?|#z$6ZT zjxTJpIX?qjhDW)Y|4;GxhO)X#UGob0pAlZ!a&nxu1CJ%w!g*d^P!ttdpHG%|{iv;k zbx-9a#*1QSuNl=aF_NOyZP`<3I=&jb|L84DBSx=z3VkP4!DYShUvlQJ~{sPASl{r0O+)|ZB5S9ie)4g|@btKN5~kV>r&tDWZHM^W-b8m@I6Ap#ZA zisL)Ut**tNI9@7HalLk;7o8?KNPRe^IwV!^Qy*2t-aP(PBV+_<6V&BScDG*t#>n*2 zPmz4#&!Zb2DVN(CXx~0Pn1DY*ufive6Qm(T=+ivN)%*OA%m-~1@%uWR=_zm5- z28r%+Am~EUB=2`kWW(;%OD(DmI+4$L$8{xLZA~6@jpxEqDnIgtw>76!%G6$99WZne z?YF=8h{fRK6Qo$f4DnyEQfCy$a}~lOhB6hGItRV{zs^^#X=huNVh5@J%h7qTFpn|` z(S5ycEjQIvir|v^<->*?_aPlpjhEGLBG#flyXwkggs`K!w@Zu{L#W8iv)fC0zpMD~ z5pM^)QtJp?b3TO4%x_tyRc>dI>y=sJCeI2roUm7!IN$VMUM3B~`gjqpRh>n$iiHgs zjPY>8MPght+rt;*ap~!i{_-vK`JYiMl}F2|Nom1ar)}x0@ec-`CF;D`P``d$(fe#7 ztj_*Tu&3L#=7LixYyG`d>;HL+Z(u>yJlhaZ&G?igLU6b7ta))VF@G=iL z@5xL$5OIXxUWBux55CY`KB{{Yf+rhD&iN6euW2}9e*Yv+MBb)gz(Fg>!>}So zmP(r-|1o+qkF*Avq1emrxGzj2BSTjEDy7UwHP4ZuO>^Tmm;6-RSw_-x+{=nZG&+G){NurgIn0$;6Asekj)@9$Qnhl6&5aEu>#X z`H^C&yd3XD)qhWOY1Satc2;ni0(&cN#HMZh$`7JYe;j{5)BBSG7*scp&e8K%gYEB| z!@+h=(3wB`@$$YgV|lqpu~Vrbxf*f~{YMM4i~JYV0Qw{}Zbn_oU8 z_1^cY+lc5#WPYO{#`1&|sywRgH}XTn&NK|cxBuG7H(LW+r6=SEKtqpxuf<3poCadC z{|?w=-eH(waU;&SKD}dm?^h)Z$KSn4Q6)Nw?bc`N`tkmV0yv2How9p*=k?xov-4NHCAjk-A;bMYe+HErc^p!rHzz*&Gi4V$OQm%YQ|3pfl*}o` zL)jt8>_-@m%ryR&zy50(G!|1Vhjaiv`4HaWB+Uqe9?A{GXCy^^p4DKa1Wft>IEuk6Pl&TI{$EFw2U>oW1l-`7Uh zy1uCFH=rY&7UoOeFcx#;Ud%d^_cR)y@}vfykN^9l+G49TENV&@B7~exIRvP2-FI*2 z$q0$JUC8-j$EC}o>FCtC@7JPwpWOsmEvTq5@2{SXMzZ+Q#%D~bE0d!{G}&u7xEfZHRs^f z@!GigY?6~QJvx>@-3D@16~n*<05EbZ<0E2=otfxxh+sAy{R+WYRN$;}MA$LVCo zXZX8c=fE21f7Gou_+x2j3Ys_OQN2rbeZ%l4G)3W>^xUr_P{{WHe(gCewG->HL}C09 z$Sw%-MM=Kmw+nysgm>fZ_0f5)>0HFfucv1M=hwfS5s<4rXr`MQrzK zkRPBp4-k+T!O!2$=3RUpPVkNKiFU0biYq3=qS{K#?P(}{lQm`Q{w zgNp0Rj~le3hjACWt4sjF>A|!-F<2s_xnDK zIQ?0Hd6T{o5={pL)cLm*zKx%W{KFpfYrcO9mDHOD(H2(6QjLE;b6n`PaBQ-Vu!sQ?Zeo2%t-e3spYG~ z@!7@BBZ{o=Bw4S*V#zvdK50qBN{E)qG}^MU^4wK^(X z^K@wkT8I67f?3#QBW`T)g5l3017OxR_5m}&Qt9Xg0Md2arHuzmfrNm02z!uWpv=oCnm>QiB=KuX?IYxKDW3zj)op~0$AQr9V1P%H5UHQzm z`WS78Hkxv?uY{JZ+rn`l7Xp3eq6mDIh7`NGl)+C>=^G-901S-8m>y(%lS5hje#$ zcMLP{4!`%-diM|3z+%olwfEV3pZ$G1(AFR=LLc0Wan;m-;YnxnJJ*z+hyB^19eX6N zeEP-{S?X5vE$Xh^5ByC$i$N-epRqsJ5Eh=A6cRQb-EmNb8-(YCmpvaALagaZJ+GuCt4DJptY%e;@X`+ zi2ez1J~W?NJ6sPK?`BCkc6z|H1D~rba)lEL^j?N{&avK4e|&=ymx~#gx26;A)wI=I zc|6%=dVNLBzua}C94G;xgAg!`OB}F-xDc5CbvogXsV2ZsA7KfLNF;20Cq5EnMnkEl z1Q8%E$qp-X+EL3J`(YJlk|lrC2ZT)Qhs3S~n^kNYc3$N@)@>PiK6>oH-J@6b#pf)L zjCsKzXcL*&18lRuW^K=yS`dk5{2r!2OoN2 zmzID~Kj#Q6U%Jl(r}FbwLzs}Vz_}{b;_iy>39?+9I^BkGzhG4Xso8o=&gfEa?#F(JzewI4SW?7ty{a8 zjymHY8NoP#2JQMd7RkrO^;%pU{8M3Ktz=7p_TC;){cPGA>E%4}S(CQd9rAL!7t<)q zgah+@&?o-fNU3GNo0)^drDUJ{o!Iy~`Frs#aPyO2k9#ZjLEeEJ5FG}5No0l7fzKKm zbWBX$DZy86B5K-lmqv*`nZL?j_ytGi>{4CxIK-rF3Y%FdbY_Hx4_KMhJKKNy{3ApF zMD_9|PD?rY-BF=0qRVzgotQ~qi9tLYdzZ9la6l%N2?SmBWn#yu*M|Q#AbamTEJNAh z;-EHPM)!@md7b{cgYg8_H8?KUP{4R%zI5$OAcv8i?lwxkn z&i7d7)cSn>)oZQKJ(+o^cADNVINz|6eWuWhf=I9CQQ?$W0OD?qha}bkXEI^Y|44;2 z65m{Ov!wkEGS?;JkI#HbBFo?rV^V*BkFU?7cKmjXndP_ZrMs|<4zFb_0U=xnk1E?h zqS)l~zfCa?k;6bW6%kb2DDhk78k>dPfuAf8u!@P$&2@wGUiu9ms#>WjP@gJZi{7F( zYC5nBmu`i*s_XfQy1Ay{49aS9`K+>o$RJcELBI&s+8XtgzoV2lp7` z#RAgU?KzC1#V*zz6CKTfJ9Vn7SN;8&UTt51wA6b)e38R5phmW4cvp0*0&ERpYqVrJ zv}}5B)?{w>0(~RE@4oSjAK11;7+oGs*Loofwlj&$6H|lrYj;nFn8Q;=iNo4O%JiG@ zj?!Yo*#7I)Hg6fgOgf@X=lQF*euFn}N)B;Q{CW%Z9*j6btdw_CwQE9eKeOr84c8;w z#EwkR1g&Rz^8^f;37+7og7GZvT#P&Zc50O!fncW)urDX#Q7-rv%*D8XBUFj+X@ZNF zA?hykO_#2Or?|vbnEE$e507zoq5?I{1emc;zw@v=uPsN**gWy&EQGHf>_;aZfQ>(R zn=RB-Cq&&j2eP;fKrT>}Za!Z&#R<%mjm~}8rHG9W-sZiSh{Dw_tq@9nW6vCDqaXFT zT66V2>@4~)%-ifW5H(ddlKDz0=$jp5F)9y@*v!?b;=g`VRfHDm@k(4@B+9nYE$NP2 zk>adR=we+$V>A)DNWsd_Jb@hN3`Xvv3bvQIAs}{wP@{=Ip9haq^2mmPd)!}ZvBepw z*wu4_mVfqdic*7xsmr{bOGd5YamYe;92eZo&gllu8&?utuMh>FLxKmHZNIXZ>A47@ zA6}z??gub2Z+f<;k2~8wYuyg}xix*~K1@!*PJ-Kr>#b1_rsC|dz>TS@7cU4IT$&1L zqr8!5mhE#StXCMrWUg{Q=apwsr&J+)&SP$LG&IS;qxZ%6=-8N~jMFO;GlHL8W-{{! zXx-%BHp;$-K9^~K%xrB#u(^5OOFN1G* z4BUyWh5WlFplwOEHv$Sgop~<0D7%UrfA7SdQN=YMKP8J?&KF>~RMAK}n2vGbotjv- z0P#*@l%m$)N$Gm?eIGB=%KZ7@USv=qFB8mtC_vnA&VM&un2SS*3knL7#l3}3Pfyh#XIbAg0%k3$(IP z=Vg)0OdD5K(ml`eQX&{}{g=*P!EvNGTY#I$>+#ZS&FDA0j4nZ?kNip-UP_QLNYw_u z-9KAG`z7YcD`yrOFviMtXY9IWK5If?TGvq2uV0RD zI6QPRg}BF_%_GSA%}f|ks9~h}g}3B-cI$W%pJMEtWkBl4@TaKpy&N zReVyFc+NjlH8keTw)LG;WK+R0Iu0^wAS{b?5eI!cZt8)XsjR&7H69mrUSFAPRevek zts8v2X5CPBGIK(#!&t@R7{mvkn+)jJo08h{TJGS>#E`m6MMxVH@fJOQq3RSB>0+pC z-D-uLTOhRvs@m?fJ(mm9nAR}kNF{JR#8Aw%=PIjLkIfszj}4fHS<7s$eTf-E*NL& z6*^P7*z7x6da8{ER(Y08R3EZELppW|s`p$xW+?#*`vVqA#KYxe#sl!d+z0cRK-RgW zhY?U&zdOjxGnB0hDvwVfA-Y%GVi!GSn7@?~XyPKx*=VtI-yAs_P?nmQFPq2c>5HGz zm);YB9IRCm%4GEf$&$RzgX*5@dw%L!q+G<)T=rXibuoT7Si|PlXTG+p60GYr0~gDF zjr44RC*#Az1mn+arzP_MYDRg%zP&Wl!^CNiC5B?NH;gSdqfS0>G)G>}Au9B+S;S*M zX2cr)3A-413ypk^x@kza7SMPAHh zFe-R?@%LAd?Ecpeeg|Nk`}53iARpc4Bq_C=d}93Tt%zh{Ky;YAuO%2~vFIo&c39cNx~lkYwq zzA=f)jPgug81wL`8#*-AZ!W$|Hoi0H|9SN%BWn>k0GVE=VeK*ZA4Bx^zkHNF^U2WB zo_sct{^`>Y>xhMncaOX^Y3FZ8tCIi168y}XgVT#jz^T*auRe3mAN3<6aYK8|{VvHOW!RWboiuEiN zfLD}UemZb#%h!@83EcG=+S|Pd{Wcd)LxI-*$!S|$+c0O(xDEH0%c^_*@`sCHFrhgQ z@ofw@&IR%S8KihvlaxPwarOZZkoe%>)%3DH8`*;{=t|_=Q8$&o-&g+;{LMvQ^0W-Ki>Oxwg zvz-%x120^!z3z?0uWQhxv7`&cA(C9SMffx#+*#mub2QhoG(ne|CsRRWxEU2_NdtSd z197w&L~SKO;HXBhoj~TF_nr9x>}c6@)1q`2>g%_@BIWo0g6zSb1oT&*e)3uT4ZeT! z<)EG#e0vW$QJ7>p&OS1HqGHX*zi`G5J7t;~KDx#&VLlU~rGNQ=G>ApQQbvTb_B=#~ zD*8k);NZ2rJX zMcgIb+zovQ61@F4hu*P=fDI0=UCq_5Ec-HO6%K`2Ue6j4r~c7c&S97ASz%H?mHRUL z_a<2YOtslRH@`!oPaw%FK`fiJ_+zi~M`EAmN$V1}@o+4n6;I??$N8^mta+SvcJgRCX4g%-=0Rzk0!)MKXT(+Bhh zcrSGK`fOKqp&$zRKH}wj%|WNYqxcBc-ug85cG2MFqY&cb>o`fa>ub-C%U6Hd1r7}KLh+L=l-Lcg+9s8&D2(lzY#zG@C2 z`y7F)S0Oox0yUKMxDgCbcERGH-f`FTp0LTJabM!^ar?vFGlP@M^8ZLAh{vUL77pXLQ0;s z{MIn)Yjy+4QT;bM^pRp(O>z11mXGZsZfj9P#+EuluM9o;a&Z)VR3_iQyz}oWb~~U3 zx1R+$Z>PFZWj?ze1{;KrtlqG4kB@z-S;JSAVC1wqrnlYJvX|c9#WZhH6zKZEA$>UL z!pmC~is~iCM*f!^dV{ejC`v&L_Gk!(VP~6O2{-A9VL)0u82a$_PnzPmY^IH6QOG(ZJe7*T!M`5s{-FdN=p>@8eO;luOA|Kd42^Fes-S{g*m)kF?N| z^g|i5@iD$1D>pr_oL_2fYvvRD5-Iyig%Wu(F-HR2FaT8Db0;O2*5(y&g!!fn@5f%Q zA(_3F1h*a6sIS(JxQQ0$=cxQEDTYUK>i0<}YeNH4fflHI^wt(d1=1O4HEibCv{(f# z{7v`c==-C&EA3Z$Vf`{((-rrn*CRtztsJf#r>_-8&NrdumI#BVfp8of(siNBA5)fD z-~Z8H-$SY86+5AJTUOMfgSS`9ORfk~XhF&km4U{US0iIVlUeA zUdG#xv&%WVbLgPhLP#sZ>>#eijs}TZO2_6*AQc`m zavL=HMOXb<$^8yKCTPo?vbi+lXDl|goPOa8V=Z)1N@tC)mN)q%1g(jII^&ZecmJ{9 zuOsVEI7Rm04sjt*_Q%EfLV8hu8r-ip5#Epd((iWdHj);@O2yqdK|I#NEkYuc?r{?S zN3aX&g~SE_An01B@dU<6%nP!j?6l)?oTIM3Romq|iCbJEvyjB82KnPk&U+Fl;`XJM z+mJJ}YeB}^K>u@fYl;$hM|TgYVc zQG0c`H@w`=PaSx=wUDUD>Ku*dtM>0%hTZ!}oz;H+MQPd9| ztL_Emcg}?3h6+VNkWoH(n7?#zpwsW`1u>^;M{cp7&#;0nSA$Cl))Yi7X1pIwaKDpo z$~*s7(Cr8YJ#KWwuD>VPW- z;Ca9R19u0`|56lHF30KV$nw*lf68$LjmXejTI!PI?gVxp?7B-DV`YN9QBxyc)Or1} z*r=NO@@eX4-{;_Xw$^V~PGp8W(W%+A-J$?x4 zn+YH{S&OU{B1igjnRn1=!i=J+}@UA&QlJYxBP*0BAe!k0aV=U=I z|5;s*cW{80985P4@DXQCyV&44R8)KoJ@Xwj3_xQ)XrHcS4G)ihvcBnyJT>xxV#C0m zR{Ct*exAIbn;a7SoH>t{rvXnzyp~kbl)>yn?{@us%~W}dHyZk;o=?BY_9U5{^Tgx4 ztxEhOcVfjAwV0T=p!B~k`x4V2rK}|8B9qTw#sLz<%oDo9yS+IQUV!l~^W*ehm9v8j zlPs}MHY>k?nhK?WmHC3mN!KXRzGWSS0LOqoUOD~{H~*sdKUH47N$IhRj?fi~MAiUi z>QvtvJ7=p!2D)FMf%*YSWtLg5N33Aymxo>!6eWmR>pC+v{z%I7MSs$Tz&u0+QP+5Y z7MCS~8Xm&L`EG?}p!>@iF4-=ozp%&s#Lt33sq9kB9NEiq%JX%~6>BX7Ec+Vj`f8P4 zVCZdUwP72R`F^WolxWyiO^{J@gUYbtQF53>$HDRJXS97U+#%Sxi=M{`V;$@d&T9LT zJAp{Js~v)PF(~ZtTA-%N#MrCAH@@rpC#IUOX+}#f1gK8}-t_{E^61F9{yznxbU7N= zEcz>knYR=bFx}IvsYUQ->gVd_T}V(7M?u%R7bI-AY1~NU)FNh3z zC{M@6_}R?``SQxEb^QttPlJcFUjDRWplWtb+ZvoHA%+h~t!qUHj^=(fvEi@C=o-u1 z`mlSdSg}u~Yi}Ae3(%kE+|5S2uYtc3NadF|Y79lneF@gRTBJkQJHGv_#jYwtM?<#@ zUFV8hb(}1k3(tYVUxONr9@~KT_v*gM0L5zq@LA169R5%Scv}!=&hE*bA}6xIetl#IH7n63@e4XX@7wP8LB{ zZs{mf#nCjEL~)>ZvwLo!86b)1JGvTSxysuu@wUjRg_zV3-dc>%#85Uqal=9_`@>b+ zwY#{~VS|eLlshgA1@n?D1vU=SJ^$Y;vYsALBp-$O-0(3P9JQJMp zoo9fWy0bsar`#Q8v7#2P-+cqs2->nH?{$9lTxcQi!evvRVazOSw$?J=X| zwT&uZe71!kc5RCB4L733KE(QE(I$V~%=y75@9r7F=~k-wNf5PIu(=LLXKl+H)SZ%P zx8d9Ls5Sa*iQpqR+`O(3Mtr?Uc%xsk6z!RongkA(5Nkmv$EnKp7y?nCS+z3Rx<8*` z?oKegqbs45z?43Vt(^}1dnthTCz8heuM-#)h-uMG(MfZcRR`Gxt%~Fl-^AQT6lZfOibOUuc5E2Qc-(_gKk1 z=KM%G%(s1rl2BQsZt{lqzqBfyLN<l-W}E_C|rbFL}Fpj?KM$|K6v2O;TAI`}K28 z8Zmkl7Ldd##_5~y*SZCO&3_xjeFIE0R6b# zqsH*(994(PKZm&K;} z@c}2{a`k<6zJMQzurnEvf&DYHoO6dap$TCpQ;U(MTb!+^qn#kyH#U|0Pmk_b!IJKU zSOxB4=O;uGZl{4?r?+u``b`c$1%^!r&+|L z-#dUrrP;XBGi@+hjXKxyah_Kc9*@Yg#n|lWiG+z`9R+QVKk+p$Zv-4Zc`S``zbmh4JD9jVqXbvW(0zYBp5f4Z* z&-t6+LfYbV=jV2M$Yj4XosxX_^XIR5`#^2Qewjbr?7OMHN=cLU9~Tlus0EgiLnA)F z{*J>J#`VUx>}#K-=vP_#;HN0R2He8g!6XRK4VI|$_A9Rv-pla6med`Ei{;5;Ai=H^ zDIB`pp^mMB5DEg7rYE%@L=A*ca@l=L;w6)BeN>FT!)(SXGBRf(-SW-pDd{Dy--PeC zpsRbiSn8|4(Jdkl;3>P7=D}z6FD?jC6^{{ylg;zCINo#^mz%PiC^b=06Vxs;9!Z!m zZ_De>E2+fqnN zeOolfCsVYXAG7@?(@_I=gFQnsl2X}{Di<9%14cKvr0uTaD zjUufO*TtFeX3wjvvaI2q>Lku5Er0($iH}co@ptdWx=epN&^0oFXf1`FV2F;ou(uq` zVu&O`{jL8USVV32P;(O*=YD#7*;eLvA$Ypn?lvRw;wViB-8iE_-g@NSYk_51g&j<` zdS|Yd^YP|7&IU@1T&%9hE$VjigU-71vm@yM`2%inhB~LEgn|3Ay=YVO)2aJ&6q3}} zjgtAt&?^_ct7XmOWoaY*N@#>`;I@8{_Dg3RDm}jsjeElmCKv`wP2?XY&@(+~iT&t7 zm}&ga=*>VR)UmaZIc!gB;Ey5Kt>nq~|CUxO*VNOUHx0y5*J{N>&QnJw3!Cy+gA`5U>> zcon?Y(&X@q!!dH*T`PfVm#9%iXpnP7X6tzc)rwumPCzXHe54R&Xm8TVOS+#fU$fhZ z?@!OX=*0%Gr+U0=XU^x|GF&9S{Ra;TX&2rPGMcT$DfTSaRMMPNj80fZkArf=f&ic# z17g~k0qGdn02um>QHR}0-5#RyirOyL)I5a=rbjGJ-|U)@+qC!nsL`|`z<*q7bUE;G zi#Cat*7Voz1McJdO>&fhiY2x?zpp-HvI*PeQhwhB5>e-B3!KFJHHu{l)m6 z=5Qb=>ab1)RA<`tK2`2X2xt~pQd0WTboy9yb|&lywp+SfI9fUQ&e%iH9}psmHL%dH zr(*5$uX%j3lVZ2M4&{#vJawywH|_$(ODLAmtCryqI3H{@1?-2o+=2#h!fz;IO^23I z!IzLVM4f;ZjH0B)?@SYGT>RqfdMc{J(R7laE$Fyi;*^&I_Dw>$8ANJuSF+ca`vC$I zfn89;*;^*3g1dInumud;lE52NG5jLO1%`7Gw+$q((U*oGf4v$A4&-N56H~gM{$kcV zB9?%?yfy@P1>Qkn*r1&ySzIm@J6AXbXpdUV&+~N!DVGhb5()i;_q0y${pnLe^RzwDmiQw8re;jm_ z78=s_h{LUTbG+<++OrKl3s(Q=-_lLxi5Vq&il#R;F#=ahJ#z(ZxmIOe;UHEdWQ>io z7fUQ$d^mHu$;$TV5ftF#pNQhDPqV9@$+^#T`}A*L`7$=1oFI;guk&Lk+2(XslLxMp zeMds0V0jX)L1NUmRtflKn;rmJ!N*n(vC;|dQtkgNbsZ^*;vAUm>SzhWq0%y??axE7o08MO$=&3p85tuuk6u_q;3<;{Ns;>jlNq z`ub-$5-_fo%j(hkZfrvW%*6TUwG?QooD^#OU#=k zR9b)BSm_~>cdz1r$|1xjz`Hbs?KYD@H%Y^jH9#G#Ptd_48(?|;ql3 z^P785#GLxA%xLX5pQw3}!#?>vh`cEEO`8=u(0-3upkirLObnjaWhF$G3%6AQj({0g z9a_jrOSi}3Pd)50lRat4C|oIiw9#yZTb~Ch1)iGxvNf0? z=z7Qox~m0~LtyKU(}f+)R!faVjR9FBsDy?WA3Ev>FT=G^>U zTO$tnA10n}A6|m%uF9Fvx4nqm)hRt9M69o0ACIf67%eR}yIoB;Ue7`A={O8qLV?s} zPbWFZo<|v5DDb~hJr3xy4Oy#|GWJ3#ei94&W)Vci%V zq7Wl0WT$)hgM5JxR&Uw*rD$kUJiamRy3_ba{|gQFT=k!?G{*sLnuCbq)Z?!A%_YJB zUWR!~q+$rBOHo<{(EaTvYu(43hz;o9mG&U#Bki`VRQZR#9{kJ#K9I*3vRy}Q zcc(ez;`v9j>5<|~^*rz5W!DF(|8mZOVFR7kkg0o_XUawLS6<>$b$oJiMF6xWv2*g_ zOD-^^=!1dIFya$nJ?E~85v!2wk`fL9PVQY`3V{aMq#kyIU~B;~=5yz98{v%S$Ocbm z++qu^rj4V1HWg7o5HLeaRZh-pJm4F1&sg$U&X!Vm%ugDjca7IMhPS5dn;Tz=M3B>n z#^V1ljZc)u#UMcanRjvE__N%ffh{#Z3v@5m&6 z)2p(oXn2dSobG13;tcfAq>%pa!u~$nqT8A?K(4rY*K%~{gbVCIib+x9Wj zrrSPR+1%;o!(5H?du2fMl}u#C45PjQ#Hww_$7lv#CyW3M_s&2w%o4djG&XJFn;6@> z4T0dxA$ZDR+6UzNO7(o_f`tK_k#uz(`zZzEdP;*#hNzY=a4^h7m$OMYd9TwC?Hc^# zgu-G6HJH63fIiHGVq#;jz;4bgtgU4QDK_u+(jUv4dLzWqKoGuWeK?~nNo z8jZgd6~hZo*2NpZE!eWV+}HL3lA*QT9-8_C+c1Pb+uhqeP8sr&Lktc$Ar}?bfu4CA z514hiKI_1C+6~OBsxqb=!OD?lf=fVDQ0Qx3Ce3XTaYQJBMEXP zCyqs_!6(Gv5dB7%RUk!eV(5(spwNJZik>|Tj*;t2s+q<#hOo}CA_IOscRD#adX#^( zC{QFLzoeuq0E3uIM5MbS4Vc&(0(`Hjg@uT=Hc2O!Lm5#jq%H@VvJ80GV6+SkQ{3!N z{T>TOW_#}a8yd3oL8?w}cWVTYl^kF_b3Je9jg4F&4XM60C_u2`20Q7t2gn}oDU9ck zb0pQH^B|kLxOkUVa~e4Eh`k0O~MXb=Pw1BQrBIK{H@Z zX1E6i2bU8x4Y~$18b9i!{u?yr7k=oOHtU1h!!Xv)9UOP0&=tZ^(%)oCP}o7e+sBjQ zC%d8jwl%1{vCZ~bc}P=88QB*8Z?$iFb#{R5@pQ!ElSDU5^Pwg3FR4UN9?OS$ z((Q-%=~Z0*-d7Qh8)?3j+0R}}$F0a*uOt8<&8lkn-LBBPC*6#|9rQf=Cveh3 zAC=YAq$JV3FN&OgTnCnkUGeWJl*+4Dl01mUG@b5GdRC;_{GD>>$ROFjk8X&mP-PQF`P0c|X)k~0~ zYkmFqq5v3JCAD|fAJ}JSXYFpsStx|PqBtm(fL>c^$jQrd2?#_rGzdSh{%=F-`eq_O z2saM$V!a#9WLST%VW6>oRy~|_zcBQ|J9pb7c0LbG{Phq3zv^_{0qWH=J}GIB4LX4< zslsh*vXq)i1}we9pNZ+=nfjNu&@*g+R$d;d1bcE#y}T&QhcY|;aK!D@3E3ZZ382v! z1%Pp`_n0q}>Qq%)D~b%G`264)A;k_NQgGJEE#}$7AddTjHpI z;te!SvH1R7jsd@Idpns*K~x+bk3wQCRyc+i3the#>V&i zeyR5l^0+>kg*@TbYoZqch4tP3FvE3mWGH9s+k&4hqs+M{Kmg#-e78ML<4@3CVb!sg zLqbX^{VGEZof>Ix(WXV+$VinYSYHky&we!8+Q$!nu8k061~_>aJ@(koFE4kuH2Rw` z0;K9ZF8(aabx0sbdw$Bs9z$k*WHDsT8*4?lx5)C$mWmt=nhy)(;!wY~HN7sDo*^|e(QZ^A@udII3EB~3+5m4>bxU5n#q--C%>uu*< z7X%$@b$y^bx@RD8aKdR;&r@!_ns*k#aWJNEwpWrql-ER9YR`Da;5xX*+`LT^yV4h| z>9lu5iGI%%t)LKSz+%;}HD&qB<^|tTBii%*fV1c=wK_+~qHm+rQay4VN4HVei&2ql zuHbuDq`{ORaq|*m4h!AG6^p~@1+6Zjp-AL!;>;Y;x?U5951t#I`^6fmz&XZ`?y2#d zl6u0F)mC?3L=#^ji$SpT^6aBJ_JHzruyEyVeP3Oe-LzrS(RX3%ta|<*=T@b{-R|Bt z0pPubKIUocwebV5#EQ}wd%TL=cuhAnNk{z8_Bga%mbea6F>ll=(y$}~#;t0}-UXe{ z?z?ooxrr31U$i|v)1Y{{VH>M-Ja<3m1QETs=}*;)B*c9FMqnnVL^g_y_-D6#yDntt zqwltot4zcceOZ(0Q%rdYb9ZAdp32~7TU>m6)n(0IRD~BEEqJHe?-IQoKxzn(NH?8- z6Lh^BC_N*DiG#D}?z^N~frkr?9msY+J=>Mj_3Ux4FDZnI1W4{WWS%yO_GHMG_%IN@ ztLFMD67dySX_}k1E~TsGQp85>@XSmBYHsoON4@RyKYmW11{lC|2IWg^sw`t5btcP( zbzld`jvUOY_qJU1P~ZMgNm+T76X6j4{1NTmTM#@!)=E<|DU;RaQ>t3c(2$Qi$~WMk zSuvNSh#5UHH!+d9YK(75a&27CN%tEFa7uKz8;{>|xR_qys#dIFVJWB_iB#7gtC zGc#9Lfx!%spgGx+83159JUm=xcW<&7D-V<_1|402R3vH*d^3xPO$_ckn8eHNs7^@% zGUnwi(BYQT1*W@=ks#NS)`bUd3!<9q>T;l8~(;+6yii;a>QLX2KIwhZn2*eS_EEVUX3GR`1kK0 zfTJ@E=8AWWgBx&@S~2*2dgTXjaIJO{8+P1ZEkTzB6V6_us8g8o`uE5IQdO^MnjUwe z-0|pU5l`w+^rpu(MB%P@>6RTfLVZ7i#ly6IrvuQ{66-2Q$MmGQc5e7zc6T!?z7-5sFKV#G^75;!I8HD?=~DNatH z1IHBw$kY+a*TB43_rhU%T*AVCu%^=97@R%Hns?P(!>u5AVbmiMJgESbeH90Y7A)^1 z)2l3t)62*lut%A+t>WE8TXH5nxPug0dum%Q*u{tz2L;H)xBXJL~4e#2+XnZ9N`wwUs9DCcqy)r>CA~Iono2 zoU81)zowqAUkr|5wl;z9mckXTdWs7QBzeg?f#TLbr@nA6aSxvi#u=LL&(-1>!cUWc zKzHcDqi4+`!EDpu`zbIb^g0ZhpP{UKp~%)1EI8XC$?Uf;*Ebr&S&a#Iuu&1e`Av0g z`Y}QBiWqR6?0{Xj`@)ExrmGfH9CE*JesbK7V+?@UJbZCc6p6I)8Cf9gOehs2Jt9*O z?@R6b4B+TRp_>^hZ z9yTnE^KC2FifQhh%79l&^#yL_dh^Cn>wW zjQMgT3iL^Yp)>R4ojNzg#%+=7ap62U9b4ZLi>1$lgHjU%hHrrlmK^D<`kH8bc#Yx~ z2QLz@qU&`gUpjDZqFBA4-RpN863c@wt#Yni#acZzS)jjK158PM>#O^LAy_yz5kaHM zBu(rFhhw5BKRQ0c9S|SbIf_G5FoGy*G)!wDiGZq9l4|~C$WQ9U4$xoybMf(Aq?CFk zqg@8yj)Ld&=hwuJR8rjNcHS_Jph;XBOVoUoVG=^Ee-6G$0v8N@Pzdg6ef_cngR|)e z;4)a(ao+I^iTY8)+Nj5$4_yH&=zF%xt0y7p@x4E@v!!u9`CBopi*Wk>n1@vSC@3s^ zSkk3Ndils@WMrqh(wQCa2VKp(L|GM;s7y_hl9(+MP<+t9n%b%i1t2h?KdKksY52jc zW9?5{QE;};ZlUl}gra#UtPp%rdhWi&BC{U9e}g0(yseD6=(vkHs8Gd~uPKWAtrrgS zuHC&(+85R~yhmyur^Nn$PhA0auE9lHfIm(r%j{d~1ub%RB2S5ZtrTg~k_7y(+~Nw(B&3EL zp4E61Tc`R-e4YUP-&ViP7G;jZPzaA&1?CcGM}a$2RDLt5>sV0${c8M*gk95Ugy_P-TsGK1ZXj@K2lLx!2UXlTHkTaT+!}FFt!7< zpQM~n@t+ORDe2#vi*XNwAg?997msk~ObVa!_n5O9H<;9iEpYg%i?iNq;00xS1({F` zfR#^=fH6n8cNSznS7?t1VeE&+j3pyi(umzQi6>;l+Qts79pw#HL2=h#AjG z_D9^1dI=OQu{JdRSv=p=KeBlT>QH+9K7K{MXNfu5)a$blA;;%PvYygAP;l$rPaJQH z1rP5St1QU50Acz2CmfSwDU5rH|dOu?uD0)Ob>>$E(X%W225f9 zq8}z4A@p}hySVrlO&mf4VL26|mc8;b?i-Z~HJLIKGYVQ0 zMiVa*YU=K&`{}5iOh?_~^Rn~TOU<4F-IBJ=M9aGfb%9P&rfS2Ycd~qawHF0mmi*<&R49kAp~N;$Ny|WFKwGIi2m~$XSN@_ zETCnRXAB}L+-jM?ITW9eU|k@tSgwW1*>wB_aBd~hW?=IL;_=*2JvQQk^GG1>3$ zE-wm8TmE$4&dK_|m4U?%NX!j3=&l>qbCC1V$P6kz5k2jx$2f7L*z;+!UAe)PS~N7V zPD5nAtcc0~^JkPD`_%CVEDa(pKv!WC?QS^uMq;bqm6a@F*LlpNeAXjctiN*V-Rw6j zGr*vFl9Q7|lV*+^?c-!+Wwqi;YA3Mk-v5IRCk5^Fd5#kH>AVJSX76M`*i$K+%%a1c|>9&HW__*@?RN#t8CS!~9sJn&2H zeQocp)**f5y?+t)>3v#8#)R>DwdIuEYDdW2t<4s6$e{?Z#g$Z4EYnO_FVf?N6vTO0 zSZ*guL7#^@Yjt$ABN-tPUIwQ#?+eT%xyd%%>xtZhmgJ-c6-!E^5qGBE#~YkrQqRl% z5$#>qGri}is7Je=_!SG$<$r2xi8V_NIsukBsT=1C?`V4itAacT$fJ{LMO|GTc_%>p zbXkNlo~|Z8zE#|Lm*DbfsT30TBMqk4kojcB;S>@wkb#1N;*pu;_-}6PRKN><2AQfE zSwMT9U`c-jK>d8Z?=FiQF5`}FJ35|5QcFaxtgNtW`fSSeEOFK}7fn3XMzQH~W`EYq zN9U&xQoTXiT|U1*GR$)Hd}uN5#@0`L-}1=ao3ry9#{2E zvJ^!VTF$dyf(MJ9M`G5|Rt(ZCQR$TfD@Jxz)z#mm-%pyJy3Q92u|y4+ZVf@EjySt? zK79D+t6EPOT`g~D2r;nq+EFyrsiI5go~GCU9Db8I?@bMoz?^z@S*42l z`jm?RLrTDqGyO+h-NFo=Q&QwL%EaCiZJeI9B+yRMx%=V=d9Zk#Dx41|rT!{+_-)VK zKgj(IWW^D3mu|2WY9}pe>q&d&VQ$U@zktH0gzrH02xTi-7Ww-l_yTmyv14JEmSYV% zolRE(TcL*k%#1UE$A70YD(!1}UAkbsen)%OIIUzpm_D*aFTMeU(C=?x7lsHv1JI2r zB{PfE3}Ej$H$RW_KCFt;8~orK@Taiwq+J5mZee57@>4>i1_1=%bpgW$=3~HQljFSb zkH)eUmCEgltg0tH(7A}e$UZn9<+V-^FxmzW8pX&yrCs05FSTtYtj;3xdrRf|wbl?*usvnzFhi8R-^QskbFNR4{DvQKWe! zotnqPoHkQE2H!fH(nsp|ENAd=hQ@?PNHOEeAU-jg-Sfj10hWIW9Q6!C?R#V9w8HkH z28h+!WtS61c3E>Zs&v>tmd8x`_1tFTq-gGl80YhmD-L>sVrg0Dy_&w^naTXepRGvo z_GN-^i;oFbB)+RvHlVyt>_yuqO zMdJK|LUAI!%~2L6VxQS~kU1LXaq;s09U$bSFqf=uZ>(TP>F2de_X|s%D)-|mI~I2K+g7z`iC%vyWeEG04Vbq>Diu4J z+TMqi$?by;)?WJ6w!Q;d1EJZJisE!cuhKxa_qhBwJI<&KL302r1t3dh!jo@72W6e zCFxq)p28a`a}X61Or_1p_}aiOZyaOwX}5ttXGL#RRpTzkQtKrQ+14jtS=gVs$e&ZY zHBsa2ryHDI8tL}T!v_$qYnLDBOe(YceFaC$9w~VT)|GI`1 zk(Tc6Zjf$}?(XiElCN}8Uo3>q1 zYEW`~Hg}eBX3wu7E>#l3t$!;vx2ypDW{knp7qvx;^_E<#H<_M^rlA~MZuyS(tnK;X zN?h~MlOr*jWb*aQCAlqqMBC2e;sea(^XnVpL9pMFft;rHD=GrXmrVo+N`u){h}J^_ zsC{0%W<*2A+Ue{Pv4njSuYV_wT=sU%hzf`%CZ3lk4z5?dBM=TM&(~g-{CYZ`8(!6m zoL*PYo>xVP8e8fW2PNkA;*KcyUfw38v#Db7c3?s+n9B!Am#}dC&A*ScMFJr}|7S&_ zFg;v=KLU7omKaRHgN_ywWT)#366Y%a4p*+f&6(B$jOUrN9;okn{2movVWAQ68|mu7 z4Xk+2wP;i7r_e)`j{xc<*LH5=MoH2jPQ1RJ^|za8{cXsK^;o)I>GqVEi`2&I{4M%JwsIZ>t;CpDE3f(nqal4b8}eI51Y(BgF+|}xrTk5d?p*s5>g`)RD7V+x@_7xs%XjPRn;e6hPhh@_R`ZqmUfa3+ z%N*Ja>cDyVa!?%!Y}3|u`%llzb=7JH#hQuk0{xLkp$27biM436)N_9KE~K`0o1e#C zdp>%`f=3h3kqkw1Tj}7oB92LlBQ`BVx_0J3mP7TyFQW!ZgT&y`dF=k-b+WDD_fRa-w50l@tds#G%~bQ^ zGpSaKlS0J1LH$UebNYDBxj6hEp8%M9qh!ThkI&$Iy=FlL$OX27F0VYP2(1XL(u$24 zfcrY=*j>7p*Q)&q!0fgHpb00VBQ-=H>3wfX4f?P;?!Wj#|2I{98!Zl^)*;Qq?j!JX zZXMe4i-39mVuI|Xgyf){ubj`pMl%XE5ewO{v801ig{9%1q`M~zwH9(uQW5e-yc}!) zHHQDX!%{Mg%>3h``2PiLI`Ay$6$m@ohRDHRltPBB|?)0}l>NFAq11r)1lBH^e8*!1{>u$gi zi}*-J!1DWBQ*7#GoJ~$R5PZo?ksx(qLu%S0%0#>1h8-FKO!I(|cK(bNat#g8ovu_` zjA!(npM$h}gIzjuZ;T5o@lw`Kw5c1o0bDri^?B{pC!OC5lYxOjk{tER-B_}$mi35~vi`@4e1)D!`bZZazsCsAw_)i0W@{(~OGEj*pKQe^mSk zvoIJAPlXNBsUpuY;C78BZ}=Q+Suj9?s`a-mKVJ+F>fy!1MtIn3in~7P0whl!=hjwM zX0iB~`AM^NHyHFqObB&A#mkGAmbQ^lhl%{BL7=mI22X*E-RVLtG%z0-oz$_&uQYnO zvN%uyj_Ul@(Sy-^h2DMEOVO*;#0n~Y{q!=!Rr7UK$F~bo->uq|+Rdy{rn-?Ks3P45 z$EU}~Yo4S)!iCefr;eG5(A->MWNjUfNM1t&>T58=-9I?_qGg4%tFf=-&Iv;$!7?vc z=!Z!r@WvtG_lgbTbPNqj5C?eEJ~dfJBsx3e^-|&zMBPx!XBoK$WxnuBYSrbl3-bX z`pt;I+PI`)jOqC6V&yX~Ktu|T1Sj6qScWx(=2CxipvmNpQz*U)MF29(tQSeB`c4^XTYT?jg)p+$=mMa!TL#6nHyIF z{pN33XK{=bbq{Oytmwk12*gl+c-Z?XW-E475q8pjDL2h%*3qNGL!mi<)PR`5&*bdo zz^gjbfRIh+R}s0AbDDp$*UBz>4#yG)2^UAwr8<#30pr$5ZFXHWg3ja z6^q6A2UwjP>=;X{av_D8LQH8tOdt@~={8>JQS5sxoEVnTj^cbAxSsX!IwZGE0LnE} z6r4TsD{9`N2N&9MvXIo<>3PN7TOog+!ubRMreg49d*d`oensF{z~}Wq=(ytT$g{h| z`?w`}+jhN#m*u!Vez(qGT6$_5+|)mj^h&Sf3$#M!z0&(dWRbJKf14O)EAT`8oA zR@pQrHZ`hu4oNRvWJ#4GD;ES!0>4GMBDRdCyd++sbnT`h2?n)2-3Ea8sx?(3C&z7ca0)!7;53+W%2(TRy8XJg2R3{sm# zZ0X)w+YeSZ++NH^y9kU1HCMBH_83Ry4adx+aN5ZqO!Q%cz&xRL`VFyXEsp&4dfD0Z zdvbZ>YBcWVtkI?-(mjhXkU`BIS#dVYzCPx}|A)&A3rjYkB%V2(XKG9IwgG@Z-Hsc$x7_t=g)S@yZuBsW`2TDO)z ze};6~Nb6Zgj&z}S@)aqWm6-}ui7>~HJhh;I z@IA5!`^X6*2#K6(P=8AHQtqEFG$#7nR=W;ES;+OS`TJ1;26 znCONd;yoIYZ8;6x|IGNHjfz3xOw7gcFHBQ%=g$);f-;H_+GOEMOri8&xDU#);^Q;E ztg0gP%<`dDpdyMQLZp(%Th({qhzyRmClvMaRS5kDd^h1-(z%&LS3YUBdDVB=F6|8H zA3Uq`#_Ikt>dJ}6XwZv^bWvXTgrnfV$nb$)VgY&F6Dw%hbGuKO%;NgMaMX2Rk~2p2 zb>kydDN5`B3omD6T%3}snpzDs=Yrd0hIn{xZY%!PmGl1Tsfd>sG%)2g8sZEgBqW4d z@Wlqjr&1~_o6l4m()NtZOS#|Ts+>9}*PnEPcjMgH9HVVFGdu1n4_^trQQ#Q#j#UT* z{j;qi(V>|V#Hah?6S3LnCV~+}? zOrYddpIsD8a@&wHI40e>^{JOvIOuH3)^70{%tw_JE#Hbvn!eLBCEu5CBTNqQpo|E% z=~j0OG{IS5{cHW2STuJftqT8Z@6yJ7E$QVCkhp{?zznq_tq95`i8XAdPh}@L^dBHa zgt-y4-@Uh&XfzOZoj*zHj0ng7R`M&%gZFH#a>i%sULVxvR&c z2qHPm06z-GGeQg%i%DO|%W}GIvDIUpY2I}=_9cy;dB4u~#sfNYcF@!qK@0ju^Zw;IW=?t_DhF7TgkCufl zLzo6Q*NaZ-@TMv$d`dbI)fQ!_SjQGJR@&wQ@pFNBQBLGZ0Tp6x!90Fs$g1MpWOo0B z87;QnLEl%OB%3a20Pqp1RvfOJfj1FkQ||X5;u>85xqm*1+&?gY z64@s-D4m$ttC${3STUj$3Bf*n%Xk(b(T>*kiq0slSy_`jbpsMnoMdDO-G-1$9_JM?Y&0kXV6p z2eS1$R8uQoF7?b|Om(eURXB3GYd1pYYJDyv-Ya$Ou-hI!OPeRsaYIh)k}Lh(S`cHg zL!V;na7>}&MY=0Br~fV|yVf>?sY2om3elY`E4}_(jP&wX*Gfml(UuR1)Lmr-1(05M zcEnkM&F}G>_GZbjO?mNaST`IcBc>Sk4^O94`!2r0!YnW@M`6W*7^1hhip$G?!!H3u zIt~pAyyV42oB9;%W6EH4z*i7Hx!VgC=TC1CI&DK$Q8!je!Keb>vWO;W%JC;Xer3Z1?d)M?JN^B zD@u76<{d+bE)XuPt~7n1UmBMXf3zC`&qsJHZ2eh0)s%~2cQw!SBs1g9cW%4()@v8+ z942&!99l*98gu6qd2OXXki~Vrx;hkq>t=VLu}!kYL&)gU(ozbks3`rBmi7T)#sd>r zbm9Weqr)Q>KEB|oY8GIqIGCgmcL|})wtUAgPG_%5MKC%}-SSzY?h zRQCi1;wIEA6<1?qW6(`)dg9Z9>B%r7f5GMulJ)c~rZU3Aif|xsN0wISz+O3DH9EdPxScMRjNr%1 zg(iX#iU*`Cb>2g2z)7E^^(YPR^GT(dR;V8e1Nf)igcy>89ouLl31Kqht$)I95emOo<% z7nYXd0g&UgS_F(1{E3|%m+xOLPcN?pkqBG({r8lnI5oXxzkbb536YVJ{m9CKCc*n; z^{Df!clS=d6`FG^;D6~IkfEp^f}Ea}twhtJmF-oY=j-M(<@QAHwi1R3^!vZ;Hh)iB zt=W7b#VrK=RIm)7jQi2!OS|QfdUSQZNG^qYIvH!wS1g1>L&$F5TSE^cy5!cqQQyS}w!&nfarK9OaJkfsLYKuRvLQ}5duk*C#Gb`Sy3 z1`Joj+--oZL;ke3i3*yzg~iu{OA6!~HGmp0`2D`h4xZPo&Z>HQG?gtf7a&kY|B)JA zL{X{)+IP-(h8y$^m_CsxM z3pbk9le3SOW2U z@vh`5xmD%1sbHWQ0?d=Vs3XSFk?oL)`XsVJ3{e<){nz$(c^m)t14VoVs-nVBIrrR!3P2dPcT7@tg9I@pN1yCa1{h0SiV=3wEnR}3}h9>rr97Z*>a zdIES1H3MCcmGJofCkF>zfI!N14c_T4qR$xks2drT$mh2JV|y;2 z$GUcb>6Y+iAN+x?KYxC-0~&~2l#LO$#8eGpE1Zu94(3A91o~5xK0WG{!k#NTkWEL3 z1VW0i_U8I7D&TxZFB)i_X{HIK&9p~OvM2Q0I<{Fx#6V#ci~dF}mCr|PEIXQqZPCA9 z*r8D+NS_~EC^L`gQiI>aUkYC6Khlg#=)|P28JoJG&sVr^1U#LFna#us+h;jaM*%Ik zZkrp%vyYD?ZHJbt>t~n+IN;;ihQijK4Z@THHYKK)6{gtLNp?5BK^K-e&Wwx6AZ2ZKa|nnNUA?V{W7 zU_1mt^(sp-9M^YuCV+4ONVn7xNz?}RIP+?zDu5IT^jVQ>bbfA*c8e!8AV7eCMF-dq z{Ee@I0AM}AA3}YSAhooy=>cR!F0yZ-J(gd#F{U+y1qEg>A)?Gp?WAeQv_QCp0Gi+ksuP~?55vDpi>a9;0OA5f z*E{+d2oiT9qA`Z@A3xH2>nm)il}Me{Fh*d3e6a8*+>(TJ^=)gNg5Tqet!{_BqGa@=DBC8Ih z*5zLJ=SCSnzHV%OxxTr}0X8`R{WkcECPlC%Q9L+hIwO5pRR?@muBD#2fBUwKIZ5z>~jQUPQfL zjpb>X7 z^N^yKwQ{Gzo7WHxJw2r(hWPiD-loKQK5}#Vi?KGQOH3#=N^haoj`)KQGq#wQ7oPdv zAu|`VzsVV)D6jxqT4pO;ESFm;c5HNTh1e^W|L-K(w}+jS^JoWO0;1>ryTG?+x-WU) z6cJV6U*|jcc@R2ps2({kmSSf1Uh+=`@`9Q{oSHZZ{d)Rl#eEt*cFf;3h7 z;4)n;Neu`vjlQhRH8WkCR{}6iW_51IJ*trj0KEel;M4WkRJH>!vk&^em=n4KIiJ;i zEP0SOU{mul_&Lr75Mu<};y7KLKDYTeLLe@>)iHQvf?5K;nNSFe(uPPLJV@`<%8~B^ zy>QqS)_CdXA7Rtqbf28Q?88fRd+Je&7zn(Zs?IoO7%9MQ8hHx>K4O#~Jc7wk#PV21 zsZg6S@Avm%9v>g;CP|-fO(8%hV&_+4{zJd55sUZeZ)!P~xk*4eU4zN1iC*V}ab6d< zybge>uOpBu>l+-fd#B~o(e!`PA>i?C+vn+`L_Bz;3wUWalv|C#d%7rrT!m2S#U>M= z6+^jQ9mdDTY5h+5rVkMM`};xCMRl=mE-vUMTU)^CpM?bm3(8Nf2l{00ctEwh{JvYd zDj^K?RZmty&PbX+zxw%l6rr%F$mmC?uRyA7N#7`bZuO5B4_|x4Xt`+mN>Tfa5vb8H@cBqF3_Z6n73UNG0$U>-PHn`L+s99w zByD1EFaBiBHY|Y}A;z5N-5lQy3DQK=Hy+l-UC29X2yNcWXL^QE52u{O=&hoJ!z^M5a6NU62Tr~U&bPy+vTQ1al4liZ z_mEB#bR~%Plo9z%u3K{ZEuAW)>5G^#FrH1KzwV?8em~Kr4a0n_c5I;*9Z{>KNUEj~ zS?ey(>#uO(DfocxU0=Onu_g!EsXFaXRE0JErbGQVtK%Tmppf*|cW8YGbrS{lxxxe( zh8q(uR6#<-cqTG=j9=+8TuVL3DXg@_*d5hm1TV|I#iW#oFD8$162mr!sA=LH%2JC- zgZaXPb_sIe=~sdK^&X%<;H%JW`xTtR4hRI1;l5Ds+NqK^k2tyDKwETyFE$Ey<76p8 zVlcuI5uP8@(mtXXUFMMlrll!CWWs6?(3KA19Da7zVzanvMnyr@GpBto&w8UfKlgbP zYi&dq(`T1fSP|AAhXVDN7p*PR5Rn*9oI1zPcyflw2E6})h_+Oh41%*Adh->V!%1}a z?FznG_Glt3v9=uKTmtAdLW7>bOcCTNj=$xx*J$NTBD!i3_1Dk)c)4CCbT6@TfLzL{} zJpZQw!ul0~Phr?W`>^f1!`=aF+Tn5_NpOW1(g*qQDeJe;{02syOzxKDyq=t%_D|(W zTtvkBiKH;jDvkm!3~8ZcR6ulruZi40T-4^fJO_4Uk$pSZNqHN^8F^acrn=yovKdlu z2)@&aaN#-nRBBnRtUgn1B$+xYy9+ct-;*uZ9Q_VT_KwQh(l8MIul%+(b76u4z{U0J z1C?+4Ibi^Df~^Q>Hvg0FEto?F3gh#*z)bkw;{=Nc^S#zr?(^I_*VZmvRz3#&T}-oO zmsOElCFJwlk*PaxHR{3485<)`y>Mvm{pBEi#(PtlCO-Tn?m4M=ACITT$!bY~s5Ro> zDFa5Uz>W-oHqRS4`Aoimr!%j20O@hqC7%uu{@Ft(FOvgPQ4wJU{t>W}wtmcqC#2k* zc9geVdb#F@<62)26bKSPCXtPF?c$^l?f9jh{Cm#(Mo?7G240;qM3(2nM01ms!P(g# zX1#^A5VX-Xv4>A$nyP0ejMhHwS;C(Bc4tTA!VI+128sj5=zxs#-#f6ix-8I}-5TRG zkWg?(BJ|l-QdL)DWdj=TRp_^w^Protu^MEW^;^TB%bPR+;Fcq}4i^Dv z@b+^6&~XOaN+fGZY)GP?5D-WfDf8$!eESA7!^Vb6F+;4;(RWukxI@izm+Lu z8?>pk;RMxcd4h`|h3h-684M<`^Ye>JVoDJnqmG2)d0YkgCHS{p=T+oCCa;kHdtuu# zxpRkBD6_H>DWh0o_*c_XM&6rk!Ka6G)WVt5p|UJs(Cs~oh0J`V)E9Zvp9 z?j=9i~)iMq2<@1UR_S=jlaE~;4oYfE6V9(6#7pm(eU)|Z6$eB1T$%anGq2Z>1#8XCtY7ZvD}qK3dC8*wx9T(gvWKBk+WH2GtkI!seevd~#+c@3cN2tVZN_ z3=6~(24agm5ui@m_VIr^QXu*eOfAOY3UGzIU~unWd|)9DpzEE~#}M+F?E}hB-*7HK z-CId?@l6-c?uAzThv~JPyu7g{YzPDQ1qglfs923NA+#il#2{s16nCH;J!!S#ACV79(D|l$vYT5+9;as=%~La9hOZ z*3;7RV+a-4z~-8;=ySLynSTBL8zZQmkzPKZ{$_8%mL>-Xrxbhb2N)^xM|~i)BVV?^LHYnc0UgCqWFy=cCU&Lvg94Fmu+a6ed4(xH?2=mFyivL1~}9xjE$*k2e#($R-=-Rf6MGF!L!h+xE1e!rJ=#Mu=AQbtaT!zD z$=TOe|CNu8t#_P0F{$jStnBpQyhbR;7s zB{f%FR1pNZEXl#IWk9Y;>jxOTY$tzJ8N64P>+Ae-mEcxk`yHUNkrPF507e6XAv;Hp*wD-*dln0qeQmB!4V5x7I(GGQ(Dk z`ntde<^KMkz{?)~D{F+Po2e;ei`z-Jg}@aK(9SPAcFgO0$-RyUyxBSVpj3;bD`-^c zP9f%cFPH;%0TVBP9mbEJKd0xn0Nduz+m8z`?vstgwqk6lnJsM>BEoh69`XxdWMjdM$n`yeQ$oMx}2O=T$)zmdNu z2P@^%Qw`z{nX+GHLKhdD15W}amV=Y2GhD(e=5hAFa%~|qX3}<%o(#~=<7>p@cKA^!3)qW z7IS)S^IKxQPhod739zvza}W0fUp+FuWFajAI17q8g-uO!vnNSH(i<1WIZlcq2{VO= zDgQ3D6us_U;CqT(1{^?}cLGJHp@#t6@Lk3cc_5yFZ*{t&xAJ{rr7fp2yCpnI|a z|3gfG*zib%K-2RlXnDowXRjZJ?8@ilkrT$Rt@WgVtAl@F1a7Nib82gy zyBG~%VX@uqh zfD4ZVh+8ZQ6=7lzk(0EFlR8Ab!a9H^c_bJ1E#{RP({_!kL!_cQiS_7Uvy>5w2-|2O z@v5Zs*5&X0jPsM0HvL(Z{JEtz@e=cVW-yz0oifG8;zwkv>%;R{clEGhw=OXxv{hsO zv_C#Od4FFLXFsGO?y)v$E>dEdq2)(^_#@< z7qZ;}co9;(5y^9S!8LH+lLaV(%R-H~VFpbYmVRQU`+K4bSU{uD`xT4N0WT^btmB7)1mcM-JQ_1ZkuP7*>HmWKEyvfrF^*d15@ zE9~B`Y+dT02=G2z2fWXUzSYTxJI*gIet)wp3knKq1OW4ki&(#OoP?ur(;CUb{Oc4` zx3`)i3B)Y|A@t80Y_>#&6QMt@sja98dS-^wxSrk)a12ZXyfXf!Ho)ADA}f_jp1MXr zlHG6hJ@vdOesC+{k<80a#eLurwPAzYYGPW?@W~-Yjoeo%Ydq^HNfZ^qfX(*xwBeP^1pd zdTPq*DUdNKw#|gZc(be1{r}#`Th0Y6WkUC!nyIS8znPhtHOH<-gHGUI0Um6Phv6rT zw;%lb)!VBy#co32r737q4`5tNOXAmtB{9AF|1awQehK=7@E_yB9fQc8@`FPTMGWjr zjUfYk^#wmx#gQ-%_JR6d;m0LyOySg}{xJuWA62%_A&fBXJcZB)UiKw-Z19|yJ6wmZ zd4pc)+%YLiOQlySR~&LC8_(X{@{1}1L$6j~fy}QfFF0*}x11_jv3?RNV|Vvi9pn$?sW`5>k9BnYh_wKb}T-TGF!&ojYc}eTCjg`9vQHH z6(4Aa<(lo7SRUtl@vV3dqeb-AegVPd6UEOan*5~w9ojDw%!!RVIMR1czyvPi}W zBtKr#CTO!xwGsBAD!Nr@w`8l>#H{t0WB46QHY7J2%mDj$dg0o$3|Y@AQ4~m9 zvO8aCL)8JPU(T!$C={J5>wo#{?MOqS9J*mckQGJp;&?4Mlf;L|h9jK>DtHDACza03 z7?01ItkXad-VJ|v@m~60_ZdY$Z3ySXmUUjG-ayqlSQ7F`T%rW)R<*4SJa6HVRFjAxL@1)Z)>}CwWFU@(0)|YDd8dQ z8)}kMGV`2t!we{)I45Vsdh&Bez<%zKrC2UoFH+iSKoK@mp@|w&l9eAb(qAA;rn!pn zlpBM^nV3)WGDlFWH=~A~es1S!Z|1X(k)fff*ObCGl4ylu zRiX78JmxA)5SI^3;9e7>#?-i~_-G>hL**|1r=LY@I)TN`Nh4&uFARk-9b@XZtI7#l z-iZNa-U;D4`tSa6A|mA^@tLVB8hT!6P0lyHf@nh2sACz>Bxi#7N^e1+B}5x!K22(9 zMj+%ll`2mgCl{fM!%Ib69AZoxkZ3{IMGw+*q|GBPz~?UsWO_Qy7ju$0KY4DA+>@|phlP3FaEjJW`^9>M`JA#byK)k^7H#r=UBTRjNlf`1 zLV7C8gI~j(!VCF_l-1Sha=V@_Nt>3Q z^**1dNsFm+Jh|?Hc=;(z74&^RIE;Ze)=xRu6N{tK$o6@?P>Z1bL674*k*AHUw2?*-X* zstS!kTZzJ&eJ0?lsl+8QohMV_-*e3IdY1ls{Ov<5bOZ z;}2zsBG#||Jn_VHke|z`bq16dW|u!YCqqoQR*%T2yYM{&^5^U$xPIQAc4rdoRP9ks z`5n5Z(MY8?tOH^`%h3{Z z5=@Gs8`Uv$F1V&(jQMyG`l2M3KumO26>JKXx2NA@5GLd2Nj<)@B1(wT8Fr3J{@wEA-asCdf{h{21 zi_J7wHp1U{!b^I#`a{dbMph%%_yN{Me~ZS{UI@xL>SBdEgKLGf?MCnH^02XIZvvMeqRMN~v(k75VaG1T$GS^!vD#vrVBoB5HEpr@Wj z0@f-Naa(Glh1%*=Y%i0bc=)68qdnBj{Awp2Tlt@c+7Dv|)~^pF*ZPftGWyV&T*^gr zP$Lz8zq{$Fd4%h!tNkICSgM1>}Chxk_? z46bxntxG1Z2NqFcd6+jXz&LH^wo-Ue8!{z#s6J^W=k+qvxYDG0TeGCbeNUFA9+8N~ zD}2<0wUeH_O2ppRnaBDLq88)Lj!94q6&FW9ffX3p-^|}=M?65F;)?}~e_x@ngRBFp3ZjAhILkS&TH)>Ia zywYt=z(AEXukm&lR=4VB)!C%cGEuFIxsACScF?^1CZG}Fn#G?rGquxUeXe-rzz%)% z)e;w`EN9&>$Iks^_ibMY<2a;PPn1X^hFII<#fxQaC3OKG9*FtbqAJi*SnwSN;@VuPRU$tU%#wd7F3W;Yv-CQ*s(W0hm(M+NZDqq1wL2m_ zu28-TNGo^APEmV7Z(dU$Ur@#1x${H5OQLDWVZ!lH!EW+Kw|LCs`hz<^0G<^vkL!!e z)EVu5=$sEUtSZDn!Y}*s=i1qZygoWrZ>ce!ne;cx>~g~KVV`BfPeH(A(F4l-^ciRt zm$0TzNI!9%M`ZO9;)s^Ni!XY`^>g)QvE@ zyZc*tzej+=&nc-WvDtlfPsH@X-OhfC<9!^VSFGVy`DY}V?6(Qrs>-L<7|-!)b=sPJ zQXJc!03^Yfv$h6So1noGT9YLuXja0%0_-bPXz?}GUTfWS4($;#u7dtVe#H=lXolBcO0chrN?9rzmVn`vqasu_4yW2f?^DA`_@Qm3sLtUCpKmyQ1W>Q zd-KAK7asI{=3Zvi()oRG2V%xe);4@B5q1RJs<%v6^&NY{k;IRCF(_9tPWZT?t1#lEABFjUXw%_>=V$Se?w@@rS&b<7uzLPqd4l4Pg~`?epV2Tv<|?# z8CNmRq$ACA5?C+?JFK}5FnYoW7Co7bD17VB{gKi)3vV-j=j6vZ$k7j4jfyWxB^0~c ziNr=Y>1_+cRs1=!ItxDU=Fbf_<<1i)ermppgH(vUq>HM!LAuP}uagK|ACn<2JI` z5agDn_cN!_)=~vG!vq8zwe26KLo_+ZztbT2OC{WJ=+WZHcrI-34=x!^AG5FsB6NKh z&Dhd>ujM(zzkfg=lxxiyb4)#=PB*Nw8WTe>BdLp{x>T2~9}xhrrs=@(?4!o30P$1K zC+l(el{6`~W{KyKunN0C_LMp5{Tp0*N~tWs#uD4Kz&-jaY*x>`8}OOEtC`ges~-Ku z>~amJ3D_sSKd}35v<<#c=YB6LLAe+j=E7P~yJ>6jX~832cyH0>4@YcRlziN<8S#$w z8rBfr=nd{&{jD&V#mSEgg)0IjD!n7HA{FahFdrXKkIzHhSrG!n|uCMniPkq%Q}1$!cl2Qq!A<)YWM-I2@# zbjAa3Z*-AqZii8OI!A8CR6?(AAPC7g?BNua6bB!W;S4TV`1!s!J-}G={zPs_fs%ob z`^BRgDkqD0VAOhA!djc{ok-W3lPU<9ahHv5SiAzW&z6F%Xwd;nW154Va-)W< zYPm5u;+FMEfPXscqKLCkqmwbz773cY0EYTXN9qFts*(vyq^fkoy>vPeU$pyfkaYHy zYiy|Hj6mI7AIexEA}w@HX}gm!jGici?l z{f73gufXMxbzeJBz6w?~lq6B^fG&^5pdNB4n&-xZxH&04v*e{8poCm81@-k`Q&`;-~$chNO0m zzWD1J6gwycT(KoQgC%)M1qu-8vUKV$@66w6owke5F0@auwb1!o_GSe}JdwnbccFWOpvNn+-}EPF^_R97L0SPT zz)YpK-c}9NF{g}ps0+LN^TeY6i-o6T_XAmQRa>(17kx%ireMq7;W6liF8&PM`7LiF z;&?BtW zj=zFOUi^fp%Aw9Y;c8-Eh^7aVrPa`DP0dZQ!p#4#{m@0((WAz9NycYpenB0GDL1&s z=!6pq=>_=0u;7#_*soQQ%Uxj9EqV!yy_H9pv~n;C%z&b{e(r7@d6^=Mm4k4UUm0apUsdmf8J!|!9__@N=xVwXn`bjw8n(xvJXQ0)iDM^EK_j#E+*t*JOUjH~8qkZ4_ zbQ7lE;_nyeA6zxPEV}h*(6`Bq&cPU~+f4*~JKomELNoPf*o3&io^4;fUH8h^tWsF!y?P|r%%}f-P43Z!SurA zN%cQ=4nBv|Z2mxH*>)G5JA)tjq)~ZrbCnz)zG7Qg>;MT5$%*n;+1f5^MI8D=Gi<#a z%=@CoVk#$b%938OH6g|Fk}_ShPu1}8(&PsFcvXj4**gD?+uNNVJv! z+D;eHwMa!Gj$JqBow-p>q&J>)T&wf6ySVAg>OvPXCqCHho$z@yg(yQPv+@^v6-4?j zx!IoHZS+jtuRbSLusCJ@*BOJ<#hpY=ZEJw`P{vYA|Oe z%w|rtaD^|$D`Zr)&yH-wBdrRPB2c;c!375}m7YBZ9g1TI_kxs?Z~jb7qe zyg@qm$0CaP&Kxh!SuJSFq!3*OA-C8E4|{&kAOl&bO1Lt4AqSVzR!rKGZmgJo@1#(F z!pIJ{1ek;-u5N~?SPxrKbD=I$t)B1_`;&p2H5JJ-)2)+Z&h$L?-GWb|mE9uaz|`Xi z+jwObZ`sSRyTtYcSWd1aF0`hBPX61CAt`CNyb{6yKnM1y7G3a64+eV8k{IH zVyQKW1+R_A8^P4U8^A;AMq0W_o3Z&92WeX<>g*iX`=z@>BFsYD*0?c@2~xsrXPp;K z=uOm{WjPwU;7s#nMN0FFP?KsQsxjEv^_-6;hyU+FD}q{8hi&;)#pK z^lv!OSM5;=^Gb$6vB3>wx^K6~^xhU2j-V|C(jYXoCF)@!@sdZ!xo&GF1XKY9Zjd>x zc8O50$H+V(JRN(mB)h`!lGwV z*tqp8#ES4oKiPV9FWUr3Rh2vFu>POXM8=khj?=?W@`h>a0YW(B=)V z4~Td^n#7eo<7H~%C{{K4^r;hSchr+m(VY3s&SPtEmG7`+ zlGDi6n2&K1Rf&C&k)78!w)WMSk8jjpN{{VytL|DhS+FAv&G9Tv9@Bw|ngq#-EYPFC7Qy8UL42l=ex~NUBPP^PEqz<8rI?vqNE7t}Im}wvVX{J1^EJ}yFy>>s zC;(@1`>5;^4APhbq1J^~`?cS&am_l+Pr(VVLd@U@Zb}jYcaL6S&_t;NK|4rX8Kf}u2=CS)?lNpv}+}$-z}G$l(K;Txbize>INOaBSm=8i8wTtz#~`6oAPE>t9n4^B~F{ zs4Z}yEj-{v=@5e9emyMEP%HBS^A-v-BNoG2JqAs)rr@Myn2rWto~$9jgAj`=d?~Rd z&B-iQdRaw6-&Y++(nK-pW-S%MsYs9Nq3;SQ z1m!Xyv?${*J`GO9u)ZcN0QHVnF}7i)K#sYcTNYf@@OXBQn2KF-MDNpj{_#JT>XC4!edamhuYe{#`lb_q8M-Se zl@(wIgno*Q-nn=MU{P*ICB@~?Hy-vg@aY`CA?k)G2;?Dvj*Hv@^~{87nPxQgz()w9 zBc@FsjNv2Ao1QC;Ko#Yq@Q!*V`rlhdaUd-FyaYxz)i;<4W3rTcHRX~@g1!McoX>fv zK2{%h7HnpFQ9l8%dS<*TzUH2jus4WJZf5 zY}4BUfcMW z&Z!8B>}*Ju zxkxW0)x5h8SS^otxT(I`!Za<4V2Mu|o)w+ooHhj0!xbC8I4>%Lo3MDj@2FaMdROo! z&|CeMhEoQ%X+~5UNSD1=!Kf+TOl(IqyNaa_|azT+aKJw5sDBUrR`i^N9JT0wyC( zW@A|=EamPCxg)lOsI3o!$Hm~QK&A19I%$Z^{-kQ9RztUvacI8zHvNS;-L#41DJP;d zb3)!@R0%F_ubhj+s+J(AbmC%Jf0tRBK_xW}5gIvnb3=J=(-&Hzwn$BR>P#GVAXy*Q zpbqJ2R$+spEj^1BqRxz6;DPYB9GOzQPMs?GBrd|(u@iP_E~bfo`TZQ1-`ckqULs}XgwOcF?qFE`@fNk7%WQN}WyeBTuXBLi zQx+WyHZIV_Kv_!!=slOBNALPucWE1umxR5#fHgSKs<~lPIXq^Qf%XyJtwMSOTr{NX zq)WzMSL?J2(yzPwkkV>%g01d)qc*5rL1>=jHu;xmJFstWWwF4EW&xTlET_T*-2;vsKPTl(0nDMxmD>r$$4Dc9tEeiQO0i4tH+n;Ylu8vw(J*UF3oZl#6@pUzIIHmL zRKLDa2@Z^I#pIF$5gUU*vzm@WqOCtf1h@X75RUsEEjUpg1R|%1%{d2&5DsfF!@{ed zQfD)D&FGIN6-2_qGR0b#?_c(;$1qV4ZO(0lbsAhx3zPo^C4sm=8(v2AC|^Yec(66Q zJ`Nf7%XL4gN8Y|K=jG*ry+2*fciU13GR)Y)JgZ2i_I|kLcta0A99`dl$jlvlZ7#u=79H{RvvEGi5_ytC_37B3c`N`H5}|7F7mlaya{|CML1;t1TH??}3<6{D}#Y-ts@N~Aw4L|$J6nKr{< zLSf;e4GD?eqee)daVQL3e2MAn>_Wjn$kioHA`hwpWPcLtBy~FmdFb09J7eVFFBNWR zlI~ws2geo?r9au}I`4&QmR)t59ACEy&iPd$>rI*^{orv)nP1DVL699vM5rwp7GlT8`ibTk_Jwgy*# zgWooUTE6>TaKObV0P0R~6eRJ*WSJ_{TAZTE7(!qjN_63i4EaRP`!0t0yB(a)q)ybH zX%k4W3^mMD38kfp5jYpPD*HY6{g<1BW~15qs&B9>vb)n$d`ia${ZzCGv>ww1vkeR| z(_=;je*(w-PPdUU6Q3ib{M6#dqH_LElkfu$C}?p;lLdWK#nC#*@;5Tpu-HHJPEn-5 z10TNc-0|W0hjuh$AMy{`*L^iKUX?~)aYcG0&QI92{l}=qwWVp+EJ?J=ssc6Cer5O| zT;uTuT{&TWZ#~+@>ia`$f_leE&ScxXv-A19*E@B$Awz#810r0ynINJ`qD7|)QhyV@ z&lSHt=|q5`3w4J&qVM!w4&FDj;`ilc9gFylHMn!A=;~4iiL|*McqhPTJd?F_(?Jb`oN+@i!;zWA9cp{Miqdv(!FZs>FV zd5Lb}lRVJM+TFwb7L>C(?)vBTcsT(`F|}D&$Bf78j?Vw6$RC8y?W}jdM-y8GQP_Jp ztqGHmzz`iE15YMOV;eq#$x!!KVdA$t(ql`j;|RevVq#u{o~HdDDB{AB&iGNWuAg{DIDDVgrudupohmYdNiW5aW zONmQV<0WNV#`GOo_@fQ7Mx$JOT(0Y;+@WnS{JQ&2tt<5VwLD7usN;+WeX6aKU9rO- zE(<O6o>~XqrikT@W#W3Jenum2 z*6^YXlYM!*eW=?iDkBlKgdSW_zotIKPOp%2ecn5!Avf@P#~@6WE*n>xbttIWDSk{f ze`-;s3%NL_d`pU=upV{Q$3;5d=BFa8rSfZSV~TTm7K>qGiE_-|uz<==eELQhkxD56 zpt=-u(N#yKl7QwCPtMT8`gCtY@!@6FF$iaT|L#jGZylSG#Hw8GA{z@>sEQJCIBsnn zJ|^fILM@#+>+DWaCjRaV8m2gy0;$8T)GOp5W?PCen!mp|l3?Lq4*$&gYOO3!H&s}P z?62}fvojsFclK(DHL*g;KsSmrF;=@9^_YA8!JQ&`IeZm7XO$45(Lp}cW^c{s8-6Ec z>{%KFw17hs_Bvsfn%P2E)U7ZshktxK+4=HjI#!!sik_5=5aT$HnrmKKN*pK=MoTOt zb)PzX5sLcvgZhWkp#1KOL>Uz|UX6gtaP+{ULAl_l6I05)Ay(TRn+$Hdy{u*;a~tnO ziLn|_LgU2e0rFJW4aOF}Y+hX+&g9&R^d?g3pNJE$W82%o1(uf`FK+ot{9!)S&gH+|y<%i1blj;25MouCD(^~rGk)%ezADSpip17D(XON= zftpCSX;2G=!csGe$Igcc9o(8xNhV|)OIRtl3ylo0b?%^vUZ~-4%8^7x(@7di%hCOU!O)V}i3nUz00kDG-8Q@^6!AmKziZ5~zN% zE_Ov9_l?$A)@PsHTR4U$8x&-E1Jagh0lYw#O+E;)ui7^E|AQvvsEvZ?T*v3$j;Vbv zvcRa&Cz*mi@Dd9fNuq1iulG8oRealV8;@Ic;63WFa`TwinyAsti#V|!=psh@s*4`H4N=+9o*vJhfNJU5OssyjP^z4=-J0c?k zA44R&x~)Zc#xQYP%r{tm6I*3 zz|g1~ZgkP@T?G{DLlX{lm0egSJ*2~}M3Bk<)HGcTnn)C7CqWiB@29_5|Hi=lpqg*r z>^>5sgSvkl!nE6HnCwok;VJivO!d(~d(E;yzNKo?6Fs6SabdnXroWtYRG?-D! z%L-|D$WS0o>y&#^4zru z1SuY7pXE-U8jR)~PWmfV!_J9E7+@RG$mO`~ESRHckoWLInu=)}j;eO1jXq~_<~UJ^ zoWyJV&X&k0py5NYi35lZlEbaT|4Sa*A$N?jWsz+3RR;9>yDvFzYGdKC%X#lGK79*! z<3ivLUea!7^w@)ga(|oES*>v&dq((8-pb`~(`e0%U2veK-6){KFVYXGIbU4IJdSa9 zwZU^Vr{JTHaUTY6kauzjF}qrRZ1-o=J23kLmTcv4fX7;1!ps2u63H7v_TRpDdnFHl z<&FI7>wOZRdst{fMMtDli!N##x9M(iKU@mvs-^&$P${PQ=HD1=HYq5_y!9T}N)WCdY; zeLY|q1auAEXjiS)>Icj(+q9_M0*g|Lr5Q~<#P7PM_o)RZfr%@|>1}JjA7;iWJ?0P( z0;Cf|LBY#ErQFX8^*M^}cwOySq%;(>J-V`!57ijsR;?$3ta*SNa<_cSTxj$GMq~4Z zjF?SKWbO3%TMSV~LsL(;G%MqBV7d4(42~Ra5>tg3bvJc`F$vjXlYgnU{eH3&E4arE zQZ&`UxLH_N#1u}kmhuHOWP431@}Xg_c4xc))23cRLGsp3rVbx0;Ca@=Ke8^fD=mxc zg$DKCn36M|vKqY;Y&29$;wZP6v6NSw@sJ{yh<6Xy=M-PL+CQq9;C<*wP|o4pN3U zk@HVT{4HxFB4Y<-!2gqwu9~@KMeOdPe60C=#GG)x4qL(@{}%8EvHmc#-#3mIY@>)~ z4q3sYL6{h-0_IjFb}|Qb8JO=Lvl6T#l=q76?ST+5(xp-~p*4y#{Zpt+znIVO)+IQn z70_`m|PtaW8EUOI6p{g(#F5BU@7>pyj{H%KMBo{>N=9s2{A>XM^2 zlT(UOW>Jg9R3~rRI)=_3B9cgTYv+7|7aeVyh^HIIZOl#Bca_APP{X^WO&YcUau`{K zIXyUW{yvurjs{gKiJZ;Ildku-xooujehWS6u77+w3L^=HJFn5puHX51J_o)xY{`^? zte2px1+u`&k*?c#dV&?pWcFPgO2cR-UF|k$G@0`WzfZDn4{Cdd~91!LXrj7LWxwm!JjWH zee>UyrOhLVtis(~vRSnx`dcO~>4Be7$Y4>=`|o^p0A()W=^&IyB6KYchyUQwpuf|E zsz9&uEZKy-)ZN;NuGdqvWlJb0{h(_%nFlM!c~9J=)G`Z^vDMuJf(^xufCM zj(e^JWg`R7H1_Ckk&Itbd#|-U;`yglw1)WiWZQ!>?uDun($w_P%%3vA;^qp}2iP&2 zLvzRV0>K2mZd$_JIE1Qf{egO0+JaC z>4Z~(J9mVDz`vb(o?bTy^uP}1@=5`QUm+^WD`e~qE z>Epi#GRw>hpSlnBA%iE#vOcITikWyWpv85M2WpceZN2sSgDFEJ{u8r(#D?aO+4suses=l^fpP0L!8{yP7I2Q&sP2B6PJ2900|u!|t?B+f zXr*PINQs-bqlmek_2m80b3bCv)G81y^naFd9p$=O-6eDCL>k*O&zi)4SFXHw%*V)M z$N0Bo@$U)rSYt4tnT@pmlNIJvHxRzGNR2IPrh$=tE(E(8jLzdJr*c8-r1Z#Sp;iIA z1rTz-TU^Y<>kNAwV6Qf2#+1Mm2dA9ZS_?;3#B8JhdNciAmezCc;!`2V7ZO!OpqBPm zGb=0(B$Cav9CPfIB5C$}!_uXK5sY4ej!$1@knUcB=PB-M<`wK1h|ZdBG2gnB5fJI6ahr7Gan`5%Vz|6Zi; z(x4^O1R0DvJ649f8v@3dY&;UyfI(?J*y}+|$?5(ToCNk{f`%bVsRp(k@j^hGl;KcP z5*d(tC-y^hKNQMAvMQR(Tbb3O1A;tQyl$FGkX{{C5Qrd$N-gRJWGxFu!y?2RJ(ZHO zlNv&)s1gvBw7U(Yh29;5`r#(^(;-E~qddirlG+7t=zBhVY~Qqe^5PHu=J>) zZ72SrZ(1iuPV^%3v}E;H;3O`0t&fUGr)5gQ zhQHFlVG=-5C`jylQ6e&6MxMPe|fN0!b ztBNC6*wM(_t}f;IWungS7&Pi z5Rv0(?s8$9f~=P%t900Vn|&7|bMMm`WYmzZbE;tA!YsM{k?F9D9g(EEtM$*Pwb8WN zD)3oe?yyom4e5@jx?h7+3_d<|+c*tbb%k^+I4M3p>`LTvwdy8g1tBaO50e0s#wFMYI!$@Og%p}1ANmpD=3 zs7qWMk{^aE1AYQEq--i=N)6jEgm!o08k@4S#F;Cd9Tq%ROm6QNLB|F!V!s4J0mh1@ z^B~*3qTf>FvCdIJiVMAe9e%2>A2-KyerJkFNZ;-gqr_2iOlcMUk#n(qSjPFwid1v5 zQkq|$mB0OrMWUQ@dt@?5MPM>Qnzy`;euOj;KQaAz*?{C#XA0xNA^9iRr?C(BmtpFo zR^{w&F3iRbx#QN0l{)&^ciD?(Wds&=7o%fa%TcBV{o-|2S)5tzQ`5&F$tMO;7$)nI(~jPWKJqp zwexaIFcJ2v0HLjdlQcL7zgkrLSn#3>8Jq)@CNFNUuLs4F6$)fpxL67D@+e24e%ea4 zf5_?#1=JgJxc;?KT6KG0?SEJ_bhRB3Q-A-8R{~Vw@p37`#gx?<_}vHyAuto#p!|V8 zpK`$&;z-fXREh^E%Eg1eWy9lK@SPLb`gmU4-VTtakjZEN=4(E-tsJF|bStS@ zOvg%EiI|)ruRGVH)LEm$za&wg5A5=C&&Uyz=)*boC*NjGKqi%vY~MNhaqLyhx&Lq? z=$V2O<_-;+?uv-$cTD*vFFOg@cxgeuq8+4H`#cqy_x?bpC8<=_sz*Ux`JuIAaCRr( z!Hy%OJj{1dspCdJ@)NlG5jKtmxE#uTPm?py8`WZ*1wtLmJnK`NowoMSU-x)HpP^_X z|Mh%)b}4;5pnaSBdbktcU4_w3xD_^J_Jwvx7KB(o$>pZWizNI@<9SQQy2ks-`!d4| zTxHVezM^6(!My$xQ@y*{EBOO_8+xd zM^!^d_XnOWw?A%p(xuO@vG21uBao0~%W<8y`-#_Gp)SEfQW;}^n7Yo19ZonswJI-h z4OWkjpk?=?82<7`PX;N2l}Q+S^Qv~(B3M|ZQrUOE(rJPY9t^*v&D z$Baur0*Yz)?`TT*IoQ6s?MO&2D&H|irmyIa#2NN4){_}A>>j#oir_MlE`%)ITp@8K zCksC*?4QOMgF(}PozY{4#Kt^IYkvO#1bv7k*^(_h6Zfh8%5tc+tIe1rR8v7xN%+j+ zRB1zr9~%K7n)x(Po9$s#@R=Ua$3y3{uX+?X#8Z78ms5y4Ypc(8ZVI0WVr)Fh1N`MS zgipYxJhnJqTXxRqtLLr3Mf1iYIe%R)5FxM11lQLlgA(QX3yk%jW39hF)tU z?qogT3=HaJp7te5t1{xP2E1QY`(?@#?-b^@c&c#PJv&k7$Hjrdzsuu?9`yLzrcGyF zW`sk_^;cw&b|)ms?6etoFD!+Dl)iR{hsuUu~9Dhf+bGtAoU_j)Kb|EeUF;4xS^moXS!}&ntmehek-<6P8sfU=F-x?zq3I zN=R{0%F!=k)biL~W>Q=@^@~)%AE0CJN|0d-hgt5yPCiG)k^gI|{6i*pjG-!6OLHZh zy!a&KysG`{9BX9qML)PlnO>1XN(ZeYNcse6Ew!+(1=0wE9Sm}jRp9^qSiW`EMS45N zk%P9i$Hh-(3Is5CBUEePyWXK?n%UC!zm#I$Jhg&Qg3bFcUGHW39@sy#-*7!IGlGgj z_=B!j*L??qmhB!oZFjGAsrrivOT~@coE2*j6A>0m_`LBLI_sbx39STEjrKth=a*B? z32~R*fOi9$3>PTFV%>g#?jQQ8BCt@^;zxCc*zLIalplSR?Z+OsslxV|ROGlR^2UyV zjv+X_L59_xzVo{jHye)aPG3f5EPaBKbv?fw&CBlR7LAZk&)qZ5yTBgH>huT<336PS zG%iD;V)4ue%(kLly!H?z7#;miyXrr!H?>>i^!2j-7D|;!%<2w_$$((cbjnDe;+%me zB_lLVX>hmb)x(>-x{VB}#8fbE8RPa~nxLV}&R0JVb??Na6MyAx-RTMwp{_q3%f+7>ScKc&!r zfXJ)N4;hR?t&}AEtYP1c*>z-C^_F=bDFM(-E|1z;N_od_??g|FP=XRnST~~o9mubk z$^C1keq3$)1-s%E)?)P;Tc1<_dL(4^%*KM$WQ)Fu{p=)Qpc>`PgGq%YU4&aIwG@lr z@6W^)TyFf47iDc`p@gVl+@O46CSoo5#j*1<<6Vq-NEeQ01LCbRQU__aU^cV-2q+%}#WV4^fa&15_oQy$CTGB}28Sha&vF zS3Xwi^oTs;9E<;`5>;Aae-kmPsMcp@6g~pX%7Eir#P0&mYEK?7;McU<;T<`fL#}>X~ITmig}n|Y-?iELW8PCfRj*RSFF=IBn zNi@e$L}5fwDlLRWfe037XL5?VA!V*G(b3~6!po}>EnHer;DnwK)~u{0D(jaXdyLdY zAu}FsU*=n%*DH*Mrgtt1btZ<+W@i=TjpiEV=VpA5GU-YXLQv>RMbv-cu8qZ~WTXyN zG!66ojENy(!W6Lf;5D$&mdhiEx@@c=H)Sk48l148*bPLcq7&voRcul&)nFMwhE1XI ztQaFuc69PEWu4dsoMcO!E6TWN3tg9l#3eZ|Xbh&)y3H@!oo;Z#udtq2Rr_@+XcEl( z;V0gQnl3Xq$m|Np=uPOEI3a)bBU-pP=TzmvGd;F0II-m`ZKf-NlcYd?9|YA~|GONL z>Y5Yw>$uX!v&Mt-hop#MAeh^d73{SlwXXU`GNR^!9U8^qt}d{GQH@KKhQkl&=<`K5 zeMn%YWAvHL(e3xBFW?p$F9aNo;F71|4GqqyWn#+r*_Lu!4H6?^ilHXkk#=tWU@VCI zY}u;Ir#ND~rWXDMKl9;O6qLifM$>^kK_xi-Ou@u@`5nJP>{E!b#z6xlCfvvnc6TOl z1XUVF5&1{{cDq_cDHim<C?+a z*So4sLhh$XRM}Csqax7JnZe2I#-#zs6XUNQT)!kv0?lYO2etE#{F8X0L8E@OvOWuX zJ3s)pCokrNn?6JW3sc)oF(h(XnV6fq#v~-Yr!5#7%NP?@)mY-z97W}kzXmJj1Tky6 zLLC{X3__WJ8Pg;f3gLUCCE#BORA5yr-spp@4y9nDCGoqx(w{ge_62F+qR91>R7)Fy zP*)ILix(Al%|r5wb%fAJE_tQ2>7vJ&oz7=jY?pgv+@I>CMaNF$dfLi@*`md8 z#`=vIDd~L)0HN@{|72~To18hT4avyI751|T{afv~u?VBL8UDlG(1kqF1!nWIs)6bT zW~eAKZ&hgL89_!yzf^T3?-5#yoOCj&j`Nhhne}5kzSJJi!LLp~RM4;6i*NOlm-1_s zya|R67Kx{hd>uq^>r>K{^6in_wz!H<3d0)M5KV$77nsFMn-h2_T3I1xwQP!U5gL9)f(!Icwut!jnMc$x{w zQ=Nxj)Xlb%tZ8JFLP!3sZblU=Ku15chQ&^vfK~NFGXRe0G1zJU@&;SfuFLd}CvlY0 zdwNgmfE2Z?ql9^e!+ zrYnl2c|qd@BnpyZXk$p3Z|&g{}vjC4BnAQa&*El+_&h|Hm19G zH4T8SyhFru-y@p(94vRe9XG_PerM!!y3V?A?^J7*25!k*5?qugX207%TDcGM&*Gl< zH?ns}s|E{8xF^juKkf8_H^Pj6;b)CXZ6#&y{moZ-p*fm9D6ez1FXfJ7(e`DG{oJ$K zs=yT==fQ|QELW#C7QQ0GAtnCDCN?}G1&ZAA)ac?MZKt&k(BAk5QIVy2n7 z<>=)4;9nIM1wS18(D(t*=8sCY@Xtye1E_gkQ#*R>F^%?Ki-sSn0;=;eg};^JJ1i6R z&4X5QH2zWv%C1J#JKp*E{yzV|RFNfY!mR;FExbGA3L61S^;>IHUhota8oAY#gh|&M zPtwE}NM?P6&tRm*^~?a9Qz` z3i%)ywqnrH5Z}+IhrIzww9LdLR4SI6cr*!04+ev@F~9HfB~;cObYX`N;4oCN)5=gf zQXi1(0yAj2LQ;#Ma6WxKJ%QCGTE=FA0RyIgBBFaxivE_iuh5=h=#6f()0PtQ6`QOE1SUk{DN!7&oM1z?IhOR zBQVf8`0Jy)(A#>&!GfZpt>`!EN_s7V#+ey~U^Sw59m{k;xD{H7Sa}+t=)KMU4t+)^ zS1T^{$HuGV;mIS4Dw+T_iKC!~_~E;B^d%8`Vk=2dekM}Yh@WmCKwMdffcXX~ji-w=YOzbS+{mmG|9whIC!_hhNxe6te59MhHz%!|t@I1!=UxijJrvI~?GShs05y;v`hBdp|P|L(M=I&W(yHA9XYhR1z%t z=iw6(u~mh9EUYZ3CtzN#5^U+v*s)FExg2JDxJ!tZh=pMZwPrr=Nu{SOR2FA?qahyh|+9Q|F7T3gvT5GK&a&OQUYoh+!ran9|4aM0w|{BAYUaE295QD4;G} z+a6WG`B*F8#E^@gtOpPUe|%XTdAY49QC>IXtt`XdI`p;+3q{@R%2fwSM=X%n@*9f; zwiF8}lfkg0!8A52W5`MwqIc&KOGYrE@p6!UA52EOGxFZBhc?7Z55!ZJ$Tu1e_xRzr?+@QG<9IxH+r89a+TGm(t1eDP%Xh2<((qU!7<>$8B z1id5U1E6DKD^b+I1o}}12Xz2!K~WKt>Nk;|*34<1z`JO#6?3hD(<*5W{<5iGP)~Bm z(Zy54%n#2~Kj@4iBENk^F=H0+sNKD>rGi>->zNh4dlbV=&X2z`Qp-WS&*%N!6|5tf z&cOU?PZZr|z5t}AHJD%_wYszAv=F3a%H*FIrh*`a{x~I*({F-LT3P`-J8}I zZtnD1WbN-iia`$VyOHjA4Mi>PCwiH#V!m}~E;-yL)s~JkUd8+r5p-hpcfr(N|u93AE{De_t|{(k#v0~pvP#@0(_&>c#*?P^6B2iV|=aE$T) ziyH&YqWvofPn~f7iJRBa5(GR)>~Du9$OOVQ%_NYaK|K{vCJY1h&#w;dgIEvhS4sQ0 zKz$>8hyCd9v=gz?2PxDFHDT)x){4At`8(%U#Fexvle{wwQqus6RtYC( z&=%m>-kdmiDg-Ha7Ug@B)T;8P>+$1wi^HU#mF{O+RU(RCPD)o=qMs;8MhQ(VCCiREza!7=sVOvs}@t&N^^pcOWG%R-r&RU z?=xsI`T3p*<|tyf6j8?2uc<$VLZEk5u0+&lX1(~pz8+Tmnlh9zcW-MBK1eD!W;_no z4*`R*iLZbkg^H(5e827{jNyAbVWXyNsf7XUjYh{_=bGDnBO@?bo_CTL+ugc=s(+MB z&!^YBVg*DXjV?3;Yt-(&?aqW8!Ww~LLit0eTf_R^&=wXu_df?!q%Cd zmKD8(zLgV<&Z12Mr$rZwXw5yStD8e_%a$ejnQ1|v0wihsyY2knYb?=UP+<&JcG_@m zEl3#XY~xj7Hzu#48J-kjh?TBvXeo(ZR{3?}SK-47_A=NSPux7!1Vq_P)dE5N3Jx!C zWNn1)Uf~uw&x;xU*tx~6#B%noEq%todepBMy>$)CO-v=IS;zX~a)t85W z$m%KWUjj||B0mg>x%h%_(dA;1mL#GLiLnE;(S$w6q3?|srzVszt1|ckyi3kInxS_4 zKzNL{=u)CcyKiIQkjCo5hb3@tSW<;ffPfY{OEB1ArbSdD&3XkRrg_Sd1=VVgs)wm4 z;ym(|y!p~>A@_?o4IdHt`fhaN#xu_&3dXtlsC%!)mS+7+dDfx2?#1QaBC+01vy-gu zEKCrP=N*Tw!Z#|k{DL1Sn-FyOynu&lj;kY($-b^0|Ly+{ZC7004sZdO+k-*78csY- zxRdgy)(6Mbd*}~zH;IBg?7_WmxPEJ>34wq~GRdbTRzd2K{k2>ix~7 zA;UkOx#NF~JieD4fBsM~Ot#OX@I4Diq@3;Y#~RxscW(=^vvz`}F0VgiNApT%M6VL@ z5M8wv3ACXXsX-RJ?u8~CK$GRkh2(R1h7IX&8#o$`r(j}Y@+;1Ahua2`Yo`g^&qF<* zh^zkG68^mvIyz7pkd4g$`@y7zV>?ha?3~MFRcb#!jaWE-1m>eM2`P!2Tf|`?UDpcE zm|s`|qchm}2XoJyTN*sGlV_+g>7ha!s&<426`Unf zMvQs#`$G7-&=M5;P)k8BeM8=m+#PtWbs}^eZ80GI({*3FWdjI{#?2qb3o44b8#XkH zhdu0`w2(INK_sF=3~Y-F8;%xeV6yD)-0HY%jy0iF66WB8qiC!#pF*D2Op=u?_q%OZ z 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

32ruwT*5w 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

a=2)L?gNtURan( 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%0Dr-s740pRGty%QXEG8wcBB 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 Date: Thu, 19 Jun 2025 14:22:37 +0300 Subject: [PATCH 11/86] hides bars in `AqiDistributionChart` where all values are zero. --- .../widgets/aqi_distribution_chart.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..d4a25e9e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -32,8 +32,13 @@ class AqiDistributionChart extends StatelessWidget { } List _buildBarGroups() { - return List.generate(chartData.length, (index) { - final data = chartData[index]; + final groups = []; + for (var i = 0; i < chartData.length; i++) { + final data = chartData[i]; + final isAllZero = data.data.every((d) => d.percentage == 0); + if (isAllZero) { + continue; + } final stackItems = []; double currentY = 0; var isFirstElement = true; @@ -56,13 +61,15 @@ class AqiDistributionChart extends StatelessWidget { currentY += percentageData.percentage + _rodStackItemsSpacing; isFirstElement = false; } - - return BarChartGroupData( - x: index, - barRods: stackItems, - groupVertically: true, + groups.add( + BarChartGroupData( + x: i, + barRods: stackItems, + groupVertically: true, + ), ); - }); + } + return groups; } BarTouchData _barTouchData(BuildContext context) { From 78898968e89572e338c920862034d13c5d91b895 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:23:04 +0300 Subject: [PATCH 12/86] include min in `RangeOfAqiChartsHelper.titlesData.leftTitles`. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..7e8f3b04 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -40,6 +40,7 @@ abstract final class RangeOfAqiChartsHelper { reservedSize: 70, interval: 50, maxIncluded: false, + minIncluded: true, getTitlesWidget: (value, meta) { final text = value >= 300 ? '301+' : value.toInt().toString(); return Padding( From 7172a0e3fb2ae7aa37db58560316cae57ce026c9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:23:39 +0300 Subject: [PATCH 13/86] Matched aqi charts title's to have the same size no matter what the window size is. --- .../widgets/aqi_distribution_chart_title.dart | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index 926d28e1..f7be6ee3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -19,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget { children: [ ChartsLoadingWidget(isLoading: isLoading), const Expanded( - flex: 3, + flex: 4, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, @@ -28,23 +28,26 @@ class AqiDistributionChartTitle extends StatelessWidget { ), ), ), - FittedBox( - alignment: AlignmentDirectional.centerEnd, - fit: BoxFit.scaleDown, - child: AqiTypeDropdown( - onChanged: (value) { - if (value != null) { - final bloc = context.read(); - try { - final param = _makeLoadAqiDistributionParam(context, value); - bloc.add(LoadAirQualityDistribution(param)); - } catch (_) { - return; - } finally { - bloc.add(UpdateAqiTypeEvent(value)); + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) { + if (value != null) { + final bloc = context.read(); + try { + final param = _makeLoadAqiDistributionParam(context, value); + bloc.add(LoadAirQualityDistribution(param)); + } catch (_) { + return; + } finally { + bloc.add(UpdateAqiTypeEvent(value)); + } } - } - }, + }, + ), ), ), ], From ad5ada9d5528fbfc6e85f5865919d5e036ffb5ab Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 14:24:49 +0300 Subject: [PATCH 14/86] allowed `RangeOfAqiValue` values to be nullable, and if they were null they fallback to zero. --- lib/pages/analytics/models/range_of_aqi.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 0308d564..dfb48ecb 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable { factory RangeOfAqiValue.fromJson(Map json) { return RangeOfAqiValue( type: json['type'] as String, - min: (json['min'] as num).toDouble(), - average: (json['average'] as num).toDouble(), - max: (json['max'] as num).toDouble(), + min: (json['min'] as num? ?? 0).toDouble(), + average: (json['average'] as num? ?? 0).toDouble(), + max: (json['max'] as num? ?? 0).toDouble(), ); } From 8dea89db0eebcdd6d8949e387f35e853496b6e8a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:12:54 +0300 Subject: [PATCH 15/86] Implemented AQI legend. --- .../air_quality/views/air_quality_view.dart | 12 ++++-- .../air_quality/widgets/aqi_legend.dart | 38 +++++++++++++++++++ .../widgets/chart_informative_cell.dart | 4 +- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index b6d403eb..61179d15 100644 --- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { @@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 32, children: [ + SizedBox( + height: height * 0.1, + child: const AqiLegend(), + ), SizedBox( height: height * 1.2, child: const AirQualityEndSideWidget(), @@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height * 1.1, + height: height * 1.2, child: const Column( children: [ Expanded( @@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 20, children: [ - Expanded(child: RangeOfAqiChartBox()), - Expanded(child: AqiDistributionChartBox()), + Expanded(flex: 2, child: AqiLegend()), + Expanded(flex: 12, child: RangeOfAqiChartBox()), + Expanded(flex: 12, child: AqiDistributionChartBox()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart new file mode 100644 index 00000000..3a00925d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLegend extends StatelessWidget { + const AqiLegend({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(20), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 16, + children: RangeOfAqiChartsHelper.gradientData.map((e) { + return Flexible( + flex: 4, + child: FittedBox( + fit: BoxFit.fill, + child: ChartInformativeCell( + color: e.$1, + title: FittedBox( + fit: BoxFit.fill, + child: Text(e.$2), + ), + height: null, + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart index eec31998..f79ecb44 100644 --- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget { required this.title, required this.color, this.hasBorder = false, + this.height, }); final Widget title; final Color color; final bool hasBorder; + final double? height; @override Widget build(BuildContext context) { return Container( - height: MediaQuery.sizeOf(context).height * 0.0385, + height: height ?? MediaQuery.sizeOf(context).height * 0.0385, padding: const EdgeInsetsDirectional.symmetric( vertical: 8, horizontal: 12, From e4cc5fce50508c9f474129b4ab837d91c6b49267 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:18:18 +0300 Subject: [PATCH 16/86] Increased the size of `AqiDistributionChart` tooltip. --- .../widgets/aqi_distribution_chart.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index d4a25e9e..4807ebbd 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -80,6 +80,7 @@ class AqiDistributionChart extends StatelessWidget { color: ColorsManager.semiTransparentBlack, ), tooltipRoundedRadius: 16, + maxContentWidth: 500, tooltipPadding: const EdgeInsets.all(8), getTooltipItem: (group, groupIndex, rod, rodIndex) { final data = chartData[group.x]; @@ -88,10 +89,13 @@ class AqiDistributionChart extends StatelessWidget { final textStyle = context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, - fontSize: 8, + fontSize: 11, ); for (final percentageData in data.data) { + if (percentageData.percentage == 0) { + continue; + } final percentage = percentageData.percentage.toStringAsFixed(1); final type = percentageData.type[0].toUpperCase() + percentageData.type.substring(1).replaceAll('_', ' '); @@ -105,7 +109,7 @@ class AqiDistributionChart extends StatelessWidget { DateFormat('dd/MM/yyyy').format(data.date), context.textTheme.bodyMedium!.copyWith( color: ColorsManager.blackColor, - fontSize: 9, + fontSize: 12, fontWeight: FontWeight.w600, ), textAlign: TextAlign.start, @@ -125,7 +129,6 @@ class AqiDistributionChart extends StatelessWidget { final leftTitles = titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 20, maxIncluded: false, minIncluded: true, getTitlesWidget: (value, meta) => Padding( @@ -147,7 +150,7 @@ class AqiDistributionChart extends StatelessWidget { final bottomTitles = AxisTitles( sideTitles: SideTitles( - showTitles: true, + showTitles: chartData.isNotEmpty, getTitlesWidget: (value, _) => FittedBox( alignment: AlignmentDirectional.bottomCenter, fit: BoxFit.scaleDown, @@ -155,11 +158,10 @@ class AqiDistributionChart extends StatelessWidget { chartData[value.toInt()].date.day.toString(), style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.lightGreyColor, - fontSize: 8, + fontSize: 12, ), ), ), - reservedSize: 36, ), ); From 5201a65a978f795108423a29a758826e9884b9f3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:19:58 +0300 Subject: [PATCH 17/86] matched sizes of bottom titles in aqi charts. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 1 + .../modules/air_quality/widgets/aqi_distribution_chart.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 7e8f3b04..e4aa5b6f 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -23,6 +23,7 @@ abstract final class RangeOfAqiChartsHelper { return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + reservedSize: 36, getTitlesWidget: (value, meta) => Padding( padding: const EdgeInsetsDirectional.only(top: 20.0), child: Text( diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 4807ebbd..e35a05e7 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -162,6 +162,7 @@ class AqiDistributionChart extends StatelessWidget { ), ), ), + reservedSize: 36, ), ); From 23c3bf11f9496dbb73f8b4910fcfbae94c9f1053 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Thu, 19 Jun 2025 15:38:28 +0300 Subject: [PATCH 18/86] Improved alignment of `AqiLocationInfoCell`. --- .../widgets/aqi_location_info_cell.dart | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index fa0216a1..00233ad3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget { ), ), Align( - alignment: AlignmentDirectional.bottomEnd, - child: Padding( - padding: const EdgeInsetsDirectional.all(10), - child: SizedBox( - height: 40, - width: 120, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomEnd, - child: Text( - value, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.vividBlue.withValues(alpha: 0.7), - fontWeight: FontWeight.w700, - fontSize: 24, + alignment: AlignmentDirectional.bottomCenter, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: SvgPicture.asset( + svgPath, + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomStart, + ), + ), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues( + alpha: 0.7, + ), + fontWeight: FontWeight.w700, + fontSize: 24, + ), + ), ), ), ), - ), - ), - ), - Align( - alignment: AlignmentDirectional.bottomStart, - child: SizedBox.square( - dimension: MediaQuery.sizeOf(context).width * 0.45, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.bottomStart, - child: SvgPicture.asset(svgPath), - ), + ], ), ), ], From 2267d95795413182bdb882b878d5c1575aa1dfb6 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 15:46:40 +0300 Subject: [PATCH 19/86] Add schedule saving functionality and update schedule events --- .../schedule_device/bloc/schedule_bloc.dart | 125 +++++++++++++----- .../schedule_device/bloc/schedule_event.dart | 11 +- .../schedule_widgets/schedule_table.dart | 80 ++++++----- lib/services/devices_mang_api.dart | 31 +++++ lib/utils/constants/api_const.dart | 1 + 5 files changed, 186 insertions(+), 62 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index e6a2645d..c4e731db 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -37,28 +37,39 @@ class ScheduleBloc extends Bloc { Timer? _countdownTimer; Duration countdownRemaining = Duration.zero; - void _onStopScheduleEvent( + Future _onStopScheduleEvent( StopScheduleEvent event, Emitter emit, - ) { + ) async { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; - _countdownTimer?.cancel(); - if (event.mode == ScheduleModes.countdown) { - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - isCountdownActive: false, - countdownRemaining: Duration.zero, - )); - } else if (event.mode == ScheduleModes.inching) { - emit(currentState.copyWith( - inchingHours: 0, - inchingMinutes: 0, - isInchingActive: false, - countdownRemaining: Duration.zero, - )); + final success = await RemoteControlDeviceService().controlDevice( + deviceUuid: deviceId, + status: Status( + code: 'countdown_1', + value: 0, + ), + ); + if (success) { + _countdownTimer?.cancel(); + if (event.mode == ScheduleModes.countdown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } else if (event.mode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + countdownRemaining: Duration.zero, + )); + } + } else { + emit(const ScheduleError('Failed to stop schedule')); } } } @@ -241,16 +252,14 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { - final newSchedule = ScheduleEntry( + final dateTime = DateTime.parse(event.time); + final success = await DevicesManagementApi().postSchedule( category: event.category, - time: event.time, - function: Status(code: 'switch_1', value: event.functionOn), + deviceId: deviceId, + time: getTimeStampWithoutSeconds(dateTime).toString(), + code: 'switch_1', + value: event.functionOn, days: event.selectedDays); - final success = await DevicesManagementApi().addScheduleRecord( - newSchedule, - deviceId, - ); - if (success) { add(const ScheduleGetEvent(category: 'switch_1')); } else { @@ -268,14 +277,14 @@ class ScheduleBloc extends Bloc { ) async { try { if (state is ScheduleLoaded) { + final dateTime = DateTime.parse(event.time); final updatedSchedule = ScheduleEntry( scheduleId: event.scheduleId, category: event.category, - time: event.time, + time: getTimeStampWithoutSeconds(dateTime).toString(), function: Status(code: 'switch_1', value: event.functionOn), days: event.selectedDays, ); - final success = await DevicesManagementApi().editScheduleRecord( deviceId, updatedSchedule, @@ -299,10 +308,12 @@ class ScheduleBloc extends Bloc { try { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; + final updatedSchedules = currentState.schedules.map((schedule) { if (schedule.scheduleId == event.scheduleId) { return schedule.copyWith( function: Status(code: 'switch_1', value: event.functionOn), + enable: event.enable, ); } return schedule; @@ -491,10 +502,16 @@ class ScheduleBloc extends Bloc { try { final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + print(status.status); final deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); - final scheduleMode = deviceStatus.scheduleMode; + final scheduleMode = + deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0 + ? ScheduleModes.countdown + : deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0 + ? ScheduleModes.inching + : ScheduleModes.schedule; final isCountdown = scheduleMode == ScheduleModes.countdown; final isInching = scheduleMode == ScheduleModes.inching; @@ -516,6 +533,10 @@ class ScheduleBloc extends Bloc { Duration.zero; } if (state is ScheduleLoaded) { + print('Updating existing state with fetched status'); + print('scheduleMode: $scheduleMode'); + print('countdownRemaining: $countdownRemaining'); + print('isCountdownActive: $isCountdownActive'); final currentState = state as ScheduleLoaded; emit(currentState.copyWith( scheduleMode: scheduleMode, @@ -546,12 +567,54 @@ class ScheduleBloc extends Bloc { )); } - if (isCountdownActive && countdownRemaining != null) { - _startCountdownTimer(emit, countdownRemaining); - } + // if (isCountdownActive && countdownRemaining != null) { + // _startCountdownTimer(emit, countdownRemaining); + // } } catch (e) { emit(ScheduleError('Failed to fetch device status: $e')); } } + String extractTime(String isoDateTime) { + // Example input: "2025-06-19T15:45:00.000" + return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + } + + int? getTimeStampWithoutSeconds(DateTime? dateTime) { + if (dateTime == null) return null; + DateTime dateTimeWithoutSeconds = DateTime(dateTime.year, dateTime.month, + dateTime.day, dateTime.hour, dateTime.minute); + return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000; + } + + // Future _updateScheduleEvent( + // StatusUpdatedScheduleEvent event, + // Emitter emit, + // ) async { + // if (state is ScheduleLoaded) { + // final currentState = state as ScheduleLoaded; + + // final updatedSchedules = currentState.schedules.map((schedule) { + // if (schedule.scheduleId == event.scheduleId) { + // return schedule.copyWith( + // function: Status(code: 'switch_1', value: event.functionOn), + // enable: event.enable, + // ); + // } + // return schedule; + // }).toList(); + + // bool success = await DevicesManagementApi().updateScheduleRecord( + // enable: event.enable, + // uuid: currentState.status.uuid, + // scheduleId: event.scheduleId, + // ); + + // if (success) { + // emit(currentState.copyWith(schedules: updatedSchedules)); + // } else { + // emit(currentState); + // } + // } + // } } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 369ca795..5099679c 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -68,7 +68,7 @@ class ScheduleGetEvent extends ScheduleEvent { class ScheduleAddEvent extends ScheduleEvent { final String category; - final String time; + final String time; final List selectedDays; final bool functionOn; @@ -219,3 +219,12 @@ class DeleteScheduleEvent extends ScheduleEvent { @override List get props => [scheduleId]; } + +class StatusUpdatedScheduleEvent extends ScheduleEvent { + final String id; + + const StatusUpdatedScheduleEvent(this.id); + + @override + List get props => [id]; +} diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 4d36e0e2..97ca03e1 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -160,28 +160,26 @@ class _ScheduleTableView extends StatelessWidget { children: [ Center( child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { - ///TODO: Implement toggle functionality - - // Toggle enabled state using ScheduleBloc - // context.read().add( - // UpdateScheduleEvent( - // scheduleId: schedule.scheduleId, - // functionOn: schedule.function.value, - // enable: !schedule.enable, - // ), - // ); - }, - child: SizedBox( - width: 24, - height: 24, - child: schedule.enable - ? const Icon(Icons.radio_button_checked, - color: ColorsManager.blueColor) - : const Icon( - Icons.radio_button_unchecked, - color: ColorsManager.grayColor, + context.read().add( + ScheduleUpdateEntryEvent( + scheduleId: schedule.scheduleId, + functionOn: schedule.function.value, + enable: !schedule.enable, ), + ); + }, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: schedule.enable + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked, + color: ColorsManager.grayColor), + ), ), ), ), @@ -202,7 +200,6 @@ class _ScheduleTableView extends StatelessWidget { schedule: ScheduleEntry.fromScheduleModel(schedule), isEdit: true, ).then((updatedSchedule) { - print('updatedSchedule : $updatedSchedule'); if (updatedSchedule != null) { context.read().add( ScheduleEditEvent( @@ -225,12 +222,38 @@ class _ScheduleTableView extends StatelessWidget { ), TextButton( style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - context.read().add( - DeleteScheduleEvent( - schedule.scheduleId, - ), + onPressed: () async { + final confirmed = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('Confirm Delete'), + content: const Text( + 'Are you sure you want to delete this schedule?'), + actions: [ + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(false), + child: Text('Cancel'), + ), + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(true), + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + ), + ], ); + }, + ); + + if (confirmed == true) { + context.read().add( + ScheduleDeleteEvent(schedule.scheduleId), + ); + } }, child: Text( 'Delete', @@ -239,7 +262,7 @@ class _ScheduleTableView extends StatelessWidget { .bodySmall! .copyWith(color: ColorsManager.blueColor), ), - ), + ) ], ), ), @@ -248,7 +271,6 @@ class _ScheduleTableView extends StatelessWidget { } String _getSelectedDays(List selectedDays) { - // Use the same order as in ScheduleDialogHelper const days = ScheduleDialogHelper.allDays; return selectedDays .asMap() @@ -257,6 +279,4 @@ class _ScheduleTableView extends StatelessWidget { .map((entry) => days[entry.key]) .join(', '); } - - // Removed allDays from here as it is now in ScheduleDialogHelper } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6f60e34f..6fb27daf 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:core'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; @@ -386,4 +387,34 @@ class DevicesManagementApi { ); return response; } + + Future postSchedule({ + required String category, + required String deviceId, + required String time, + required String code, + required bool value, + required List days, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.saveSchedule.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + body: jsonEncode( + { + 'category': category, + 'time': time, + 'function': { + 'code': code, + 'value': value, + }, + 'days': days + }, + ), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 411e72a5..79fd013e 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -136,4 +136,5 @@ abstract class ApiEndpoints { static const String assignDeviceToRoom = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; + static const String saveSchedule = '/schedule/{deviceUuid}'; } From 0b774a6dfca22f356801d83b8630b236661b6076 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 16:20:46 +0300 Subject: [PATCH 20/86] Add scheduling category parameter to BuildScheduleView and update device control dialogs --- .../one_gang_glass_switch_control_view.dart | 3 +- .../view/wall_light_device_control.dart | 3 +- .../schedule_widgets/schedual_view.dart | 6 +- .../three_gang_glass_switch_control_view.dart | 24 +- .../view/living_room_device_control.dart | 143 +++++++++- .../two_gang_glass_switch_control_view.dart | 46 ++++ .../view/wall_light_device_control.dart | 256 ++++++++++++------ .../view/water_heater_device_control.dart | 3 +- 8 files changed, 368 insertions(+), 116 deletions(-) diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 694c2f6e..3f4e6024 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -83,12 +83,13 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index e590adea..e5f2358e 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -76,12 +76,13 @@ class WallLightDeviceControl extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 591b114f..2ae5b869 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -12,8 +12,10 @@ import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_sched import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { - const BuildScheduleView({super.key, required this.deviceUuid}); + const BuildScheduleView( + {super.key, required this.deviceUuid, required this.category}); final String deviceUuid; + final String category; @override Widget build(BuildContext context) { @@ -21,7 +23,7 @@ class BuildScheduleView extends StatelessWidget { create: (_) => ScheduleBloc( deviceId: deviceUuid, ) - ..add(const ScheduleGetEvent(category: "switch_1")) + ..add(ScheduleGetEvent(category: category)) ..add(ScheduleFetchStatusEvent(deviceUuid)), child: Dialog( backgroundColor: Colors.white, diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 7d80f289..79c2138b 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -115,14 +115,14 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( + category: 'switch_1', deviceUuid: deviceId, ), )); @@ -160,26 +160,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), ), ) - // ToggleWidget( - // value: true, - // code: '', - // deviceId: deviceId, - // label: 'Scheduling', - // icon: Assets.scheduling, - // onChange: (value) { - // print('Scheduling clicked'); - // showDialog( - // context: context, - // builder: (ctx) => BlocProvider.value( - // value: BlocProvider.of(context), - // child: BuildScheduleView( - // deviceUuid: deviceId, - // ), - // ), - // ); - // }, - // showToggle: false, - // ), ], ); } diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index f4739b88..57c4d397 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -98,12 +98,13 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', ), )); }, @@ -128,13 +129,141 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_3', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Spotlight', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index cd80f528..c4df483b 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -115,6 +115,52 @@ class TwoGangGlassSwitchControlView extends StatelessWidget value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', ), )); }, diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 05a02a69..d1f75564 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -43,92 +43,184 @@ class TwoGangDeviceControlView extends StatelessWidget Widget _buildStatusControls(BuildContext context, TwoGangStatusModel status) { return Center( - child: Wrap( - alignment: WrapAlignment.center, - spacing: 12, - runSpacing: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - width: 200, - height: 120, - child: ToggleWidget( - value: status.switch1, - code: 'switch_1', - deviceId: deviceId, - label: 'Wall Light', - onChange: (value) { - context.read().add(TwoGangSwitchControl( - deviceId: deviceId, - code: 'switch_1', - value: value, - )); - }, - ), - ), - SizedBox( - width: 200, - height: 120, - child: ToggleWidget( - value: status.switch2, - code: 'switch_2', - deviceId: deviceId, - label: 'Ceiling Light', - onChange: (value) { - context.read().add(TwoGangSwitchControl( - deviceId: deviceId, - code: 'switch_2', - value: value, - )); - }, - ), - ), - SizedBox( - width: 200, - height: 120, - child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (ctx) => BlocProvider.value( - value: BlocProvider.of(context), - child: BuildScheduleView( - deviceUuid: deviceId, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + height: 150, + child: ToggleWidget( + value: status.switch1, + code: 'switch_1', + deviceId: deviceId, + label: 'Wall Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_1', + value: value, )); - }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], + }, ), ), - ), + const SizedBox(width: 10), + SizedBox( + width: 200, + height: 150, + child: ToggleWidget( + value: status.switch2, + code: 'switch_2', + deviceId: deviceId, + label: 'Ceiling Light', + onChange: (value) { + context.read().add(TwoGangSwitchControl( + deviceId: deviceId, + code: 'switch_2', + value: value, + )); + }, + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + height: 150, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + color: ColorsManager.blackColor, + fontSize: 12), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + ), + const SizedBox(width: 10), + SizedBox( + width: 200, + height: 150, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + color: ColorsManager.blackColor, + fontSize: 12), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], ) ], ), diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 1d80fd9f..a847e315 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -76,12 +76,13 @@ class WaterHeaterDeviceControlView extends StatelessWidget ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( deviceUuid: device.uuid ?? '', + category: 'switch_1', ), )); }, From 8cf73e3efc1aa6afd2ac0c86a5e5a79036240b19 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 19 Jun 2025 16:38:45 +0300 Subject: [PATCH 21/86] Enhance scheduling UI in glass switch control views with improved layout and dialog integration --- .../three_gang_glass_switch_control_view.dart | 164 ++++++++++++++++-- .../two_gang_glass_switch_control_view.dart | 71 +++++--- 2 files changed, 193 insertions(+), 42 deletions(-) diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 79c2138b..bfc0a73e 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -106,14 +106,123 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ); }, ), - ToggleWidget( - value: false, - code: '', - deviceId: deviceId, - label: 'Preferences', - icon: Assets.preferences, - onChange: (value) {}, - showToggle: false, + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceId, + ), + )); + }, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + Assets.scheduling, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), ), GestureDetector( onTap: () { @@ -148,18 +257,41 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'SpotLight', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), ), - ) + ), + ToggleWidget( + value: false, + code: '', + deviceId: deviceId, + label: 'Preferences', + icon: Assets.preferences, + onChange: (value) {}, + showToggle: false, + ), ], ); } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index c4df483b..3c6d7551 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -98,18 +98,9 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - // ToggleWidget( - // value: false, - // code: '', - // deviceId: deviceId, - // label: 'Scheduling', - // icon: Assets.scheduling, - // onChange: (value) {}, - // showToggle: false, - // ), GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), @@ -140,21 +131,35 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Wall Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), ), ), - GestureDetector( + GestureDetector( onTap: () { - showDialog( + showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), @@ -185,13 +190,27 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), ), const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ceiling Light', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + 'Scheduling', + textAlign: TextAlign.center, + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], ), ], ), From 1f82e8411561cdc7582717fc2b466566ae240fc9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 10:55:41 +0300 Subject: [PATCH 22/86] doesnt fetch devices on date change. --- .../helpers/fetch_air_quality_data_helper.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..f23abd7b 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper { }) { final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + if (shouldFetchAnalyticsDevices) { + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, From 2f233db3326c0d3846b0b87506d1a4e3fb9a7401 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:04:39 +0300 Subject: [PATCH 23/86] implemented space management side bar. --- .../views/space_management_page.dart | 81 ++++++-- .../widgets/space_management_body.dart | 15 ++ .../services/remote_communities_service.dart | 32 +++- .../domain/models/space_model.dart | 16 ++ .../communities_tree_selection_bloc.dart | 47 +++++ .../communities_tree_selection_event.dart | 30 +++ .../communities_tree_selection_state.dart | 29 +++ .../presentation/widgets/community_tile.dart | 37 ++++ .../widgets/create_community_dialog.dart | 181 ++++++++++++++++++ .../space_management_communities_tree.dart | 160 ++++++++++++++++ ...nagement_sidebar_add_community_button.dart | 34 ++++ ...e_management_sidebar_communities_list.dart | 72 +++++++ .../space_management_sidebar_header.dart | 36 ++++ .../presentation/widgets/space_tile.dart | 54 ++++++ .../all_spaces/widgets/space_tile_widget.dart | 3 +- lib/utils/app_routes.dart | 2 +- 16 files changed, 808 insertions(+), 21 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_body.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 03e17165..4c3c7452 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -1,5 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -8,20 +16,67 @@ class SpaceManagementPage extends StatelessWidget { @override Widget build(BuildContext context) { - return WebScaffold( - appBarTitle: Text( - 'Space Management', - style: ResponsiveTextTheme.of(context).deviceManagementTitle, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: _FakeCommunitiesService(), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), ), - enableMenuSidebar: false, - centerBody: Text( - 'Community Structure', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} + +class _FakeCommunitiesService extends CommunitiesService { + @override + Future> getCommunity(LoadCommunitiesParam param) async { + return Future.delayed( + const Duration(seconds: 1), + () => [ + const CommunityModel( + uuid: '1', + name: 'Community 1', + spaces: [ + SpaceModel( + uuid: '3', + spaceName: 'Space 1', + icon: 'assets/icons/space.png', + children: [ + SpaceModel( + uuid: '4', + spaceName: 'Space 2', + icon: 'assets/icons/space.png', + children: [], + status: SpaceStatus.active, + ), + ], + status: SpaceStatus.active, + ), + ], + ), + const CommunityModel( + uuid: '2', + name: 'Community 1', + spaces: [], + ), + ], ); } } diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..3a9aa3c8 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + SpaceManagementCommunitiesTree(), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..83a212ca 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -14,13 +16,27 @@ class RemoteCommunitiesService implements CommunitiesService { @override Future> getCommunity(LoadCommunitiesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is not set'); + try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final allCommunities = []; + await _httpService.get( + path: await _makeUrl(), + expectedResponseModel: (json) { + final response = json as Map; + final jsonData = response['data'] as List? ?? []; + return jsonData + .map( + (jsonItem) => CommunityModel.fromJson( + jsonItem as Map, + ), + ) + .toList(); + }, ); + + return allCommunities; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +47,10 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..519e8ee7 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,16 +1,31 @@ import 'package:equatable/equatable.dart'; +enum SpaceStatus { + active, + deleted, + parentDeleted; + + static SpaceStatus getValueFromString(String value) => switch (value) { + 'active' => active, + 'deleted' => deleted, + 'parentDeleted' => parentDeleted, + _ => active, + }; +} + class SpaceModel extends Equatable { final String uuid; final String spaceName; final String icon; final List children; + final SpaceStatus status; const SpaceModel({ required this.uuid, required this.spaceName, required this.icon, required this.children, + required this.status, }); factory SpaceModel.fromJson(Map json) { @@ -22,6 +37,7 @@ class SpaceModel extends Equatable { ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + status: SpaceStatus.getValueFromString(json['status'] as String), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bfc02f11 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: null, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..95ffe173 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,30 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel? community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + + const SelectSpaceEvent({required this.space}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart new file mode 100644 index 00000000..fd8a0a68 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialog extends StatefulWidget { + final void Function(String name) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + State createState() => _CreateCommunityDialogState(); +} + +class _CreateCommunityDialogState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CommunityDialogBloc([]), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + var isNameValid = true; + var isNameEmpty = false; + + if (state is CommunityNameValidationState) { + isNameValid = state.isNameValid; + isNameEmpty = state.isNameEmpty; + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + TextField( + controller: _nameController, + onChanged: (value) { + context + .read() + .add(ValidateCommunityNameEvent(value)); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isNameValid && !isNameEmpty + ? ColorsManager.boxColor + : ColorsManager.red, + width: 1, + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (!isNameValid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exists.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DefaultButton( + onPressed: () { + if (isNameValid && !isNameEmpty) { + widget.onCreateCommunity( + _nameController.text.trim(), + ); + Navigator.of(context).pop(); + } + }, + backgroundColor: isNameValid && !isNameEmpty + ? ColorsManager.secondaryColor + : ColorsManager.lightGrayColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..3248fa7d --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatelessWidget { + const SpaceManagementCommunitiesTree({super.key}); + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } + + static const _width = 300.0; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + width: _width, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + SpaceManagementSidebarHeader( + onAddCommunity: () => _onAddCommunity(context), + ), + CustomSearchBar(onSearchChanged: (value) {}), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.loading => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.success => + _buildCommunitiesTree(context, state.communities), + CommunitiesStatus.failure => Center( + child: Text(state.errorMessage ?? 'Something went wrong'), + ), + }, + ], + ), + ); + }, + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + List communities, + ) { + return Expanded( + child: SpaceManagementSidebarCommunitiesList( + communities: communities, + itemBuilder: (context, index) { + return _buildCommunityTile(context, communities[index]); + }, + ), + ); + } + + Widget _buildCommunityTile(BuildContext context, CommunityModel community) { + final spaces = community.spaces + .where((space) => space.status == SpaceStatus.active) + .map((space) => _buildSpaceTile( + space: space, + community: community, + context: context, + )) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } + + Widget _buildSpaceTile({ + required SpaceModel space, + required CommunityModel community, + required BuildContext context, + }) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => _buildSpaceTile( + space: childSpace, + community: community, + context: context, + ), + ) + .toList(), + ), + ); + } + + void _onAddCommunity(BuildContext context) => context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + true + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + + void _clearSelection(BuildContext context) => + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (name) {}, + ), + ); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..e7cb1ef6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd) { + // Call data from API. + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(() {}) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: _scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: widget.communities.length, + controller: _scrollController, + itemBuilder: widget.itemBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..cf40f95c --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({ + required this.onAddCommunity, + super.key, + }); + + final void Function() onAddCommunity; + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: onAddCommunity, + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 51c088d9984f0860d36177cd1645d74d5ac87aa8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:11:25 +0300 Subject: [PATCH 24/86] made communities paginatable. --- .../views/space_management_page.dart | 39 +++----- .../services/remote_communities_service.dart | 25 +++-- .../models/communities_pagination_model.dart | 69 +++++++++++++ .../domain/params/load_communities_param.dart | 33 ++++++- .../domain/services/communities_service.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 99 ++++++++++++++++++- .../presentation/bloc/communities_event.dart | 16 +++ .../presentation/bloc/communities_state.dart | 38 ++++++- 8 files changed, 272 insertions(+), 51 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 4c3c7452..93e2684f 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; @@ -46,37 +46,20 @@ class SpaceManagementPage extends StatelessWidget { class _FakeCommunitiesService extends CommunitiesService { @override - Future> getCommunity(LoadCommunitiesParam param) async { - return Future.delayed( - const Duration(seconds: 1), - () => [ - const CommunityModel( + Future getCommunity(LoadCommunitiesParam param) { + return Future.value(const CommunitiesPaginationModel( + communities: [ + CommunityModel( uuid: '1', name: 'Community 1', - spaces: [ - SpaceModel( - uuid: '3', - spaceName: 'Space 1', - icon: 'assets/icons/space.png', - children: [ - SpaceModel( - uuid: '4', - spaceName: 'Space 2', - icon: 'assets/icons/space.png', - children: [], - status: SpaceStatus.active, - ), - ], - status: SpaceStatus.active, - ), - ], - ), - const CommunityModel( - uuid: '2', - name: 'Community 1', spaces: [], ), ], - ); + page: 1, + size: 10, + hasNext: false, + totalItems: 2, + totalPages: 1, + )); } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 83a212ca..e4202398 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -15,28 +15,25 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future> getCommunity(LoadCommunitiesParam param) async { + Future getCommunity(LoadCommunitiesParam param) async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is not set'); try { - final allCommunities = []; - await _httpService.get( + final response = await _httpService.get( path: await _makeUrl(), + queryParameters: { + 'page': param.page, + 'size': param.size, + 'includeSpaces': param.includeSpaces, + if (param.search.isNotEmpty) 'search': param.search, + }, expectedResponseModel: (json) { - final response = json as Map; - final jsonData = response['data'] as List? ?? []; - return jsonData - .map( - (jsonItem) => CommunityModel.fromJson( - jsonItem as Map, - ), - ) - .toList(); + return CommunitiesPaginationModel.fromJson(json as Map); }, ); - return allCommunities; + return response; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart new file mode 100644 index 00000000..f13ef8ba --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; + +class CommunitiesPaginationModel extends Equatable { + const CommunitiesPaginationModel({ + required this.communities, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List communities; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + const CommunitiesPaginationModel.empty() + : communities = const [], + page = 1, + size = 25, + hasNext = false, + totalItems = 0, + totalPages = 0; + + factory CommunitiesPaginationModel.fromJson(Map json) { + return CommunitiesPaginationModel( + communities: (json['data'] as List? ?? []) + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItems'] as int? ?? 0, + totalPages: json['totalPages'] as int? ?? 0, + ); + } + + CommunitiesPaginationModel copyWith({ + List? communities, + int? page, + int? size, + bool? hasNext, + int? totalItems, + int? totalPages, + }) { + return CommunitiesPaginationModel( + communities: communities ?? this.communities, + page: page ?? this.page, + size: size ?? this.size, + hasNext: hasNext ?? this.hasNext, + totalItems: totalItems ?? this.totalItems, + totalPages: totalPages ?? this.totalPages, + ); + } + + @override + List get props => [ + communities, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart index 9bdc215c..774c4c31 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart @@ -1,3 +1,32 @@ -class LoadCommunitiesParam { - const LoadCommunitiesParam(); +import 'package:equatable/equatable.dart'; + +class LoadCommunitiesParam extends Equatable { + const LoadCommunitiesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.includeSpaces = true, + }); + + final int page; + final int size; + final String search; + final bool includeSpaces; + + LoadCommunitiesParam copyWith({ + int? page, + int? size, + String? search, + bool? includeSpaces, + }) { + return LoadCommunitiesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + includeSpaces: includeSpaces ?? this.includeSpaces, + ); + } + + @override + List get props => [page, size, search, includeSpaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index bccad2ad..564dc4da 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,6 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; abstract class CommunitiesService { - Future> getCommunity(LoadCommunitiesParam param); + Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0d85b22f..47dd43f8 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc { }) : _communitiesService = communitiesService, super(const CommunitiesState()) { on(_onLoadCommunities); + on(_onLoadMoreCommunities); + on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -23,24 +25,113 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(const CommunitiesState(status: CommunitiesStatus.loading)); - final communities = await _communitiesService.getCommunity(event.param); + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final paginationResponse = await _communitiesService.getCommunity(event.param); + emit( CommunitiesState( status: CommunitiesStatus.success, - communities: communities, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.param.search, ), ); } on APIException catch (e) { emit( - CommunitiesState( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.message, ), ); } catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onLoadMoreCommunities( + LoadMoreCommunities event, + Emitter emit, + ) async { + if (!state.hasNext || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + + final param = LoadCommunitiesParam( + page: state.currentPage + 1, + search: state.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + + final updatedCommunities = List.from(state.communities) + ..addAll(paginationResponse.communities); + + emit( + state.copyWith( + communities: updatedCommunities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + isLoadingMore: false, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onSearchCommunities( + SearchCommunities event, + Emitter emit, + ) async { + try { + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final param = LoadCommunitiesParam( + page: 1, + search: event.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + emit( CommunitiesState( + status: CommunitiesStatus.success, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.searchQuery, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ef375c5a..aa6eda17 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent { @override List get props => [param]; } + +class LoadMoreCommunities extends CommunitiesEvent { + const LoadMoreCommunities(); + + @override + List get props => []; +} + +class SearchCommunities extends CommunitiesEvent { + const SearchCommunities(this.searchQuery); + + final String searchQuery; + + @override + List get props => [searchQuery]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart index 94740f0b..c0e57ffd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart @@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable { this.status = CommunitiesStatus.initial, this.communities = const [], this.errorMessage, + this.isLoadingMore = false, + this.hasNext = false, + this.currentPage = 1, + this.searchQuery = '', }); final CommunitiesStatus status; final List communities; final String? errorMessage; + final bool isLoadingMore; + final bool hasNext; + final int currentPage; + final String searchQuery; + + CommunitiesState copyWith({ + CommunitiesStatus? status, + List? communities, + String? errorMessage, + bool? isLoadingMore, + bool? hasNext, + int? currentPage, + String? searchQuery, + }) { + return CommunitiesState( + status: status ?? this.status, + communities: communities ?? this.communities, + errorMessage: errorMessage ?? this.errorMessage, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasNext: hasNext ?? this.hasNext, + currentPage: currentPage ?? this.currentPage, + searchQuery: searchQuery ?? this.searchQuery, + ); + } @override - List get props => [status, communities, errorMessage]; + List get props => [ + status, + communities, + errorMessage, + isLoadingMore, + hasNext, + currentPage, + searchQuery, + ]; } From 65ed94eb089d5c135ce2ec456471a23d7d069efa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:01:32 +0300 Subject: [PATCH 25/86] debounce and refactored `CommunitiesBloc`. --- .../debounced_communities_service.dart | 60 ++++++++++++ .../presentation/bloc/communities_bloc.dart | 82 ++++------------ .../presentation/bloc/communities_event.dart | 9 -- .../space_management_communities_tree.dart | 97 ++++++++++++++++--- ...e_management_sidebar_communities_list.dart | 44 ++++++++- 5 files changed, 204 insertions(+), 88 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart new file mode 100644 index 00000000..f8bd56d1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; + +class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService({ + required CommunitiesService communitiesService, + this.debounceDuration = const Duration(milliseconds: 400), + }) : _communitiesService = communitiesService; + + final CommunitiesService _communitiesService; + final Duration debounceDuration; + + Timer? _debounceTimer; + String _lastSearchQuery = ''; + + @override + Future getCommunity( + LoadCommunitiesParam param, + ) async { + if (param.search.isNotEmpty) { + return _getDebouncedCommunity(param); + } + + return _communitiesService.getCommunity(param); + } + + Future _getDebouncedCommunity( + LoadCommunitiesParam param, + ) async { + final completer = Completer(); + + _debounceTimer?.cancel(); + + _lastSearchQuery = param.search; + + _debounceTimer = Timer(debounceDuration, () async { + try { + if (_lastSearchQuery == param.search) { + final result = await _communitiesService.getCommunity(param); + if (!completer.isCompleted) { + completer.complete(result); + } + } else { + if (!completer.isCompleted) { + completer.complete(const CommunitiesPaginationModel.empty()); + } + } + } catch (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + } + }); + + return completer.future; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 47dd43f8..53eb9d3f 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,7 +15,6 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); - on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -39,19 +38,9 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } @@ -83,59 +72,30 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } - Future _onSearchCommunities( - SearchCommunities event, + void _onApiException( + APIException e, Emitter emit, - ) async { - try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + ) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } - final param = LoadCommunitiesParam( - page: 1, - search: event.searchQuery, - ); - - final paginationResponse = await _communitiesService.getCommunity(param); - - emit( - CommunitiesState( - status: CommunitiesStatus.success, - communities: paginationResponse.communities, - hasNext: paginationResponse.hasNext, - currentPage: paginationResponse.page, - searchQuery: event.searchQuery, - ), - ); - } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); - } + void _onError(Object e, Emitter emit) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index aa6eda17..ae4d86bf 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,12 +22,3 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } - -class SearchCommunities extends CommunitiesEvent { - const SearchCommunities(this.searchQuery); - - final String searchQuery; - - @override - List get props => [searchQuery]; -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 3248fa7d..51322b52 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,20 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; import 'package:syncrow_web/utils/style.dart'; -class SpaceManagementCommunitiesTree extends StatelessWidget { +class SpaceManagementCommunitiesTree extends StatefulWidget { const SpaceManagementCommunitiesTree({super.key}); + @override + State createState() => + _SpaceManagementCommunitiesTreeState(); +} + +class _SpaceManagementCommunitiesTreeState + extends State { + @override + void initState() { + context.read().add( + const LoadCommunities(LoadCommunitiesParam()), + ); + super.initState(); + } + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { final selectedSpace = context.read().state.selectedSpace; @@ -25,6 +41,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { return isSpaceSelected || anySubSpaceIsSelected; } + void _onSearchChanged(String searchQuery) { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: searchQuery.trim(), + )), + ); + } + + void _onLoadMore() { + context.read().add(const LoadMoreCommunities()); + } + static const _width = 300.0; @override @@ -39,18 +67,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { SpaceManagementSidebarHeader( onAddCommunity: () => _onAddCommunity(context), ), - CustomSearchBar(onSearchChanged: (value) {}), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), const SizedBox(height: 16), switch (state.status) { CommunitiesStatus.initial => const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.success => - _buildCommunitiesTree(context, state.communities), - CommunitiesStatus.failure => Center( - child: Text(state.errorMessage ?? 'Something went wrong'), - ), + CommunitiesStatus.loading => state.communities.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => _buildErrorState(context, state), }, ], ), @@ -59,15 +87,58 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { ); } + Widget _buildErrorState(BuildContext context, CommunitiesState state) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: state.searchQuery, + )), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + Widget _buildCommunitiesTree( BuildContext context, - List communities, + CommunitiesState state, ) { + if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { + return Expanded( + child: Center( + child: Text( + state.searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "${state.searchQuery}"', + textAlign: TextAlign.center, + ), + ), + ); + } + return Expanded( child: SpaceManagementSidebarCommunitiesList( - communities: communities, + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, itemBuilder: (context, index) { - return _buildCommunityTile(context, communities[index]); + return _buildCommunityTile(context, state.communities[index]); }, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index e7cb1ef6..68119dcd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -6,11 +6,17 @@ class SpaceManagementSidebarCommunitiesList extends StatefulWidget { const SpaceManagementSidebarCommunitiesList({ required this.communities, required this.itemBuilder, + this.onLoadMore, + this.isLoadingMore = false, + this.hasNext = false, super.key, }); final List communities; final Widget Function(BuildContext context, int index) itemBuilder; + final VoidCallback? onLoadMore; + final bool isLoadingMore; + final bool hasNext; @override State createState() => @@ -25,12 +31,26 @@ class _SpaceManagementSidebarCommunitiesListState void initState() { super.initState(); _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + // Trigger pagination when user is close to the bottom + if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { + widget.onLoadMore!(); + } + } } bool _onNotification(ScrollEndNotification notification) { final hasReachedEnd = notification.metrics.extentAfter == 0; - if (hasReachedEnd) { - // Call data from API. + if (hasReachedEnd && + widget.hasNext && + !widget.isLoadingMore && + widget.onLoadMore != null) { + widget.onLoadMore!(); return true; } @@ -40,13 +60,16 @@ class _SpaceManagementSidebarCommunitiesListState @override void dispose() { _scrollController - ..removeListener(() {}) + ..removeListener(_onScroll) ..dispose(); super.dispose(); } @override Widget build(BuildContext context) { + // Calculate item count including loading indicator + final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( @@ -60,9 +83,20 @@ class _SpaceManagementSidebarCommunitiesListState child: ListView.builder( shrinkWrap: true, padding: const EdgeInsetsDirectional.only(start: 16), - itemCount: widget.communities.length, + itemCount: itemCount, controller: _scrollController, - itemBuilder: widget.itemBuilder, + itemBuilder: (context, index) { + if (index == widget.communities.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return widget.itemBuilder(context, index); + }, ), ), ), From d2713c590285b0bc67b65175cb66720fe8bab9a9 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 12:23:09 +0300 Subject: [PATCH 26/86] Add ScheduleControlButton widget and integrate it into water heater and wall light device controls --- .../one_gang_glass_switch_control_view.dart | 55 ++---- .../view/wall_light_device_control.dart | 46 +---- .../schedule_control_button.dart | 72 ++++++++ .../three_gang_glass_switch_control_view.dart | 161 ++---------------- .../view/living_room_device_control.dart | 154 ++--------------- .../two_gang_glass_switch_control_view.dart | 109 ++---------- .../view/wall_light_batch_control.dart | 47 ++++- .../view/wall_light_device_control.dart | 105 +----------- .../view/water_heater_device_control.dart | 40 +---- 9 files changed, 187 insertions(+), 602 deletions(-) create mode 100644 lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 3f4e6024..1ad5d43b 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,18 +1,16 @@ 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/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { +class OneGangGlassSwitchControlView extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const OneGangGlassSwitchControlView({required this.deviceId, super.key}); @@ -21,7 +19,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv Widget build(BuildContext context) { return BlocProvider( create: (context) => - OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + OneGangGlassSwitchBlocFactory.create(deviceId: deviceId) + ..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is OneGangGlassSwitchLoading) { @@ -38,7 +37,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv ); } - Widget _buildStatusControls(BuildContext context, OneGangGlassStatusModel status) { + Widget _buildStatusControls( + BuildContext context, OneGangGlassStatusModel status) { final isExtraLarge = isExtraLargeScreenSize(context); final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); @@ -81,51 +81,22 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv onChange: (value) {}, showToggle: false, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceUuid: deviceId, category: 'switch_1', + deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index e5f2358e..2f6008d2 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -1,17 +1,14 @@ 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/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class WallLightDeviceControl extends StatelessWidget @@ -74,51 +71,22 @@ class WallLightDeviceControl extends StatelessWidget )); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - deviceUuid: deviceId, category: 'switch_1', + deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart new file mode 100644 index 00000000..86fc5ba5 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleControlButton extends StatelessWidget { + final VoidCallback onTap; + final String mainText; + final String subtitle; + final String iconPath; + + const ScheduleControlButton({ + super.key, + required this.onTap, + required this.mainText, + required this.subtitle, + required this.iconPath, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: DeviceControlsContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + child: ClipOval( + child: SvgPicture.asset( + iconPath, + fit: BoxFit.fill, + ), + ), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + mainText, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w200, + fontSize: 12, + color: ColorsManager.blackColor, + ), + ), + Text( + subtitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index bfc0a73e..72435b74 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -1,16 +1,12 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; - import '../models/three_gang_glass_switch.dart'; class ThreeGangGlassSwitchControlView extends StatelessWidget @@ -106,7 +102,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -118,54 +114,11 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -177,111 +130,25 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(context), child: BuildScheduleView( - category: 'switch_1', + category: 'switch_3', deviceUuid: deviceId, ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'SpotLight', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'SpotLight', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ToggleWidget( value: false, diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 57c4d397..66784bd5 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,15 +1,12 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class LivingRoomDeviceControlsView extends StatelessWidget @@ -96,7 +93,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget ); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -108,52 +105,11 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -165,53 +121,11 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -223,51 +137,9 @@ class LivingRoomDeviceControlsView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Spotlight', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Spotlight', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ], ); diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 3c6d7551..34b30dd3 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -1,15 +1,12 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangGlassSwitchControlView extends StatelessWidget @@ -98,7 +95,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget onChange: (value) {}, showToggle: false, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -110,54 +107,11 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -169,53 +123,10 @@ class TwoGangGlassSwitchControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - fontSize: 12, - color: ColorsManager.blackColor, - ), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), - ) + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index e8346cb2..849412f2 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; @@ -8,9 +10,11 @@ import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class TwoGangBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const TwoGangBatchControlView({super.key, required this.deviceIds}); final List deviceIds; @@ -18,15 +22,17 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) - ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), + create: (context) => + TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) + ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is TwoGangSwitchLoading) { return const Center(child: CircularProgressIndicator()); } else if (state is TwoGangSwitchStatusLoaded) { return _buildStatusControls(context, state.status); - } else if (state is TwoGangSwitchError || state is TwoGangSwitchControlError) { + } else if (state is TwoGangSwitchError || + state is TwoGangSwitchControlError) { return const Center(child: Text('Error fetching status')); } else { return const Center(child: CircularProgressIndicator()); @@ -82,6 +88,39 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou )); }, ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), // FirmwareUpdateWidget( // deviceId: deviceIds.first, // version: 12, diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index d1f75564..ac3fe579 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -1,17 +1,14 @@ 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/schedule_device/schedule_widgets/schedual_view.dart'; -import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.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:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class TwoGangDeviceControlView extends StatelessWidget @@ -96,7 +93,7 @@ class TwoGangDeviceControlView extends StatelessWidget SizedBox( width: 200, height: 150, - child: GestureDetector( + child: ScheduleControlButton( onTap: () { showDialog( context: context, @@ -109,58 +106,16 @@ class TwoGangDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Wall Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - color: ColorsManager.blackColor, - fontSize: 12), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), const SizedBox(width: 10), SizedBox( width: 200, height: 150, - child: GestureDetector( + child: ScheduleControlButton( onTap: () { showDialog( context: context, @@ -173,51 +128,9 @@ class TwoGangDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ceiling Light', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w200, - color: ColorsManager.blackColor, - fontSize: 12), - ), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ], - ), - ), + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ), ], diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index a847e315..16eff86a 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; @@ -74,7 +75,7 @@ class WaterHeaterDeviceControlView extends StatelessWidget )); }, ), - GestureDetector( + ScheduleControlButton( onTap: () { showDialog( context: context, @@ -86,39 +87,10 @@ class WaterHeaterDeviceControlView extends StatelessWidget ), )); }, - child: DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: const EdgeInsets.all(12), - child: ClipOval( - child: SvgPicture.asset( - Assets.scheduling, - fit: BoxFit.fill, - ), - ), - ), - const Spacer(), - Text( - 'Scheduling', - textAlign: TextAlign.center, - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ) + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } From 8494f0a8f1a484148bdae6afec928c53cf738720 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:21:46 +0300 Subject: [PATCH 27/86] matched community and space models with API. --- .../views/space_management_page.dart | 30 +++-------- .../debounced_communities_service.dart | 51 +++++++------------ .../services/remote_communities_service.dart | 18 ++++--- .../models/communities_pagination_model.dart | 4 +- .../domain/models/community_model.dart | 12 +++++ .../domain/models/space_model.dart | 27 ++++------ .../space_management_communities_tree.dart | 1 - lib/utils/constants/api_const.dart | 1 + 8 files changed, 62 insertions(+), 82 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 93e2684f..957be65a 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -20,7 +20,9 @@ class SpaceManagementPage extends StatelessWidget { providers: [ BlocProvider( create: (context) => CommunitiesBloc( - communitiesService: _FakeCommunitiesService(), + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), @@ -43,23 +45,3 @@ class SpaceManagementPage extends StatelessWidget { ); } } - -class _FakeCommunitiesService extends CommunitiesService { - @override - Future getCommunity(LoadCommunitiesParam param) { - return Future.value(const CommunitiesPaginationModel( - communities: [ - CommunityModel( - uuid: '1', - name: 'Community 1', - spaces: [], - ), - ], - page: 1, - size: 10, - hasNext: false, - totalItems: 2, - totalPages: 1, - )); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index f8bd56d1..ca1923f9 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -4,57 +4,44 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; -class DebouncedCommunitiesService implements CommunitiesService { - DebouncedCommunitiesService({ - required CommunitiesService communitiesService, - this.debounceDuration = const Duration(milliseconds: 400), - }) : _communitiesService = communitiesService; +final class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService( + this._decoratee, { + this.debounceDuration = const Duration(milliseconds: 500), + }); - final CommunitiesService _communitiesService; + final CommunitiesService _decoratee; final Duration debounceDuration; Timer? _debounceTimer; - String _lastSearchQuery = ''; + Completer? _completer; @override Future getCommunity( LoadCommunitiesParam param, ) async { - if (param.search.isNotEmpty) { - return _getDebouncedCommunity(param); - } - - return _communitiesService.getCommunity(param); - } - - Future _getDebouncedCommunity( - LoadCommunitiesParam param, - ) async { - final completer = Completer(); - _debounceTimer?.cancel(); - _lastSearchQuery = param.search; + if (_completer != null && !_completer!.isCompleted) { + _completer!.completeError(Exception('Request cancelled by newer request')); + } + + _completer = Completer(); + final currentCompleter = _completer!; _debounceTimer = Timer(debounceDuration, () async { try { - if (_lastSearchQuery == param.search) { - final result = await _communitiesService.getCommunity(param); - if (!completer.isCompleted) { - completer.complete(result); - } - } else { - if (!completer.isCompleted) { - completer.complete(const CommunitiesPaginationModel.empty()); - } + final result = await _decoratee.getCommunity(param); + if (!currentCompleter.isCompleted) { + currentCompleter.complete(result); } } catch (error) { - if (!completer.isCompleted) { - completer.completeError(error); + if (!currentCompleter.isCompleted) { + currentCompleter.completeError(error); } } }); - return completer.future; + return currentCompleter.future; } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index e4202398..925a1cd0 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -15,10 +15,9 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future getCommunity(LoadCommunitiesParam param) async { - final projectUuid = await ProjectManager.getProjectUUID(); - if (projectUuid == null) throw APIException('Project UUID is not set'); - + Future getCommunity( + LoadCommunitiesParam param, + ) async { try { final response = await _httpService.get( path: await _makeUrl(), @@ -26,10 +25,12 @@ class RemoteCommunitiesService implements CommunitiesService { 'page': param.page, 'size': param.size, 'includeSpaces': param.includeSpaces, - if (param.search.isNotEmpty) 'search': param.search, + if (param.search.isNotEmpty && param.search != 'null') + 'search': param.search, }, expectedResponseModel: (json) { - return CommunitiesPaginationModel.fromJson(json as Map); + final data = json as Map; + return CommunitiesPaginationModel.fromJson(data); }, ); @@ -48,6 +49,9 @@ class RemoteCommunitiesService implements CommunitiesService { Future _makeUrl() async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is required'); - return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + return ApiEndpoints.getCommunityListv2.replaceAll( + '{projectId}', + projectUuid, + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart index f13ef8ba..a86783be 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -34,8 +34,8 @@ class CommunitiesPaginationModel extends Equatable { page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItems'] as int? ?? 0, - totalPages: json['totalPages'] as int? ?? 0, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index c6efad9e..ea0839f9 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain class CommunityModel extends Equatable { final String uuid; final String name; + final DateTime createdAt; + final DateTime updatedAt; + final String description; + final String externalId; final List spaces; const CommunityModel({ required this.uuid, required this.name, + required this.createdAt, + required this.updatedAt, + required this.description, + required this.externalId, required this.spaces, }); @@ -16,6 +24,10 @@ class CommunityModel extends Equatable { return CommunityModel( uuid: json['uuid'] as String, name: json['name'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + description: json['description'] as String, + externalId: json['externalId'] as String, spaces: (json['spaces'] as List) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 519e8ee7..d6007815 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,43 +1,38 @@ import 'package:equatable/equatable.dart'; -enum SpaceStatus { - active, - deleted, - parentDeleted; - - static SpaceStatus getValueFromString(String value) => switch (value) { - 'active' => active, - 'deleted' => deleted, - 'parentDeleted' => parentDeleted, - _ => active, - }; -} - class SpaceModel extends Equatable { final String uuid; + final DateTime createdAt; + final DateTime updatedAt; final String spaceName; final String icon; final List children; - final SpaceStatus status; + final SpaceModel? parent; const SpaceModel({ required this.uuid, + required this.createdAt, + required this.updatedAt, required this.spaceName, required this.icon, required this.children, - required this.status, + required this.parent, }); factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), spaceName: json['spaceName'] as String, icon: json['icon'] as String, children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], - status: SpaceStatus.getValueFromString(json['status'] as String), + parent: json['parent'] != null + ? SpaceModel.fromJson(json['parent'] as Map) + : null, ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 51322b52..b9902bd6 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -146,7 +146,6 @@ class _SpaceManagementCommunitiesTreeState Widget _buildCommunityTile(BuildContext context, CommunityModel community) { final spaces = community.spaces - .where((space) => space.status == SpaceStatus.active) .map((space) => _buildSpaceTile( space: space, community: community, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index d58d0f28..048f3000 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,6 +46,7 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; + static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = From 0a424300aa4d17ef8bfe15e237889cba2fbe25cf Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 12:46:54 +0300 Subject: [PATCH 28/86] Refactor ScheduleBloc and related components to use dynamic category handling for schedule events --- .../schedule_device/bloc/schedule_bloc.dart | 47 +++---------------- .../schedule_device/bloc/schedule_event.dart | 4 +- .../schedule_widgets/schedual_view.dart | 3 +- .../schedule_managment_ui.dart | 4 +- .../schedule_mode_selector.dart | 8 ++-- .../schedule_widgets/schedule_table.dart | 1 + 6 files changed, 20 insertions(+), 47 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index c4e731db..62213205 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -257,11 +257,11 @@ class ScheduleBloc extends Bloc { category: event.category, deviceId: deviceId, time: getTimeStampWithoutSeconds(dateTime).toString(), - code: 'switch_1', + code: event.category, value: event.functionOn, days: event.selectedDays); if (success) { - add(const ScheduleGetEvent(category: 'switch_1')); + add(ScheduleGetEvent(category: event.category)); } else { emit(const ScheduleError('Failed to add schedule')); } @@ -282,7 +282,7 @@ class ScheduleBloc extends Bloc { scheduleId: event.scheduleId, category: event.category, time: getTimeStampWithoutSeconds(dateTime).toString(), - function: Status(code: 'switch_1', value: event.functionOn), + function: Status(code: event.category, value: event.functionOn), days: event.selectedDays, ); final success = await DevicesManagementApi().editScheduleRecord( @@ -291,7 +291,9 @@ class ScheduleBloc extends Bloc { ); if (success) { - add(const ScheduleGetEvent(category: 'switch_1')); + add(ScheduleGetEvent( + category: event.category, + )); } else { emit(const ScheduleError('Failed to update schedule')); } @@ -312,7 +314,7 @@ class ScheduleBloc extends Bloc { final updatedSchedules = currentState.schedules.map((schedule) { if (schedule.scheduleId == event.scheduleId) { return schedule.copyWith( - function: Status(code: 'switch_1', value: event.functionOn), + function: Status(code: event.category, value: event.functionOn), enable: event.enable, ); } @@ -533,10 +535,6 @@ class ScheduleBloc extends Bloc { Duration.zero; } if (state is ScheduleLoaded) { - print('Updating existing state with fetched status'); - print('scheduleMode: $scheduleMode'); - print('countdownRemaining: $countdownRemaining'); - print('isCountdownActive: $isCountdownActive'); final currentState = state as ScheduleLoaded; emit(currentState.copyWith( scheduleMode: scheduleMode, @@ -586,35 +584,4 @@ class ScheduleBloc extends Bloc { dateTime.day, dateTime.hour, dateTime.minute); return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000; } - - // Future _updateScheduleEvent( - // StatusUpdatedScheduleEvent event, - // Emitter emit, - // ) async { - // if (state is ScheduleLoaded) { - // final currentState = state as ScheduleLoaded; - - // final updatedSchedules = currentState.schedules.map((schedule) { - // if (schedule.scheduleId == event.scheduleId) { - // return schedule.copyWith( - // function: Status(code: 'switch_1', value: event.functionOn), - // enable: event.enable, - // ); - // } - // return schedule; - // }).toList(); - - // bool success = await DevicesManagementApi().updateScheduleRecord( - // enable: event.enable, - // uuid: currentState.status.uuid, - // scheduleId: event.scheduleId, - // ); - - // if (success) { - // emit(currentState.copyWith(schedules: updatedSchedules)); - // } else { - // emit(currentState); - // } - // } - // } } diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 5099679c..7ec144fe 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -121,15 +121,17 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent { final String scheduleId; final bool functionOn; final bool enable; + final String category; const ScheduleUpdateEntryEvent({ required this.scheduleId, required this.functionOn, required this.enable, + required this.category, }); @override - List get props => [scheduleId, functionOn, enable]; + List get props => [scheduleId, functionOn, enable, category]; } class UpdateScheduleModeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 2ae5b869..2fa34559 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -15,7 +15,7 @@ class BuildScheduleView extends StatelessWidget { const BuildScheduleView( {super.key, required this.deviceUuid, required this.category}); final String deviceUuid; - final String category; + final String category; @override Widget build(BuildContext context) { @@ -51,6 +51,7 @@ class BuildScheduleView extends StatelessWidget { const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) ScheduleManagementUI( + category: category, deviceUuid: deviceUuid, onAddSchedule: () async { final entry = await ScheduleDialogHelper diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index b60f00b9..8f871ce4 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -7,11 +7,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart'; class ScheduleManagementUI extends StatelessWidget { final String deviceUuid; final VoidCallback onAddSchedule; + final String category; const ScheduleManagementUI({ super.key, required this.deviceUuid, required this.onAddSchedule, + this.category = 'switch_1', }); @override @@ -42,7 +44,7 @@ class ScheduleManagementUI extends StatelessWidget { ), ), const SizedBox(height: 20), - ScheduleTableWidget(deviceUuid: deviceUuid), + ScheduleTableWidget(deviceUuid: deviceUuid, category: category), ], ); } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart index 2bcc0957..25bf7f2c 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -39,10 +39,10 @@ class ScheduleModeSelector extends StatelessWidget { context, 'Countdown', ScheduleModes.countdown, currentMode), _buildRadioTile( context, 'Schedule', ScheduleModes.schedule, currentMode), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, currentMode), - _buildRadioTile( - context, 'Inching', ScheduleModes.inching, currentMode), + // _buildRadioTile( + // context, 'Circulate', ScheduleModes.circulate, currentMode), + // _buildRadioTile( + // context, 'Inching', ScheduleModes.inching, currentMode), ], ), ], diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart index 97ca03e1..98ae0515 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -164,6 +164,7 @@ class _ScheduleTableView extends StatelessWidget { onTap: () { context.read().add( ScheduleUpdateEntryEvent( + category: schedule.category, scheduleId: schedule.scheduleId, functionOn: schedule.function.value, enable: !schedule.enable, From b79ab06d9523ff11058be8e12a1c1aed30cf2a9d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:46:17 +0300 Subject: [PATCH 29/86] shows a loading indicator when loading. --- .../debounced_communities_service.dart | 6 +-- .../presentation/bloc/communities_bloc.dart | 12 +++++- .../space_management_communities_tree.dart | 41 +++++++++++++------ ...e_management_sidebar_communities_list.dart | 2 - 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index ca1923f9..e512679b 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -14,7 +14,7 @@ final class DebouncedCommunitiesService implements CommunitiesService { final Duration debounceDuration; Timer? _debounceTimer; - Completer? _completer; + late Completer? _completer; @override Future getCommunity( @@ -22,10 +22,6 @@ final class DebouncedCommunitiesService implements CommunitiesService { ) async { _debounceTimer?.cancel(); - if (_completer != null && !_completer!.isCompleted) { - _completer!.completeError(Exception('Request cancelled by newer request')); - } - _completer = Completer(); final currentCompleter = _completer!; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 53eb9d3f..ef91baa2 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -24,9 +24,13 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + emit( + state.copyWith(status: CommunitiesStatus.loading), + ); - final paginationResponse = await _communitiesService.getCommunity(event.param); + final paginationResponse = await _communitiesService.getCommunity( + event.param, + ); emit( CommunitiesState( @@ -35,6 +39,7 @@ class CommunitiesBloc extends Bloc { hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, + isLoadingMore: false, ), ); } on APIException catch (e) { @@ -65,6 +70,7 @@ class CommunitiesBloc extends Bloc { emit( state.copyWith( + status: CommunitiesStatus.success, communities: updatedCommunities, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, @@ -84,6 +90,7 @@ class CommunitiesBloc extends Bloc { ) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.message, ), @@ -93,6 +100,7 @@ class CommunitiesBloc extends Bloc { void _onError(Object e, Emitter emit) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index b9902bd6..4501cf7e 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -42,11 +42,9 @@ class _SpaceManagementCommunitiesTreeState } void _onSearchChanged(String searchQuery) { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: searchQuery.trim(), - )), - ); + context + .read() + .add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim()))); } void _onLoadMore() { @@ -80,6 +78,13 @@ class _SpaceManagementCommunitiesTreeState CommunitiesStatus.success => _buildCommunitiesTree(context, state), CommunitiesStatus.failure => _buildErrorState(context, state), }, + Visibility( + visible: state.isLoadingMore, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator()), + ), + ), ], ), ); @@ -132,14 +137,24 @@ class _SpaceManagementCommunitiesTreeState } return Expanded( - child: SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, + child: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return _buildCommunityTile(context, state.communities[index]); + }, + ), + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const Center(child: CircularProgressIndicator()), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index 68119dcd..40766be5 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -37,7 +37,6 @@ class _SpaceManagementSidebarCommunitiesListState void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) { - // Trigger pagination when user is close to the bottom if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { widget.onLoadMore!(); } @@ -67,7 +66,6 @@ class _SpaceManagementSidebarCommunitiesListState @override Widget build(BuildContext context) { - // Calculate item count including loading indicator final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); return SingleChildScrollView( From f02788eaa5835255e7707f266e53cea6dd860c11 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 14:58:38 +0300 Subject: [PATCH 30/86] implemented create community feature. --- .../domain/models/community_model.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 8 + .../presentation/bloc/communities_event.dart | 9 + .../widgets/create_community_dialog.dart | 181 ------------------ .../space_management_communities_tree.dart | 33 ++-- .../remote_create_community_service.dart | 50 +++-- .../domain/param/create_community_param.dart | 8 +- .../presentation/create_community_dialog.dart | 61 ++++++ .../create_community_dialog_widget.dart | 144 ++++++++++++++ .../create_community_name_text_field.dart | 48 +++++ 10 files changed, 338 insertions(+), 208 deletions(-) delete mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index ea0839f9..344dbff5 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -27,8 +27,8 @@ class CommunityModel extends Equatable { createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), description: json['description'] as String, - externalId: json['externalId'] as String, - spaces: (json['spaces'] as List) + externalId: json['externalId']?.toString() ?? '', + spaces: (json['spaces'] as List? ?? []) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index ef91baa2..0f754b06 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,6 +15,7 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); + on(_onInsertCommunity); } final CommunitiesService _communitiesService; @@ -106,4 +107,11 @@ class CommunitiesBloc extends Bloc { ), ); } + + void _onInsertCommunity( + InsertCommunity event, + Emitter emit, + ) { + emit(state.copyWith(communities: [event.community, ...state.communities])); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ae4d86bf..cd14fa3d 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,3 +22,12 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } + +final class InsertCommunity extends CommunitiesEvent { + const InsertCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart deleted file mode 100644 index fd8a0a68..00000000 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class CreateCommunityDialog extends StatefulWidget { - final void Function(String name) onCreateCommunity; - final String? initialName; - final Widget title; - - const CreateCommunityDialog({ - super.key, - required this.onCreateCommunity, - required this.title, - this.initialName, - }); - - @override - State createState() => _CreateCommunityDialogState(); -} - -class _CreateCommunityDialogState extends State { - late final TextEditingController _nameController; - - @override - void initState() { - _nameController = TextEditingController(text: widget.initialName ?? ''); - super.initState(); - } - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CommunityDialogBloc([]), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: ColorsManager.transparentColor, - child: Stack( - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.3, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.25), - blurRadius: 20, - spreadRadius: 5, - offset: const Offset(0, 5), - ), - ], - ), - child: SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) { - var isNameValid = true; - var isNameEmpty = false; - - if (state is CommunityNameValidationState) { - isNameValid = state.isNameValid; - isNameEmpty = state.isNameEmpty; - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context).textTheme.headlineMedium!, - child: widget.title, - ), - const SizedBox(height: 18), - TextField( - controller: _nameController, - onChanged: (value) { - context - .read() - .add(ValidateCommunityNameEvent(value)); - }, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: 'Please enter the community name', - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isNameValid && !isNameEmpty - ? ColorsManager.boxColor - : ColorsManager.red, - width: 1, - ), - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, - ), - ), - ), - ), - if (!isNameValid) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name already exists.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - if (isNameEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name should not be empty.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () => Navigator.of(context).pop(), - ), - ), - const SizedBox(width: 16), - Expanded( - child: DefaultButton( - onPressed: () { - if (isNameValid && !isNameEmpty) { - widget.onCreateCommunity( - _nameController.text.trim(), - ); - Navigator.of(context).pop(); - } - }, - backgroundColor: isNameValid && !isNameEmpty - ? ColorsManager.secondaryColor - : ColorsManager.lightGrayColor, - borderRadius: 10, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), - ), - ], - ), - ], - ); - }, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 4501cf7e..efafdd85 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -7,10 +7,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -220,15 +220,17 @@ class _SpaceManagementCommunitiesTreeState ); } - void _onAddCommunity(BuildContext context) => context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - true - ? _clearSelection(context) - : _showCreateCommunityDialog(context); + void _onAddCommunity(BuildContext context) { + context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + false + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + } void _clearSelection(BuildContext context) => context.read().add( @@ -237,9 +239,16 @@ class _SpaceManagementCommunitiesTreeState void _showCreateCommunityDialog(BuildContext context) => showDialog( context: context, - builder: (context) => CreateCommunityDialog( + builder: (_) => CreateCommunityDialog( title: const Text('Community Name'), - onCreateCommunity: (name) {}, + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, ), ); } diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index be83124b..bd91f6ce 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart'; @@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService { Future createCommunity(CreateCommunityParam param) async { try { final response = await _httpService.post( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + path: await _makeUrl(), + body: { + 'name': param.name, + 'description': param.description, + }, + expectedResponseModel: (data) { + final json = data as Map; + if (json['success'] == true) { + return CommunityModel.fromJson( + json['data'] as Map, + ); + } + return null; + }, ); + + if (response == null) { + throw APIException( + _getErrorMessageFromBody(response as Map?), + ); + } return response; } on DioException catch (e) { final message = e.response?.data as Map?; - final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; - final formattedErrorMessage = [ - _defaultErrorMessage, - errorMessage, - ].join(': '); - throw APIException(formattedErrorMessage); + throw APIException(_getErrorMessageFromBody(message)); } catch (e) { final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); throw APIException(formattedErrorMessage); } } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) { + return _defaultErrorMessage; + } + final error = body['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities'; + } } diff --git a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart index 3d7c203b..68a9fa11 100644 --- a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart +++ b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; class CreateCommunityParam extends Equatable { - const CreateCommunityParam({required this.name}); - + const CreateCommunityParam({ + required this.name, + this.description = '', + }); + final String name; + final String description; @override List get props => [name]; diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart new file mode 100644 index 00000000..8c1d474d --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class CreateCommunityDialog extends StatelessWidget { + final void Function(CommunityModel community) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), + child: BlocListener( + listener: (context, state) { + switch (state) { + case CreateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case CreateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Community created successfully')), + ); + onCreateCommunity.call(community); + break; + case CreateCommunityFailure(:final message): + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + break; + default: + break; + } + }, + child: CreateCommunityDialogWidget( + title: title, + initialName: initialName, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart new file mode 100644 index 00000000..49d43ae6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialogWidget extends StatefulWidget { + final String? initialName; + final Widget title; + + const CreateCommunityDialogWidget({ + super.key, + required this.title, + this.initialName, + }); + + @override + State createState() => + _CreateCommunityDialogWidgetState(); +} + +class _CreateCommunityDialogWidgetState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withValues(alpha: 0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (state case CreateCommunityFailure(:final message)) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* $message', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Row _buildActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + _buildCreateCommunityButton(context), + ], + ); + } + + Widget _buildCreateCommunityButton(BuildContext context) { + return Expanded( + child: DefaultButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _onSubmit(context); + } + }, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } + + void _onSubmit(BuildContext context) { + if (_formKey.currentState?.validate() ?? false) { + context.read().add( + CreateCommunity( + CreateCommunityParam( + name: _nameController.text.trim(), + ), + ), + ); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart new file mode 100644 index 00000000..d42474d5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CreateCommunityNameTextField extends StatelessWidget { + const CreateCommunityNameTextField({ + required this.nameController, + super.key, + }); + + final TextEditingController nameController; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: nameController, + validator: _validator, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(ColorsManager.boxColor), + focusedBorder: _buildBorder(), + focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error), + errorBorder: _buildBorder(Theme.of(context).colorScheme.error), + ), + ); + } + + String? _validator(String? value) { + if (value == null || value.isEmpty) { + return '*Name should not be empty.'; + } + + return null; + } + + InputBorder _buildBorder([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5), + width: 1, + ), + ); + } +} From 09446844b0e689b829ad586ac6f09cd1eea46e1c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:11:38 +0300 Subject: [PATCH 31/86] reverted initializing the new space management page in the router, to avoid any confusion with the QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From a793cc3967952b5cb4b0d1d46acfcb31a7795654 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Sun, 22 Jun 2025 15:24:53 +0300 Subject: [PATCH 32/86] fix it and add lock to open when press (as loved simple animation) with adding the timer as circle --- .../door_lock/bloc/door_lock_bloc.dart | 97 +++++-------------- .../door_lock/widget/door_button.dart | 89 +++++------------ 2 files changed, 48 insertions(+), 138 deletions(-) diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart index f83ced1a..f6cebe4d 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart @@ -1,5 +1,3 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,45 +14,38 @@ class DoorLockBloc extends Bloc { DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) { on(_onFetchDeviceStatus); - //on(_onDoorLockControl); on(_updateLock); on(_onFactoryReset); on(_onStatusUpdated); } - _listenToChanges(deviceId) { + void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final data = event.snapshot.value; + if (data is Map) { + final statusData = data['status'] as List? ?? []; + final statusList = statusData.map((item) { + return Status(code: item['code'], value: item['value']); + }).toList(); - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - DoorLockStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final model = + DoorLockStatusModel.fromJson(data['productUuid'], statusList); + if (!isClosed) { + add(StatusUpdated(model)); + } } }); } catch (_) {} } void _onStatusUpdated(StatusUpdated event, Emitter emit) { - emit(DoorLockStatusLoading()); - deviceStatus = event.deviceStatus; emit(DoorLockStatusLoaded(deviceStatus)); } - FutureOr _onFetchDeviceStatus( + Future _onFetchDeviceStatus( DoorLockFetchStatus event, Emitter emit) async { emit(DoorLockStatusLoading()); try { @@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc { deviceStatus = DoorLockStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); - emit(DoorLockStatusLoaded(deviceStatus)); } catch (e) { emit(DoorLockControlError(e.toString())); } } - FutureOr _updateLock( + Future _updateLock( UpdateLockEvent event, Emitter emit) async { final oldValue = deviceStatus.normalOpenSwitch; deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue); @@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc { try { final response = await DevicesManagementApi.openDoorLock(deviceId); - if (!response) { _revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit); } @@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc { } } - Future _runDebounce({ - required String deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - }) async { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - if (!response) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit( - String deviceId, - String code, - dynamic oldValue, - Emitter emit, - ) { + void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, + Emitter emit) { _updateLocalValue(code, oldValue); emit(DoorLockStatusLoaded(deviceStatus)); emit(const DoorLockControlError('Failed to control the device.')); @@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc { void _updateLocalValue(String code, dynamic value) { switch (code) { - case 'reverse_lock': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(reverseLock: value); - } - break; case 'normal_open_switch': if (value is bool) { deviceStatus = deviceStatus.copyWith(normalOpenSwitch: value); } break; + case 'reverse_lock': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(reverseLock: value); + } + break; default: break; } emit(DoorLockStatusLoaded(deviceStatus)); } - dynamic _getValueByCode(String code) { - switch (code) { - case 'reverse_lock': - return deviceStatus.reverseLock; - case 'normal_open_switch': - return deviceStatus.normalOpenSwitch; - default: - return null; - } - } - - FutureOr _onFactoryReset( + Future _onFactoryReset( DoorLockFactoryReset event, Emitter emit) async { emit(DoorLockStatusLoading()); try { diff --git a/lib/pages/device_managment/door_lock/widget/door_button.dart b/lib/pages/device_managment/door_lock/widget/door_button.dart index e8e3066e..c1ac7bc0 100644 --- a/lib/pages/device_managment/door_lock/widget/door_button.dart +++ b/lib/pages/device_managment/door_lock/widget/door_button.dart @@ -8,7 +8,7 @@ import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_st import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class DoorLockButton extends StatefulWidget { +class DoorLockButton extends StatelessWidget { const DoorLockButton({ super.key, required this.doorLock, @@ -18,70 +18,28 @@ class DoorLockButton extends StatefulWidget { final AllDevicesModel doorLock; final DoorLockStatusModel smartDoorModel; - @override - State createState() => - _DoorLockButtonState(smartDoorModel: smartDoorModel); -} - -class _DoorLockButtonState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _animation; - DoorLockStatusModel smartDoorModel; - - _DoorLockButtonState({required this.smartDoorModel}); - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 500), - ); - _animation = Tween(begin: 0, end: 1).animate(_animationController) - ..addListener(() { - setState(() {}); - }); - - if (smartDoorModel.unlockRequest > 0) { - _animationController.reverse(from: 1); - } - } - - @override - void didUpdateWidget(covariant DoorLockButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.smartDoorModel.normalOpenSwitch != - widget.smartDoorModel.normalOpenSwitch) { - setState(() { - smartDoorModel = widget.smartDoorModel; - }); - - if (smartDoorModel.unlockRequest > 0) { - _animationController.forward(from: 0); - } else { - _animationController.reverse(from: 1); - } - } - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); + double _calculateProgress() { + final value = smartDoorModel.unlockRequest; + if (value <= 0 || value > 30) return 0; + return value / 30.0; } @override Widget build(BuildContext context) { + final progress = _calculateProgress(); + final isEnabled = smartDoorModel.unlockRequest > 0; + return SizedBox( width: 255, height: 255, child: InkWell( - onTap: () { - _animationController.forward(from: 0); - BlocProvider.of(context) - .add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch)); - }, + onTap: isEnabled + ? () { + BlocProvider.of(context).add( + UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch), + ); + } + : null, child: Container( width: 255, height: 255, @@ -115,15 +73,16 @@ class _DoorLockButtonState extends State ), ), ), - SizedBox.expand( - child: CircularProgressIndicator( - value: _animation.value, - strokeWidth: 8, - backgroundColor: Colors.transparent, - valueColor: const AlwaysStoppedAnimation( - ColorsManager.primaryColor), + if (progress > 0) + SizedBox.expand( + child: CircularProgressIndicator( + value: progress, + strokeWidth: 8, + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation( + ColorsManager.primaryColor), + ), ), - ), ], ), ), From 28ac911f3f6f939b3e20dc140f56dd012086f1e0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:30:47 +0300 Subject: [PATCH 33/86] Accomodated for null values in `SpaceModel`. --- .../communities/domain/models/space_model.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index d6007815..36943adb 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart'; class SpaceModel extends Equatable { final String uuid; - final DateTime createdAt; - final DateTime updatedAt; + final DateTime? createdAt; + final DateTime? updatedAt; final String spaceName; final String icon; final List children; @@ -21,11 +21,11 @@ class SpaceModel extends Equatable { factory SpaceModel.fromJson(Map json) { return SpaceModel( - uuid: json['uuid'] as String, - createdAt: DateTime.parse(json['createdAt'] as String), - updatedAt: DateTime.parse(json['updatedAt'] as String), - spaceName: json['spaceName'] as String, - icon: json['icon'] as String, + uuid: json['uuid'] as String? ?? '', + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''), + spaceName: json['spaceName'] as String? ?? '', + icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg', children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? From 48d7ab430f524e242c2f46523da1f92c65e773e3 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 22 Jun 2025 15:35:46 +0300 Subject: [PATCH 34/86] refactor: rename productName to deviceNameOrProductName in search functionality --- .../device_managment_bloc.dart | 72 ++++++++++--------- .../device_managment_event.dart | 6 +- .../widgets/device_search_filters.dart | 4 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 05e82f1f..98b0c195 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -40,17 +40,18 @@ class DeviceManagementBloc List devices = []; _devices.clear(); var spaceBloc = event.context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; if (spaceBloc.state.selectedCommunities.isEmpty) { - devices = await DevicesManagementApi().fetchDevices('', '', projectUuid); + devices = + await DevicesManagementApi().fetchDevices('', '', projectUuid); } else { for (var community in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; for (var space in spacesList) { - devices.addAll(await DevicesManagementApi().fetchDevices( - community, space, projectUuid)); + devices.addAll(await DevicesManagementApi() + .fetchDevices(community, space, projectUuid)); } } } @@ -100,7 +101,7 @@ class DeviceManagementBloc )); if (currentProductName.isNotEmpty) { - add(SearchDevices(productName: currentProductName)); + add(SearchDevices(deviceNameOrProductName: currentProductName)); } } } @@ -269,34 +270,41 @@ class DeviceManagementBloc return 'All'; } } - void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) && - (event.productName == null || event.productName!.isEmpty)) { + (event.deviceNameOrProductName == null || + event.deviceNameOrProductName!.isEmpty)) { currentProductName = ''; - if (state is DeviceManagementFiltered) { - add(FilterDevices(_getFilterFromIndex(_selectedIndex))); - } else { - return; - } + _filteredDevices = List.from(_devices); + emit(DeviceManagementLoaded( + devices: _devices, + selectedIndex: _selectedIndex, + onlineCount: _onlineCount, + offlineCount: _offlineCount, + lowBatteryCount: _lowBatteryCount, + selectedDevice: null, + isControlButtonEnabled: false, + )); + return; } - - if (event.productName == currentProductName && + if (event.deviceNameOrProductName == currentProductName && event.community == currentCommunity && event.unitName == currentUnitName && event.searchField) { return; } - currentProductName = event.productName ?? ''; + currentProductName = event.deviceNameOrProductName ?? ''; currentCommunity = event.community; currentUnitName = event.unitName; - List devicesToSearch = _filteredDevices; + List devicesToSearch = _devices; if (devicesToSearch.isNotEmpty) { + final searchText = event.deviceNameOrProductName?.toLowerCase() ?? ''; + final filteredDevices = devicesToSearch.where((device) { final matchesCommunity = event.community == null || event.community!.isEmpty || @@ -304,31 +312,25 @@ class DeviceManagementBloc ?.toLowerCase() .contains(event.community!.toLowerCase()) ?? false); + final matchesUnit = event.unitName == null || event.unitName!.isEmpty || (device.spaces != null && - device.spaces!.isNotEmpty && - device.spaces![0].spaceName! - .toLowerCase() - .contains(event.unitName!.toLowerCase())); - final matchesProductName = event.productName == null || - event.productName!.isEmpty || - (device.name - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); - final matchesDeviceName = event.productName == null || - event.productName!.isEmpty || - (device.categoryName - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); + device.spaces!.any((space) => + space.spaceName != null && + space.spaceName! + .toLowerCase() + .contains(event.unitName!.toLowerCase()))); - return matchesCommunity && - matchesUnit && - (matchesProductName || matchesDeviceName); + final matchesSearchText = searchText.isEmpty || + (device.name?.toLowerCase().contains(searchText) ?? false) || + (device.productName?.toLowerCase().contains(searchText) ?? false); + + return matchesCommunity && matchesUnit && matchesSearchText; }).toList(); + _filteredDevices = filteredDevices; + emit(DeviceManagementFiltered( filteredDevices: filteredDevices, selectedIndex: _selectedIndex, diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart index 9928c50e..5292de0e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart @@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent { class SearchDevices extends DeviceManagementEvent { final String? community; final String? unitName; - final String? productName; + final String? deviceNameOrProductName; final bool searchField; const SearchDevices({ this.community, this.unitName, - this.productName, + this.deviceNameOrProductName, this.searchField = false, }); @override - List get props => [community, unitName, productName]; + List get props => [community, unitName, deviceNameOrProductName]; } class SelectDevice extends DeviceManagementEvent { diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index 6440d18f..7e998ed6 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State controller: controller, onSubmitted: () { final searchDevicesEvent = SearchDevices( - productName: _productNameController.text, + deviceNameOrProductName: _productNameController.text, unitName: _unitNameController.text, searchField: true, ); @@ -68,7 +68,7 @@ class _DeviceSearchFiltersState extends State onSearch: () => context.read().add( SearchDevices( unitName: _unitNameController.text, - productName: _productNameController.text, + deviceNameOrProductName: _productNameController.text, searchField: true, ), ), From 41d4fbb555585a3cda5da9221d2a7faa06543638 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 16:00:20 +0300 Subject: [PATCH 35/86] Extracted pagination data into a generic DTO. --- .../shared/models/paginated_data_model.dart | 45 ++++++++++++ .../debounced_communities_service.dart | 1 - .../services/remote_communities_service.dart | 4 +- .../models/communities_pagination_model.dart | 69 ------------------- .../domain/services/communities_service.dart | 5 +- .../presentation/bloc/communities_bloc.dart | 4 +- 6 files changed, 53 insertions(+), 75 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart delete mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart new file mode 100644 index 00000000..e37cd0a1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; + +class PaginatedDataModel extends Equatable { + const PaginatedDataModel({ + required this.data, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List data; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + factory PaginatedDataModel.fromJson( + Map json, + T Function(Map) fromJsonT, + ) { + return PaginatedDataModel( + data: (json['data'] as List? ?? []) + .map((e) => fromJsonT(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, + ); + } + + @override + List get props => [ + data, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index e512679b..a97e8524 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 925a1cd0..b58961a6 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -30,7 +30,7 @@ class RemoteCommunitiesService implements CommunitiesService { }, expectedResponseModel: (json) { final data = json as Map; - return CommunitiesPaginationModel.fromJson(data); + return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); }, ); diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart deleted file mode 100644 index a86783be..00000000 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; - -class CommunitiesPaginationModel extends Equatable { - const CommunitiesPaginationModel({ - required this.communities, - required this.page, - required this.size, - required this.hasNext, - required this.totalItems, - required this.totalPages, - }); - - final List communities; - final int page; - final int size; - final bool hasNext; - final int totalItems; - final int totalPages; - - const CommunitiesPaginationModel.empty() - : communities = const [], - page = 1, - size = 25, - hasNext = false, - totalItems = 0, - totalPages = 0; - - factory CommunitiesPaginationModel.fromJson(Map json) { - return CommunitiesPaginationModel( - communities: (json['data'] as List? ?? []) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), - page: json['page'] as int? ?? 1, - size: json['size'] as int? ?? 25, - hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItem'] as int? ?? 0, - totalPages: json['totalPage'] as int? ?? 0, - ); - } - - CommunitiesPaginationModel copyWith({ - List? communities, - int? page, - int? size, - bool? hasNext, - int? totalItems, - int? totalPages, - }) { - return CommunitiesPaginationModel( - communities: communities ?? this.communities, - page: page ?? this.page, - size: size ?? this.size, - hasNext: hasNext ?? this.hasNext, - totalItems: totalItems ?? this.totalItems, - totalPages: totalPages ?? this.totalPages, - ); - } - - @override - List get props => [ - communities, - page, - size, - hasNext, - totalItems, - totalPages, - ]; -} diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index 564dc4da..baa84590 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,9 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +typedef CommunitiesPaginationModel = PaginatedDataModel; + abstract class CommunitiesService { Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0f754b06..9094a632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -36,7 +36,7 @@ class CommunitiesBloc extends Bloc { emit( CommunitiesState( status: CommunitiesStatus.success, - communities: paginationResponse.communities, + communities: paginationResponse.data, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, @@ -67,7 +67,7 @@ class CommunitiesBloc extends Bloc { final paginationResponse = await _communitiesService.getCommunity(param); final updatedCommunities = List.from(state.communities) - ..addAll(paginationResponse.communities); + ..addAll(paginationResponse.data); emit( state.copyWith( From 27349a6cc0956878a0b590f8ffd66e2d7245bc7e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 09:24:53 +0300 Subject: [PATCH 36/86] Implemented PR notes by extracting widgets into their own classes. --- lib/common/widgets/app_loading_indicator.dart | 10 + .../shared/models/paginated_data_model.dart | 6 +- .../services/remote_communities_service.dart | 8 +- .../domain/models/community_model.dart | 5 + .../communities_tree_failure_widget.dart | 38 +++ ...communities_tree_search_result_widget.dart | 22 ++ .../space_management_communities_tree.dart | 240 ++++-------------- ...ement_communities_tree_community_tile.dart | 45 ++++ ...anagement_communities_tree_space_tile.dart | 56 ++++ .../space_management_sidebar_header.dart | 46 +++- .../create_community_dialog_widget.dart | 2 +- 11 files changed, 271 insertions(+), 207 deletions(-) create mode 100644 lib/common/widgets/app_loading_indicator.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart new file mode 100644 index 00000000..bc811c56 --- /dev/null +++ b/lib/common/widgets/app_loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart index e37cd0a1..ac35975d 100644 --- a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -19,12 +19,10 @@ class PaginatedDataModel extends Equatable { factory PaginatedDataModel.fromJson( Map json, - T Function(Map) fromJsonT, + List Function(List) fromJsonList, ) { return PaginatedDataModel( - data: (json['data'] as List? ?? []) - .map((e) => fromJsonT(e as Map)) - .toList(), + data: fromJsonList(json['data'] as List), page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index b58961a6..cc842de8 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -28,10 +28,10 @@ class RemoteCommunitiesService implements CommunitiesService { if (param.search.isNotEmpty && param.search != 'null') 'search': param.search, }, - expectedResponseModel: (json) { - final data = json as Map; - return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); - }, + expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson( + json as Map, + CommunityModel.fromJsonList, + ), ); return response; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index 344dbff5..37f131b3 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -33,6 +33,11 @@ class CommunityModel extends Equatable { .toList(), ); } + static List fromJsonList(List json) { + return json + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(); + } @override List get props => [uuid, name, spaces]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart new file mode 100644 index 00000000..cfd32f52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; + +class CommunitiesTreeFailureWidget extends StatelessWidget { + const CommunitiesTreeFailureWidget({super.key, this.errorMessage}); + + final String? errorMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.read().add( + LoadCommunities( + LoadCommunitiesParam( + search: context.read().state.searchQuery, + ), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart new file mode 100644 index 00000000..bfc9e30e --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget { + const EmptyCommunitiesTreeSearchResultWidget({ + required this.searchQuery, + super.key, + }); + + final String searchQuery; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "$searchQuery"', + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index efafdd85..1adf9911 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -31,16 +29,6 @@ class _SpaceManagementCommunitiesTreeState super.initState(); } - bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { - final selectedSpace = - context.read().state.selectedSpace; - final isSpaceSelected = selectedSpace?.uuid == space.uuid; - final anySubSpaceIsSelected = space.children.any( - (child) => _isSpaceOrChildSelected(context, child), - ); - return isSpaceSelected || anySubSpaceIsSelected; - } - void _onSearchChanged(String searchQuery) { context .read() @@ -51,67 +39,32 @@ class _SpaceManagementCommunitiesTreeState context.read().add(const LoadMoreCommunities()); } - static const _width = 300.0; - @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - return Container( - width: _width, - decoration: subSectionContainerDecoration, - child: Column( - children: [ - SpaceManagementSidebarHeader( - onAddCommunity: () => _onAddCommunity(context), - ), - CustomSearchBar( - onSearchChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - switch (state.status) { - CommunitiesStatus.initial => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => state.communities.isEmpty - ? const Center(child: CircularProgressIndicator()) - : _buildCommunitiesTree(context, state), - CommunitiesStatus.success => _buildCommunitiesTree(context, state), - CommunitiesStatus.failure => _buildErrorState(context, state), - }, - Visibility( - visible: state.isLoadingMore, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Center(child: CircularProgressIndicator()), - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildErrorState(BuildContext context, CommunitiesState state) { - return Expanded( - child: Center( + builder: (context, state) => Container( + width: 320, + decoration: subSectionContainerDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - state.errorMessage ?? 'Something went wrong', - textAlign: TextAlign.center, + const SpaceManagementSidebarHeader(), + CustomSearchBar( + onSearchChanged: _onSearchChanged, ), const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: state.searchQuery, - )), - ); - }, - child: const Text('Retry'), + switch (state.status) { + CommunitiesStatus.initial => const AppLoadingIndicator(), + CommunitiesStatus.loading => state.communities.isEmpty + ? const AppLoadingIndicator() + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => CommunitiesTreeFailureWidget( + errorMessage: state.errorMessage, + ), + }, + Visibility( + visible: state.isLoadingMore, + child: const AppLoadingIndicator(), ), ], ), @@ -123,132 +76,37 @@ class _SpaceManagementCommunitiesTreeState BuildContext context, CommunitiesState state, ) { - if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { - return Expanded( - child: Center( - child: Text( - state.searchQuery.isEmpty - ? 'No communities found' - : 'No communities found for "${state.searchQuery}"', - textAlign: TextAlign.center, - ), - ), - ); - } + final communitiesIsEmpty = state.communities.isEmpty; + final statusIsSuccess = state.status == CommunitiesStatus.success; return Expanded( - child: Stack( - children: [ - SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, - ), - if (state.status == CommunitiesStatus.loading && - state.communities.isNotEmpty) - ColoredBox( - color: Colors.white.withValues(alpha: 0.7), - child: const Center(child: CircularProgressIndicator()), + child: Visibility( + visible: statusIsSuccess && communitiesIsEmpty, + replacement: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return SpaceManagementCommunitiesTreeCommunityTile( + community: state.communities[index], + ); + }, ), - ], - ), - ); - } - - Widget _buildCommunityTile(BuildContext context, CommunityModel community) { - final spaces = community.spaces - .map((space) => _buildSpaceTile( - space: space, - community: community, - context: context, - )) - .toList(); - return CommunityTile( - title: community.name, - key: ValueKey(community.uuid), - isSelected: context - .watch() - .state - .selectedCommunity - ?.uuid == - community.uuid, - isExpanded: false, - onItemSelected: () { - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - onExpansionChanged: (title, expanded) {}, - children: spaces, - ); - } - - Widget _buildSpaceTile({ - required SpaceModel space, - required CommunityModel community, - required BuildContext context, - }) { - final spaceIsExpanded = _isSpaceOrChildSelected(context, space); - final isSelected = - context.watch().state.selectedSpace?.uuid == - space.uuid; - return Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: SpaceTile( - title: space.spaceName, - key: ValueKey(space.uuid), - isSelected: isSelected, - initiallyExpanded: spaceIsExpanded, - onExpansionChanged: (expanded) {}, - onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), - ), - children: space.children - .map( - (childSpace) => _buildSpaceTile( - space: childSpace, - community: community, - context: context, + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const AppLoadingIndicator(), ), - ) - .toList(), + ], + ), + child: EmptyCommunitiesTreeSearchResultWidget( + searchQuery: state.searchQuery, + ), ), ); } - - void _onAddCommunity(BuildContext context) { - context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - false - ? _clearSelection(context) - : _showCreateCommunityDialog(context); - } - - void _clearSelection(BuildContext context) => - context.read().add( - const ClearCommunitiesTreeSelectionEvent(), - ); - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart new file mode 100644 index 00000000..736a499f --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart'; + +class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeCommunityTile({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaces = community.spaces + .map( + (space) => SpaceManagementCommunitiesTreeSpaceTile( + space: space, + community: community, + ), + ) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart new file mode 100644 index 00000000..dcd44ac8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; + +class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeSpaceTile({ + required this.space, + required this.community, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => SpaceManagementCommunitiesTreeSpaceTile( + space: childSpace, + community: community, + ), + ) + .toList(), + ), + ); + } + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index cf40f95c..25c094db 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementSidebarHeader extends StatelessWidget { - const SpaceManagementSidebarHeader({ - required this.onAddCommunity, - super.key, - }); - - final void Function() onAddCommunity; + const SpaceManagementSidebarHeader({super.key}); @override Widget build(BuildContext context) { @@ -27,10 +26,43 @@ class SpaceManagementSidebarHeader extends StatelessWidget { ), ), SpaceManagementSidebarAddCommunityButton( - onTap: onAddCommunity, + onTap: () => _onAddCommunity(context), ), ], ), ); } + + void _onAddCommunity(BuildContext context) { + final bloc = context.read(); + final selectedCommunity = bloc.state.selectedCommunity; + final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false; + + if (isSelected) { + _clearSelection(context); + } else { + _showCreateCommunityDialog(context); + } + } + + void _clearSelection(BuildContext context) { + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + } + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart index 49d43ae6..ab9f7b9a 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -99,7 +99,7 @@ class _CreateCommunityDialogWidgetState extends State Date: Mon, 23 Jun 2025 10:01:01 +0300 Subject: [PATCH 37/86] Refactor empty state widget to use a container for better layout control --- lib/pages/common/custom_table.dart | 55 ++++++++++++++++-------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 625c59c2..fb8237b7 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -179,31 +179,36 @@ class _DynamicTableState extends State { ); } - Widget _buildEmptyState() => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox(height: 15), - Text( - widget.tableName == 'AccessManagement' - ? 'No Password ' - : 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: ColorsManager.grayColor), - ) - ], - ), - ], - ), - ], + Widget _buildEmptyState() => Container( + height: widget.size.height, + color: ColorsManager.whiteColors, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox(height: 15), + Text( + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + SizedBox(height: widget.size.height * 0.5), + ], + ), ); Widget _buildSelectAllCheckbox() { return Container( From ff3d5cd996d4493d2bf89dbc125c45b074d6528f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:02 +0300 Subject: [PATCH 38/86] Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. --- ...ce_management_community_dialog_helper.dart | 24 +++++++++++++++++++ .../space_management_sidebar_header.dart | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart new file mode 100644 index 00000000..a18834df --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; + +abstract final class SpaceManagementCommunityDialogHelper { + static void showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index 25c094db..b5f2a1b7 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget { if (isSelected) { _clearSelection(context); } else { - _showCreateCommunityDialog(context); + SpaceManagementCommunityDialogHelper.showCreateDialog(context); } } @@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget { const ClearCommunitiesTreeSelectionEvent(), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } From 0e7109a19e297bb461a8214d01c504d5b0ab6098 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:02:15 +0300 Subject: [PATCH 39/86] Created `CommunityTemplateCell` widget. --- .../widgets/community_template_cell.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart new file mode 100644 index 00000000..4352d069 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityTemplateCell extends StatelessWidget { + const CommunityTemplateCell({ + super.key, + required this.onTap, + required this.title, + }); + + final void Function() onTap; + final Widget title; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + DefaultTextStyle( + style: context.textTheme.bodyLarge!.copyWith( + color: ColorsManager.blackColor, + ), + child: title, + ), + ], + ), + ); + } +} From a78b5993a9aa6c7e425474c0c23f30d3b3c2cadd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:05:53 +0300 Subject: [PATCH 40/86] Created `SpaceManagementTemplatesView` widget. --- .../space_management_templates_view.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart new file mode 100644 index 00000000..dd46d2c1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementTemplatesView extends StatelessWidget { + const SpaceManagementTemplatesView({super.key}); + @override + Widget build(BuildContext context) { + return Expanded( + child: ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, + ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, + ), + ), + ); + } + + List<_CommunityTemplateModel> _gridItems(BuildContext context) { + return [ + _CommunityTemplateModel( + title: const Text('Blank'), + onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context), + ), + ]; + } +} + +class _CommunityTemplateModel { + final Widget title; + final void Function() onTap; + + _CommunityTemplateModel({ + required this.title, + required this.onTap, + }); +} From 7d4cdba0eff5c9e09ac075f98302ea9a8857c173 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:06:59 +0300 Subject: [PATCH 41/86] Connected templates view into `SpaceManagementBody`, while applying the correct UI principals if what to show what when? --- .../widgets/space_management_body.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 3a9aa3c8..5d28a533 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; class SpaceManagementBody extends StatelessWidget { @@ -6,9 +9,21 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return Row( children: [ - SpaceManagementCommunitiesTree(), + const SpaceManagementCommunitiesTree(), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const Placeholder(), + child: const SpaceManagementTemplatesView(), + ), + ), + ), ], ); } From f8e4c89cdb63e50ef6cc1323a298f603690f36cf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:03 +0300 Subject: [PATCH 42/86] uses correct error message that the api sends in `RemoteCreateCommunityService`. --- .../data/services/remote_create_community_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index bd91f6ce..aae92e9f 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService { return _defaultErrorMessage; } final error = body['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; return errorMessage; } From 4bdb487094fc81d9786acaacf2fcceccc8ee6f2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:11:23 +0300 Subject: [PATCH 43/86] doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. --- .../presentation/create_community_dialog.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart index 8c1d474d..a9af44d6 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget { ); onCreateCommunity.call(community); break; - case CreateCommunityFailure(:final message): + case CreateCommunityFailure(): Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); break; default: break; From ada7daf17950efb2aba67d39efd867d796f2facd Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 10:13:30 +0300 Subject: [PATCH 44/86] Switched from using `Text` to `SelectableText` in `CreateCommunityDialog`. --- .../helpers/space_management_community_dialog_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart index a18834df..5322c3ea 100644 --- a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -9,7 +9,7 @@ abstract final class SpaceManagementCommunityDialogHelper { showDialog( context: context, builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), + title: const SelectableText('Community Name'), onCreateCommunity: (community) { context.read().add( InsertCommunity(community), From 1200a809c2719f9da1f4c6d6e86b51d468981d45 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 14:33:56 +0300 Subject: [PATCH 45/86] now cant use offline device to controll --- .../widgets/device_managment_body.dart | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index f4baad0c..c484b1b1 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -103,8 +103,30 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( + backgroundColor: selectedDevices.any( + (element) => !element.online!, + ) + ? ColorsManager.primaryColor + .withValues(alpha: 0.1) + : null, onPressed: isControlButtonEnabled ? () { + if (selectedDevices.any( + (element) => !element.online!, + )) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'This Device is Offline', + ), + duration: + Duration(seconds: 2), + ), + ); + return; + } + if (selectedDevices.length == 1) { showDialog( context: context, From 71cf0a636e15c2009bd9f134ef158e685f07a9c0 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:22:24 +0300 Subject: [PATCH 46/86] send all user instead of only uuid --- .../users_table/view/users_page.dart | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 767fd9a6..88f89e30 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -19,6 +19,7 @@ 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:syncrow_web/utils/style.dart'; + class UsersPage extends StatelessWidget { UsersPage({super.key}); @@ -451,33 +452,33 @@ class UsersPage extends StatelessWidget { ), Row( children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context - .read() - .add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog( - userId: user.uuid); - }, - ).then((v) { - if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - } - }); + if (user.isEnabled != false) + actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog(user: user); }, - ) - : actionButton( - title: "Edit", - ), + ).then((v) { + if (v != null) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + } + }); + }, + ) + else + actionButton( + title: "Edit", + ), actionButton( title: "Delete", onTap: () { From e6957d566da8ec3e4a24dc9e23ffe7d47eebb290 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:29:35 +0300 Subject: [PATCH 47/86] use RoleUserModel instead of only id and all is good --- .../view/edit_user_dialog.dart | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index 071de067..c84a6baf 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; @@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class EditUserDialog extends StatefulWidget { - final String? userId; - const EditUserDialog({super.key, this.userId}); + final RolesUserModel? user; + const EditUserDialog({ + super.key, + this.user, + }); @override _EditUserDialogState createState() => _EditUserDialogState(); @@ -28,10 +33,11 @@ class _EditUserDialogState extends State { create: (BuildContext context) => UsersBloc() // ..add(const LoadCommunityAndSpacesEvent()) ..add(const RoleEvent()) - ..add(GetUserByIdEvent(uuid: widget.userId)), + ..add(GetUserByIdEvent(uuid: widget.user!.uuid)), child: BlocConsumer(listener: (context, state) { if (state is SpacesLoadedState) { - BlocProvider.of(context).add(GetUserByIdEvent(uuid: widget.userId)); + BlocProvider.of(context) + .add(GetUserByIdEvent(uuid: widget.user!.uuid)); } }, builder: (context, state) { final _blocRole = BlocProvider.of(context); @@ -39,7 +45,8 @@ class _EditUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -68,7 +75,8 @@ class _EditUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -86,7 +94,7 @@ class _EditUserDialogState extends State { children: [ const SizedBox(height: 10), Expanded( - child: _getFormContent(widget.userId), + child: _getFormContent(widget.user!), ), const SizedBox(height: 20), ], @@ -116,13 +124,14 @@ class _EditUserDialogState extends State { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(CheckStepStatus(isEditUser: true)); + _blocRole + .add(CheckStepStatus(isEditUser: true)); } else if (currentStep == 3) { _blocRole.add(const CheckSpacesStepStatus()); } } else { - _blocRole - .add(EditInviteUsers(context: context, userId: widget.userId!)); + _blocRole.add(EditInviteUsers( + context: context, userId: widget.user!.uuid)); } }); }, @@ -131,7 +140,8 @@ class _EditUserDialogState extends State { style: TextStyle( color: (_blocRole.isCompleteSpaces == false || _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -146,15 +156,15 @@ class _EditUserDialogState extends State { })); } - Widget _getFormContent(userid) { + Widget _getFormContent(RolesUserModel user) { switch (currentStep) { case 1: return BasicsView( - userId: userid, + userId: user.uuid, ); case 2: return SpacesAccessView( - userId: userid, + userId: user.uuid, ); case 3: return const RolesAndPermission(); @@ -204,8 +214,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -263,8 +277,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -321,8 +339,12 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: currentStep == step + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: currentStep == step + ? FontWeight.bold + : FontWeight.normal, ), ), ], From 8bc7a3daa2b6cc3fdd44767186f6536ad6b8a0af Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 15:45:49 +0300 Subject: [PATCH 48/86] Implemented space management canvas. --- .../models/space_connection_model.dart | 6 + .../spaces_connections_arrow_painter.dart | 60 +++++ .../widgets/community_structure_canvas.dart | 236 ++++++++++++++++++ .../widgets/create_space_button.dart | 42 ++++ .../widgets/plus_button_widget.dart | 43 ++++ .../widgets/space_card_widget.dart | 65 +++++ .../main_module/widgets/space_cell.dart | 88 +++++++ .../widgets/space_management_body.dart | 3 +- .../space_management_community_structure.dart | 22 ++ .../communities_tree_selection_bloc.dart | 2 +- .../communities_tree_selection_event.dart | 7 +- ...anagement_communities_tree_space_tile.dart | 2 +- lib/utils/app_routes.dart | 2 +- 13 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/models/space_connection_model.dart create mode 100644 lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/create_space_button.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_cell.dart create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart diff --git a/lib/pages/space_management_v2/main_module/models/space_connection_model.dart b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart new file mode 100644 index 00000000..538a922c --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart @@ -0,0 +1,6 @@ +class SpaceConnectionModel { + final String from; + final String to; + + const SpaceConnectionModel({required this.from, required this.to}); +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart new file mode 100644 index 00000000..fcf523bf --- /dev/null +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesConnectionsArrowPainter extends CustomPainter { + final List connections; + final Map positions; + final double cardWidth = 150.0; + final double cardHeight = 90.0; + final String? selectedSpaceUuid; + + SpacesConnectionsArrowPainter({ + required this.connections, + required this.positions, + this.selectedSpaceUuid, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final connection in connections) { + final isSelected = connection.to == selectedSpaceUuid; + final paint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final from = positions[connection.from]; + final to = positions[connection.to]; + + if (from != null && to != null) { + final startPoint = + Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + + final path = Path()..moveTo(startPoint.dx, startPoint.dy); + + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, + controlPoint2.dy, endPoint.dx, endPoint.dy); + + canvas.drawPath(path, paint); + + final circlePaint = Paint() + ..color = isSelected + ? ColorsManager.primaryColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.srcIn; + canvas.drawCircle(endPoint, 4, circlePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart new file mode 100644 index 00000000..92c5add6 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class CommunityStructureCanvas extends StatefulWidget { + const CommunityStructureCanvas({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + State createState() =>_CommunityStructureCanvasState(); +} + +class _CommunityStructureCanvasState extends State + with SingleTickerProviderStateMixin { + final Map _positions = {}; + final double _cardWidth = 150.0; + final double _cardHeight = 90.0; + final double _horizontalSpacing = 150.0; + final double _verticalSpacing = 120.0; + String? _selectedSpaceUuid; + + late TransformationController _transformationController; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _transformationController = TransformationController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _runAnimation(Matrix4 target) { + final animation = Matrix4Tween( + begin: _transformationController.value, + end: target, + ).animate(_animationController); + + void listener() { + _transformationController.value = animation.value; + } + + animation.addListener(listener); + _animationController.forward(from: 0).whenCompleteOrCancel(() { + animation.removeListener(listener); + }); + } + + void _onSpaceTapped(String spaceUuid) { + setState(() { + _selectedSpaceUuid = spaceUuid; + }); + + final position = _positions[spaceUuid]; + if (position == null) return; + + const scale = 2.0; + final viewSize = context.size; + if (viewSize == null) return; + + final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final y = + -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + _runAnimation(matrix); + } + + void _resetSelectionAndZoom() { + setState(() { + _selectedSpaceUuid = null; + }); + _runAnimation(Matrix4.identity()); + } + + void _calculateLayout( + List spaces, + int depth, + Map levelXOffset, + ) { + for (final space in spaces) { + double childSubtreeWidth = 0; + if (space.children.isNotEmpty) { + _calculateLayout(space.children, depth + 1, levelXOffset); + final firstChildPos = _positions[space.children.first.uuid]; + final lastChildPos = _positions[space.children.last.uuid]; + if (firstChildPos != null && lastChildPos != null) { + childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + } + } + + final currentX = levelXOffset.putIfAbsent(depth, () => 0.0); + double? x; + + if (space.children.isNotEmpty) { + final firstChildPos = _positions[space.children.first.uuid]!; + x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + } else { + x = currentX; + } + + if (x < currentX) { + final shiftX = currentX - x; + _shiftSubtree(space, shiftX); + final keysToShift = levelXOffset.keys.where((d) => d > depth).toList(); + for (final key in keysToShift) { + levelXOffset[key] = levelXOffset[key]! + shiftX; + } + x += shiftX; + } + + final y = depth * (_verticalSpacing + _cardHeight); + _positions[space.uuid] = Offset(x, y); + levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + } + } + + void _shiftSubtree(SpaceModel space, double shiftX) { + if (_positions.containsKey(space.uuid)) { + _positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0); + } + for (final child in space.children) { + _shiftSubtree(child, shiftX); + } + } + + List _buildTreeWidgets() { + _positions.clear(); + final community = widget.community; + + _calculateLayout(community.spaces, 0, {}); + + final widgets = []; + final connections = []; + _generateWidgets(community.spaces, widgets, connections); + + return [ + CustomPaint( + painter: SpacesConnectionsArrowPainter( + connections: connections, + positions: _positions, + selectedSpaceUuid: _selectedSpaceUuid, + ), + child: Stack(alignment: AlignmentDirectional.center, children: widgets), + ), + ]; + } + + void _generateWidgets( + List spaces, + List widgets, + List connections, + ) { + for (final space in spaces) { + final position = _positions[space.uuid]; + if (position == null) continue; + + widgets.add( + Positioned( + left: position.dx, + top: position.dy, + width: _cardWidth, + height: _cardHeight, + child: SpaceCardWidget( + index: spaces.indexOf(space), + onPositionChanged: (newPosition) {}, + buildSpaceContainer: (index) { + return Opacity( + opacity: 1.0, + child: SpaceCell( + index: index, + onTap: () => _onSpaceTapped(space.uuid), + icon: space.icon, + name: space.spaceName, + ), + ); + }, + screenSize: MediaQuery.sizeOf(context), + position: position, + isHovered: false, + onHoverChanged: (int index, bool isHovered) {}, + onButtonTap: (int index, Offset newPosition) {}, + ), + ), + ); + + for (final child in space.children) { + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + _generateWidgets(space.children, widgets, connections); + } + } + + @override + Widget build(BuildContext context) { + final treeWidgets = _buildTreeWidgets(); + return InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: MediaQuery.sizeOf(context).width * 0.3, + vertical: MediaQuery.sizeOf(context).height * 0.2, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: GestureDetector( + onTap: _resetSelectionAndZoom, + child: SizedBox( + width: MediaQuery.sizeOf(context).width * 2, + height: MediaQuery.sizeOf(context).height * 2, + child: Stack(children: treeWidgets), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart new file mode 100644 index 00000000..5caf6a81 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceButton extends StatelessWidget { + const CreateSpaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () {}, + child: Container( + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart new file mode 100644 index 00000000..755a6ab9 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final int index; + final String direction; + final Offset offset; + final void Function(int index, Offset newPosition) onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.index, + required this.direction, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (direction == 'down') { + onButtonTap(index, const Offset(0, 150)); + } else { + onButtonTap(index, const Offset(150, 0)); + } + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart new file mode 100644 index 00000000..1ce28502 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; + +class SpaceCardWidget extends StatelessWidget { + final int index; + final Size screenSize; + final Offset position; + final bool isHovered; + final void Function(int index, bool isHovered) onHoverChanged; + final void Function(int index, Offset newPosition) onButtonTap; + final Widget Function(int index) buildSpaceContainer; + final ValueChanged onPositionChanged; + + const SpaceCardWidget({ + super.key, + required this.index, + required this.onPositionChanged, + required this.screenSize, + required this.position, + required this.isHovered, + required this.onHoverChanged, + required this.onButtonTap, + required this.buildSpaceContainer, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => onHoverChanged(index, true), + onExit: (_) => onHoverChanged(index, false), + child: SizedBox( + width: 150, + height: 90, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + buildSpaceContainer(index), + + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + index: index, + direction: 'down', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + if (isHovered) + Positioned( + right: -15, + child: PlusButtonWidget( + index: index, + direction: 'right', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart new file mode 100644 index 00000000..1b08835a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceCell extends StatelessWidget { + final int index; + final String icon; + final String name; + final VoidCallback? onDoubleTap; + final VoidCallback? onTap; + + const SpaceCell({ + super.key, + required this.index, + required this.icon, + required this.name, + this.onTap, + this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onDoubleTap: onDoubleTap, + onTap: onTap, + child: Container( + width: 150, + height: 70, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + color: ColorsManager.whiteColors, + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: ColorsManager.lightGrayColor.withValues(alpha: 0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 5d28a533..5d81bffb 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; @@ -19,7 +20,7 @@ class SpaceManagementBody extends StatelessWidget { previous.selectedCommunity != current.selectedCommunity, builder: (context, state) => Visibility( visible: state.selectedCommunity == null, - replacement: const Placeholder(), + replacement: const SpaceManagementCommunityStructure(), child: const SpaceManagementTemplatesView(), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart new file mode 100644 index 00000000..11ee5078 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; + +class SpaceManagementCommunityStructure extends StatelessWidget { + const SpaceManagementCommunityStructure({super.key}); + + @override + Widget build(BuildContext context) { + final selectedCommunity = + context.watch().state.selectedCommunity!; + const spacer = Spacer(flex: 10); + return Visibility( + visible: selectedCommunity.spaces.isNotEmpty, + replacement: const Row( + children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + child: CommunityStructureCanvas(community: selectedCommunity), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart index bfc02f11..bdda04ee 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc ) { emit( CommunitiesTreeSelectionState( - selectedCommunity: null, + selectedCommunity: event.community, selectedSpace: event.space, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 95ffe173..40a41f74 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable { } final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { - final CommunityModel? community; + final CommunityModel community; const SelectCommunityEvent({required this.community}); @override @@ -16,9 +16,10 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel? space; + final SpaceModel space; + final CommunityModel community; - const SelectSpaceEvent({required this.space}); + const SelectSpaceEvent({required this.space, required this.community}); @override List get props => [space]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart index dcd44ac8..795e2c3a 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { initiallyExpanded: spaceIsExpanded, onExpansionChanged: (expanded) {}, onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), + SelectSpaceEvent(community: community, space: space), ), children: space.children .map( diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 5276f4186c46022f5bf42cc6e9aa23fba8cf8ddb Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 15:55:56 +0300 Subject: [PATCH 49/86] emit error state when catch error and send the real API exception --- lib/pages/auth/bloc/auth_bloc.dart | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 58950089..bfe0b3eb 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -36,7 +36,8 @@ class AuthBloc extends Bloc { ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); - final TextEditingController forgetPasswordController = TextEditingController(); + final TextEditingController forgetPasswordController = + TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); final forgetEmailKey = GlobalKey(); @@ -53,7 +54,8 @@ class AuthBloc extends Bloc { return; } _remainingTime = 1; - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); try { forgetEmailValidate = ''; _remainingTime = (await AuthenticationAPI.sendOtp( @@ -90,7 +92,8 @@ class AuthBloc extends Bloc { _timer?.cancel(); add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true)); } else { - add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false)); + add(UpdateTimerEvent( + remainingTime: _remainingTime, isButtonEnabled: false)); } }); } @@ -100,7 +103,7 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } -Future changePassword( + Future changePassword( ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { @@ -122,7 +125,6 @@ Future changePassword( } } - String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -131,7 +133,9 @@ Future changePassword( } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { - emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime)); + emit(TimerState( + isButtonEnabled: event.isButtonEnabled, + remainingTime: event.remainingTime)); } ///////////////////////////////////// login ///////////////////////////////////// @@ -151,7 +155,6 @@ Future changePassword( static UserModel? user; bool showValidationMessage = false; - void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -170,11 +173,11 @@ Future changePassword( ); } on APIException catch (e) { validate = e.message; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } catch (e) { validate = 'Something went wrong'; - emit(LoginInitial()); + emit(LoginFailure(error: validate)); return; } @@ -197,7 +200,6 @@ Future changePassword( } } - checkBoxToggle( CheckBoxEvent event, Emitter emit, @@ -339,12 +341,14 @@ Future changePassword( static Future getTokenAndValidate() async { try { const storage = FlutterSecureStorage(); - final firstLaunch = - await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; + final firstLaunch = await SharedPreferencesHelper.readBoolFromSP( + StringsManager.firstLaunch) ?? + true; if (firstLaunch) { storage.deleteAll(); } - await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false); + await SharedPreferencesHelper.saveBoolToSP( + StringsManager.firstLaunch, false); final value = await storage.read(key: Token.loginAccessTokenKey) ?? ''; if (value.isEmpty) { return 'Token not found'; @@ -397,7 +401,9 @@ Future changePassword( final String formattedTime = [ if (days > 0) '${days}d', // Append 'd' for days if (days > 0 || hours > 0) - hours.toString().padLeft(2, '0'), // Show hours if there are days or hours + hours + .toString() + .padLeft(2, '0'), // Show hours if there are days or hours minutes.toString().padLeft(2, '0'), seconds.toString().padLeft(2, '0'), ].join(':'); From ad00cf35ba65e6a935b4203289dfe6bbe0f8ac29 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:05:16 +0300 Subject: [PATCH 50/86] added the PR notes --- .../all_devices/widgets/device_managment_body.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index c484b1b1..c865a5dc 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; - + final isAnyDeviceOffline = + selectedDevices.any((element) => !(element.online ?? false)); return Row( children: [ Expanded(child: SpaceTreeView( @@ -103,17 +104,13 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { decoration: containerDecoration, child: Center( child: DefaultButton( - backgroundColor: selectedDevices.any( - (element) => !element.online!, - ) + backgroundColor: isAnyDeviceOffline ? ColorsManager.primaryColor .withValues(alpha: 0.1) : null, onPressed: isControlButtonEnabled ? () { - if (selectedDevices.any( - (element) => !element.online!, - )) { + if (isAnyDeviceOffline) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( From 379ecec789e2339fa3e4e4b07419155384991c41 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:15:05 +0300 Subject: [PATCH 51/86] use isCurrentStep instead of checking with multi variables everyTime --- .../view/edit_user_dialog.dart | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index c84a6baf..00c566c6 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -176,6 +176,7 @@ class _EditUserDialogState extends State { int step3 = 0; Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -199,7 +200,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteBasics == false ? Assets.wrongProcessIcon @@ -214,12 +215,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -243,6 +243,7 @@ class _EditUserDialogState extends State { } Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -262,7 +263,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteSpaces == false ? Assets.wrongProcessIcon @@ -277,12 +278,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -306,6 +306,7 @@ class _EditUserDialogState extends State { } Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -324,7 +325,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteRolePermissions == false ? Assets.wrongProcessIcon @@ -339,12 +340,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step + color: isCurrentStep ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step - ? FontWeight.bold - : FontWeight.normal, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], From d14cc785a8f23dda61d274cf8b22d4f08bbb4c90 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:16:34 +0300 Subject: [PATCH 52/86] no need for two condtions inside themselves --- .../users_page/users_table/view/users_page.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 88f89e30..da159d94 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -468,9 +468,7 @@ class UsersPage extends StatelessWidget { }, ).then((v) { if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } + _blocRole.add(const GetUsers()); } }); }, From 75efc595b47b6ef4eec385e57b1dd8f26158ad08 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 16:22:11 +0300 Subject: [PATCH 53/86] reverted to old import to avoid confusion with QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 95d6e1ecda4709a693e39f6101ca95af69207f60 Mon Sep 17 00:00:00 2001 From: raf-dev1 Date: Mon, 23 Jun 2025 16:33:45 +0300 Subject: [PATCH 54/86] if online go green with online status else red with offline status --- .../shared/device_control_dialog.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index beb3b52c..7a046bea 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { } Widget _buildDeviceInfoSection() { + final isOnlineDevice = device.online != null && device.online!; return Padding( padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50), child: Table( @@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { 'Installation Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.createTime ?? 0) * 1000), + (device.createTime ?? 0) * 1000, ), ), ), @@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), TableRow( children: [ - _buildInfoRow('Status:', 'Online', statusColor: Colors.green), + _buildInfoRow( + 'Status:', + isOnlineDevice ? 'Online' : 'offline', + statusColor: isOnlineDevice ? Colors.green : Colors.red, + ), _buildInfoRow( 'Last Offline Date and Time:', formatDateTime( DateTime.fromMillisecondsSinceEpoch( - ((device.updateTime ?? 0) * 1000), + (device.updateTime ?? 0) * 1000, ), ), ), From 95ae50d12dd97ec616d22f0b14d9ab70c2ea7920 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:16:03 +0300 Subject: [PATCH 55/86] navigates to selected space when changed on sidebar in space management canvas. --- .../widgets/community_structure_canvas.dart | 82 ++++++++++++------- .../widgets/plus_button_widget.dart | 14 +--- .../widgets/space_card_widget.dart | 51 ++++-------- .../main_module/widgets/space_cell.dart | 17 ++-- .../space_management_community_structure.dart | 12 ++- .../communities_tree_selection_event.dart | 2 +- 6 files changed, 86 insertions(+), 92 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 92c5add6..22b4536a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ required this.community, + required this.selectedSpace, super.key, }); final CommunityModel community; + final SpaceModel? selectedSpace; @override - State createState() =>_CommunityStructureCanvasState(); + State createState() => _CommunityStructureCanvasState(); } class _CommunityStructureCanvasState extends State @@ -25,19 +29,30 @@ class _CommunityStructureCanvasState extends State final double _cardHeight = 90.0; final double _horizontalSpacing = 150.0; final double _verticalSpacing = 120.0; - String? _selectedSpaceUuid; late TransformationController _transformationController; late AnimationController _animationController; @override void initState() { - super.initState(); _transformationController = TransformationController(); _animationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 100), + duration: const Duration(milliseconds: 150), ); + super.initState(); + } + + @override + void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _animateToSpace(widget.selectedSpace); + } + }); + } } @override @@ -63,15 +78,16 @@ class _CommunityStructureCanvasState extends State }); } - void _onSpaceTapped(String spaceUuid) { - setState(() { - _selectedSpaceUuid = spaceUuid; - }); + void _animateToSpace(SpaceModel? space) { + if (space == null) { + _runAnimation(Matrix4.identity()); + return; + } - final position = _positions[spaceUuid]; + final position = _positions[space.uuid]; if (position == null) return; - const scale = 2.0; + const scale = 1.5; final viewSize = context.size; if (viewSize == null) return; @@ -86,11 +102,19 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onSpaceTapped(SpaceModel? space) { + context.read().add( + SelectSpaceEvent(community: widget.community, space: space), + ); + } + void _resetSelectionAndZoom() { - setState(() { - _selectedSpaceUuid = null; - }); - _runAnimation(Matrix4.identity()); + context.read().add( + SelectSpaceEvent( + community: widget.community, + space: null, + ), + ); } void _calculateLayout( @@ -159,7 +183,7 @@ class _CommunityStructureCanvasState extends State painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: _selectedSpaceUuid, + selectedSpaceUuid: widget.selectedSpace?.uuid, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -182,24 +206,24 @@ class _CommunityStructureCanvasState extends State width: _cardWidth, height: _cardHeight, child: SpaceCardWidget( - index: spaces.indexOf(space), - onPositionChanged: (newPosition) {}, - buildSpaceContainer: (index) { + buildSpaceContainer: () { return Opacity( - opacity: 1.0, + opacity: widget.selectedSpace == null + ? 1.0 + : widget.selectedSpace?.uuid != space.uuid + ? 0.5 + : 1.0, child: SpaceCell( - index: index, - onTap: () => _onSpaceTapped(space.uuid), + onTap: () => _onSpaceTapped(space), icon: space.icon, name: space.spaceName, ), ); }, - screenSize: MediaQuery.sizeOf(context), - position: position, - isHovered: false, - onHoverChanged: (int index, bool isHovered) {}, - onButtonTap: (int index, Offset newPosition) {}, + onTap: () => showDialog( + context: context, + builder: (context) => const Text('123'), + ), ), ), ); @@ -218,7 +242,7 @@ class _CommunityStructureCanvasState extends State transformationController: _transformationController, boundaryMargin: EdgeInsets.symmetric( horizontal: MediaQuery.sizeOf(context).width * 0.3, - vertical: MediaQuery.sizeOf(context).height * 0.2, + vertical: MediaQuery.sizeOf(context).height * 0.3, ), minScale: 0.5, maxScale: 3.0, @@ -226,8 +250,8 @@ class _CommunityStructureCanvasState extends State child: GestureDetector( onTap: _resetSelectionAndZoom, child: SizedBox( - width: MediaQuery.sizeOf(context).width * 2, - height: MediaQuery.sizeOf(context).height * 2, + width: MediaQuery.sizeOf(context).width * 5, + height: MediaQuery.sizeOf(context).height * 5, child: Stack(children: treeWidgets), ), ), diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart index 755a6ab9..68169861 100644 --- a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -2,15 +2,11 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class PlusButtonWidget extends StatelessWidget { - final int index; - final String direction; final Offset offset; - final void Function(int index, Offset newPosition) onButtonTap; + final void Function() onButtonTap; const PlusButtonWidget({ super.key, - required this.index, - required this.direction, required this.offset, required this.onButtonTap, }); @@ -18,13 +14,7 @@ class PlusButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - if (direction == 'down') { - onButtonTap(index, const Offset(0, 150)); - } else { - onButtonTap(index, const Offset(150, 0)); - } - }, + onTap: onButtonTap, child: Container( width: 30, height: 30, diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart index 1ce28502..e91e577f 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -1,60 +1,39 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; -class SpaceCardWidget extends StatelessWidget { - final int index; - final Size screenSize; - final Offset position; - final bool isHovered; - final void Function(int index, bool isHovered) onHoverChanged; - final void Function(int index, Offset newPosition) onButtonTap; - final Widget Function(int index) buildSpaceContainer; - final ValueChanged onPositionChanged; +class SpaceCardWidget extends StatefulWidget { + final void Function() onTap; + final Widget Function() buildSpaceContainer; const SpaceCardWidget({ - super.key, - required this.index, - required this.onPositionChanged, - required this.screenSize, - required this.position, - required this.isHovered, - required this.onHoverChanged, - required this.onButtonTap, + required this.onTap, required this.buildSpaceContainer, + super.key, }); + @override + State createState() => _SpaceCardWidgetState(); +} + +class _SpaceCardWidgetState extends State { + bool isHovered = false; @override Widget build(BuildContext context) { return MouseRegion( - onEnter: (_) => onHoverChanged(index, true), - onExit: (_) => onHoverChanged(index, false), + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), child: SizedBox( - width: 150, - height: 90, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ - buildSpaceContainer(index), - + widget.buildSpaceContainer(), if (isHovered) Positioned( bottom: 0, child: PlusButtonWidget( - index: index, - direction: 'down', offset: Offset.zero, - onButtonTap: onButtonTap, - ), - ), - if (isHovered) - Positioned( - right: -15, - child: PlusButtonWidget( - index: index, - direction: 'right', - offset: Offset.zero, - onButtonTap: onButtonTap, + onButtonTap: widget.onTap, ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart index 1b08835a..bcde6560 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -1,29 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class SpaceCell extends StatelessWidget { - final int index; final String icon; final String name; - final VoidCallback? onDoubleTap; final VoidCallback? onTap; const SpaceCell({ super.key, - required this.index, required this.icon, required this.name, - this.onTap, - this.onDoubleTap, + required this.onTap, }); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return GestureDetector( - onDoubleTap: onDoubleTap, onTap: onTap, child: Container( width: 150, @@ -36,7 +30,7 @@ class SpaceCell extends StatelessWidget { Expanded( child: Text( name, - style: theme.textTheme.bodyLarge?.copyWith( + style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: ColorsManager.blackColor, ), @@ -63,7 +57,10 @@ class SpaceCell extends StatelessWidget { child: Center( child: SvgPicture.asset( icon, - color: ColorsManager.whiteColors, + colorFilter: const ColorFilter.mode( + ColorsManager.whiteColors, + BlendMode.srcIn, + ), width: 24, height: 24, ), diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 11ee5078..6fe80835 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -9,14 +9,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget { @override Widget build(BuildContext context) { - final selectedCommunity = - context.watch().state.selectedCommunity!; + final selectionBloc = context.watch().state; + final selectedCommunity = selectionBloc.selectedCommunity; + final selectedSpace = selectionBloc.selectedSpace; const spacer = Spacer(flex: 10); return Visibility( - visible: selectedCommunity.spaces.isNotEmpty, + visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), - child: CommunityStructureCanvas(community: selectedCommunity), + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart index 40a41f74..21088632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -16,7 +16,7 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { } final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { - final SpaceModel space; + final SpaceModel? space; final CommunityModel community; const SelectSpaceEvent({required this.space, required this.community}); From 87b45fff1dfee05f8ae716ca7eed31589864d6db Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:21:25 +0300 Subject: [PATCH 56/86] removed expanded widget that caused a size exception. --- .../space_management_templates_view.dart | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart index dd46d2c1..138dbbc4 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -7,26 +7,24 @@ class SpaceManagementTemplatesView extends StatelessWidget { const SpaceManagementTemplatesView({super.key}); @override Widget build(BuildContext context) { - return Expanded( - child: ColoredBox( - color: ColorsManager.whiteColors, - child: GridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 400, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 2.0, - ), - itemCount: _gridItems(context).length, - itemBuilder: (context, index) { - final model = _gridItems(context)[index]; - return CommunityTemplateCell( - onTap: model.onTap, - title: model.title, - ); - }, + return ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, ), ); } From 0fb91496135e059fc00d2aea2f13f62b253a4ec1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:26:56 +0300 Subject: [PATCH 57/86] Selects all children of a space when selecting a parent. --- .../spaces_connections_arrow_painter.dart | 10 +++--- .../widgets/community_structure_canvas.dart | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index fcf523bf..cad82a60 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -7,21 +7,21 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final Map positions; final double cardWidth = 150.0; final double cardHeight = 90.0; - final String? selectedSpaceUuid; + final Set highlightedUuids; SpacesConnectionsArrowPainter({ required this.connections, required this.positions, - this.selectedSpaceUuid, + required this.highlightedUuids, }); @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = connection.to == selectedSpaceUuid; + final isSelected = highlightedUuids.contains(connection.from); final paint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; @@ -46,7 +46,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final circlePaint = Paint() ..color = isSelected - ? ColorsManager.primaryColor + ? ColorsManager.blackColor : ColorsManager.blackColor.withValues(alpha: 0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.srcIn; diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 22b4536a..04795c11 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -62,6 +62,15 @@ class _CommunityStructureCanvasState extends State super.dispose(); } + Set _getAllDescendantUuids(SpaceModel space) { + final uuids = {}; + for (final child in space.children) { + uuids.add(child.uuid); + uuids.addAll(_getAllDescendantUuids(child)); + } + return uuids; + } + void _runAnimation(Matrix4 target) { final animation = Matrix4Tween( begin: _transformationController.value, @@ -174,16 +183,23 @@ class _CommunityStructureCanvasState extends State _calculateLayout(community.spaces, 0, {}); + final selectedSpace = widget.selectedSpace; + final highlightedUuids = {}; + if (selectedSpace != null) { + highlightedUuids.add(selectedSpace.uuid); + highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace)); + } + final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections); + _generateWidgets(community.spaces, widgets, connections, highlightedUuids); return [ CustomPaint( painter: SpacesConnectionsArrowPainter( connections: connections, positions: _positions, - selectedSpaceUuid: widget.selectedSpace?.uuid, + highlightedUuids: highlightedUuids, ), child: Stack(alignment: AlignmentDirectional.center, children: widgets), ), @@ -194,11 +210,15 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, + Set highlightedUuids, ) { for (final space in spaces) { final position = _positions[space.uuid]; if (position == null) continue; + final isHighlighted = highlightedUuids.contains(space.uuid); + final hasNoSelectedSpace = widget.selectedSpace == null; + widgets.add( Positioned( left: position.dx, @@ -208,11 +228,7 @@ class _CommunityStructureCanvasState extends State child: SpaceCardWidget( buildSpaceContainer: () { return Opacity( - opacity: widget.selectedSpace == null - ? 1.0 - : widget.selectedSpace?.uuid != space.uuid - ? 0.5 - : 1.0, + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, child: SpaceCell( onTap: () => _onSpaceTapped(space), icon: space.icon, @@ -229,9 +245,11 @@ class _CommunityStructureCanvasState extends State ); for (final child in space.children) { - connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + connections.add( + SpaceConnectionModel(from: space.uuid, to: child.uuid), + ); } - _generateWidgets(space.children, widgets, connections); + _generateWidgets(space.children, widgets, connections, highlightedUuids); } } From 329b2ba472ca1f0b4591ef4142911eb35c63f6df Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:36:13 +0300 Subject: [PATCH 58/86] selects the space from and to connection when selecting a space. --- .../painters/spaces_connections_arrow_painter.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart index cad82a60..e9fa0a15 100644 --- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -18,7 +18,8 @@ class SpacesConnectionsArrowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { for (final connection in connections) { - final isSelected = highlightedUuids.contains(connection.from); + final isSelected = highlightedUuids.contains(connection.from) || + highlightedUuids.contains(connection.to); final paint = Paint() ..color = isSelected ? ColorsManager.blackColor @@ -36,7 +37,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter { final path = Path()..moveTo(startPoint.dx, startPoint.dy); - final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60); + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, From 90f8305aa1baaee3536000415ff5bf79d739c9a0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:45:13 +0300 Subject: [PATCH 59/86] shows tooltip on `SpaceCell`. --- .../widgets/community_structure_canvas.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 04795c11..5d38cfa2 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -229,10 +229,14 @@ class _CommunityStructureCanvasState extends State buildSpaceContainer: () { return Opacity( opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), ), ); }, From 5a2299ea2f166cd812409af66f0803be252f4d5f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 10:47:48 +0300 Subject: [PATCH 60/86] navigates to initial create space dialog from the respective buttons. --- .../widgets/community_structure_canvas.dart | 6 ++---- .../main_module/widgets/create_space_button.dart | 3 ++- .../space_management_community_structure.dart | 3 ++- .../helpers/space_details_dialog_helper.dart | 11 +++++++++++ .../presentation/widgets/space_details_dialog.dart | 12 ++++++++++++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart create mode 100644 lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 5d38cfa2..4aea103a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; class CommunityStructureCanvas extends StatefulWidget { const CommunityStructureCanvas({ @@ -240,10 +241,7 @@ class _CommunityStructureCanvasState extends State ), ); }, - onTap: () => showDialog( - context: context, - builder: (context) => const Text('123'), - ), + onTap: () => SpaceDetailsDialogHelper.showCreate(context), ), ), ); diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart index 5caf6a81..4cbfd7fd 100644 --- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class CreateSpaceButton extends StatelessWidget { @@ -7,7 +8,7 @@ class CreateSpaceButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () {}, + onTap: () => SpaceDetailsDialogHelper.showCreate(context), child: Container( height: 60, decoration: BoxDecoration( diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart index 6fe80835..99d0668a 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -16,7 +16,8 @@ class SpaceManagementCommunityStructure extends StatelessWidget { return Visibility( visible: selectedCommunity!.spaces.isNotEmpty, replacement: const Row( - children: [spacer, Expanded(child: CreateSpaceButton()), spacer]), + children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + ), child: CommunityStructureCanvas( community: selectedCommunity, selectedSpace: selectedSpace, diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart new file mode 100644 index 00000000..e871f4d0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; + +abstract final class SpaceDetailsDialogHelper { + static void showCreate(BuildContext context) { + showDialog( + context: context, + builder: (context) => const SpaceDetailsDialog(), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart new file mode 100644 index 00000000..7213c99e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class SpaceDetailsDialog extends StatelessWidget { + const SpaceDetailsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: Text('Create Space'), + ); + } +} From 2a2fb7ffca48658623ec6f366ac9b97c61b9892f Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 11:36:50 +0300 Subject: [PATCH 61/86] Add responsive input fields and radio groups for visitor password setup --- .../view/access_type_radio_group.dart | 120 ++++ .../view/responsive_fields_row.dart | 73 +++ .../view/usage_frequency_radio_group.dart | 91 +++ .../view/visitor_password_dialog.dart | 589 +++++++++--------- 4 files changed, 574 insertions(+), 299 deletions(-) create mode 100644 lib/pages/visitor_password/view/access_type_radio_group.dart create mode 100644 lib/pages/visitor_password/view/responsive_fields_row.dart create mode 100644 lib/pages/visitor_password/view/usage_frequency_radio_group.dart diff --git a/lib/pages/visitor_password/view/access_type_radio_group.dart b/lib/pages/visitor_password/view/access_type_radio_group.dart new file mode 100644 index 00000000..be4adb9d --- /dev/null +++ b/lib/pages/visitor_password/view/access_type_radio_group.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; + +class AccessTypeRadioGroup extends StatelessWidget { + final String? selectedType; + final String? accessTypeSelected; + final Function(String) onTypeSelected; + final VisitorPasswordBloc visitorBloc; + + const AccessTypeRadioGroup({ + super.key, + required this.selectedType, + required this.accessTypeSelected, + required this.onTypeSelected, + required this.visitorBloc, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Access Type', style: text), + ], + ), + const SizedBox(height: 8), + if (size.width < 800) + Column( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + ], + ) + else + Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + ], + ), + ), + const Spacer(flex: 2), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + }) { + return SizedBox( + width: width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + fontSize: 13, + )), + value: value, + groupValue: groupValue, + onChanged: (value) { + if (value != null) { + onChanged(value); + if (value == 'Dynamic Password') { + visitorBloc.usageFrequencySelected = ''; + } + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/responsive_fields_row.dart b/lib/pages/visitor_password/view/responsive_fields_row.dart new file mode 100644 index 00000000..92a79276 --- /dev/null +++ b/lib/pages/visitor_password/view/responsive_fields_row.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; + +class NameAndEmailFields extends StatelessWidget { + final TextEditingController nameController; + final TextEditingController emailController; + final String? Function(String?)? nameValidator; + final String? Function(String?)? emailValidator; + + const NameAndEmailFields({ + super.key, + required this.nameController, + required this.emailController, + required this.nameValidator, + required this.emailValidator, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return Container( + width: size.width, + child: size.width < 800 + ? Column( + children: [ + CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + const SizedBox(height: 15), + CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ], + ) + : Row( + children: [ + Expanded( + flex: 2, + child: CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ), + const Spacer(), + ], + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/usage_frequency_radio_group.dart b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart new file mode 100644 index 00000000..aebebefe --- /dev/null +++ b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class UsageFrequencyRadioGroup extends StatelessWidget { + final String? selectedFrequency; + final String? usageFrequencySelected; + final Function(String) onFrequencySelected; + + const UsageFrequencyRadioGroup({ + super.key, + required this.selectedFrequency, + required this.usageFrequencySelected, + required this.onFrequencySelected, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return size.width < 600 + ? Column( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + ], + ) + : Row( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + required TextStyle text, + bool fullWidth = false, + }) { + return SizedBox( + width: fullWidth ? double.infinity : width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, style: text), + value: value, + groupValue: groupValue, + onChanged: (String? value) { + if (value != null) { + onChanged(value); + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 1e43af46..4b5cb0e2 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -9,8 +9,11 @@ import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/access_type_radio_group.dart'; import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; +import 'package:syncrow_web/pages/visitor_password/view/responsive_fields_row.dart'; +import 'package:syncrow_web/pages/visitor_password/view/usage_frequency_radio_group.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -21,7 +24,10 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; - var text = Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black, fontSize: 13); + var text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); return BlocProvider( create: (context) => VisitorPasswordBloc(), child: BlocListener( @@ -35,7 +41,8 @@ class VisitorPasswordDialog extends StatelessWidget { title: 'Sent Successfully', widgeta: Column( children: [ - if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.failedOperations.isNotEmpty) Column( children: [ const Text('Failed Devices'), @@ -45,7 +52,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.failedOperations.length, + itemCount: visitorBloc + .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), @@ -53,14 +61,17 @@ class VisitorPasswordDialog extends StatelessWidget { height: 45, child: Center( child: Text(visitorBloc - .passwordStatus!.failedOperations[index].deviceUuid)), + .passwordStatus! + .failedOperations[index] + .deviceUuid)), ); }, ), ), ], ), - if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -70,15 +81,18 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.successOperations.length, + itemCount: visitorBloc + .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc.passwordStatus! - .successOperations[index].deviceUuid)), + child: Text(visitorBloc + .passwordStatus! + .successOperations[index] + .deviceUuid)), ); }, ), @@ -89,7 +103,6 @@ class VisitorPasswordDialog extends StatelessWidget { )) .then((v) { Navigator.of(context).pop(true); - }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -102,15 +115,16 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; + bool isRepeat = + state is IsRepeatState ? state.repeat : visitorBloc.repeat; return AlertDialog( backgroundColor: Colors.white, title: Text( 'Create visitor password', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -121,34 +135,11 @@ class VisitorPasswordDialog extends StatelessWidget { padding: const EdgeInsets.all(5.0), child: ListBody( children: [ - Container( - child: Row( - children: [ - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validate, - controller: visitorBloc.userNameController, - isRequired: true, - textFieldName: 'Name', - description: '', - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validateEmail, - controller: visitorBloc.emailController, - isRequired: true, - textFieldName: 'Email Address', - description: - 'The password will be sent to the visitor’s email address.', - ), - ), - const Spacer(), - ], - ), + NameAndEmailFields( + nameController: visitorBloc.userNameController, + emailController: visitorBloc.emailController, + nameValidator: visitorBloc.validate, + emailValidator: visitorBloc.validateEmail, ), const SizedBox( height: 15, @@ -156,107 +147,43 @@ class VisitorPasswordDialog extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text('Access Type', style: text), - ], + AccessTypeRadioGroup( + selectedType: state is PasswordTypeSelected + ? state.selectedType + : null, + accessTypeSelected: + visitorBloc.accessTypeSelected, + onTypeSelected: (value) { + context + .read() + .add(SelectPasswordType(value)); + }, + visitorBloc: visitorBloc, ), - Row( - children: [ - Expanded( - flex: 2, - child: Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Online Password', - style: text, - ), - value: 'Online Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Offline Password', style: text), - value: 'Offline Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - // SizedBox( - // width: size.width * 0.12, - // child: RadioListTile( - // contentPadding: EdgeInsets.zero, - // title: Text( - // 'Dynamic Password', - // style: text, - // ), - // value: 'Dynamic Password', - // groupValue: (state is PasswordTypeSelected) - // ? state.selectedType - // : visitorBloc.accessTypeSelected, - // onChanged: (String? value) { - // if (value != null) { - // context - // .read() - // .add(SelectPasswordType(value)); - // visitorBloc.usageFrequencySelected = ''; - // } - // }, - // ), - // ), - ], - )), - const Spacer( - flex: 2, - ), - ], - ), - if (visitorBloc.accessTypeSelected == 'Online Password') + + if (visitorBloc.accessTypeSelected == + 'Online Password') Text( 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), - if (visitorBloc.accessTypeSelected == 'Offline Password') + if (visitorBloc.accessTypeSelected == + 'Offline Password') Text( 'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), // if (visitorBloc.accessTypeSelected == 'Dynamic Password') // Text( @@ -271,143 +198,170 @@ class VisitorPasswordDialog extends StatelessWidget { ) ], ), - visitorBloc.accessTypeSelected == 'Dynamic Password' - ? const SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (visitorBloc.accessTypeSelected == + 'Dynamic Password') + const SizedBox() + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text( - 'Usage Frequency', - style: text, - ), - ], + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), ), - Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'One-Time', - style: text, - ), - value: 'One-Time', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Periodic', style: text), - value: 'Periodic', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context.read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - ], + Text( + 'Usage Frequency', + style: text, ), - - //One-Time - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - // Periodic - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), ], ), + UsageFrequencyRadioGroup( + selectedFrequency: + state is UsageFrequencySelected + ? state.selectedFrequency + : null, + usageFrequencySelected: + visitorBloc.usageFrequencySelected, + onFrequencySelected: (value) { + context + .read() + .add(SelectUsageFrequency(value)); + }, + ), + + //One-Time + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, each device can be unlocked only once.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + // Periodic + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + ], + ), const SizedBox( height: 20, ), - if ((visitorBloc.usageFrequencySelected != 'One-Time' || - visitorBloc.accessTypeSelected != 'Offline Password') && + if ((visitorBloc.usageFrequencySelected != + 'One-Time' || + visitorBloc.accessTypeSelected != + 'Offline Password') && (visitorBloc.usageFrequencySelected != '')) DateTimeWebWidget( isRequired: true, title: 'Access Period', size: size, endTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: false)); } else { - visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add( - SelectTimeEvent(context: context, isEffective: true)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: true)); } else { - visitorBloc.add(SelectTimeVisitorPassword( - context: context, isStart: true, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); } }, - firstString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + firstString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess.toString(), - secondString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + : visitorBloc.startTimeAccess + .toString(), + secondString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.expirationTime : visitorBloc.endTimeAccess.toString(), icon: Assets.calendarIcon), - const SizedBox(height: 10,), - Text(visitorBloc.accessPeriodValidate, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), + const SizedBox( + height: 10, + ), + Text( + visitorBloc.accessPeriodValidate, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.red), + ), const SizedBox( height: 20, ), @@ -431,16 +385,21 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), const SizedBox( height: 20, ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') SizedBox( width: 100, child: Column( @@ -451,7 +410,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc.add(ToggleRepeatEvent()); + visitorBloc + .add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -459,12 +419,16 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - isRepeat ? const RepeatWidget() : const SizedBox(), + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + isRepeat + ? const RepeatWidget() + : const SizedBox(), Container( decoration: containerDecoration, - width: size.width / 9, + width: size.width / 6, child: DefaultButton( onPressed: () { showDialog( @@ -472,22 +436,28 @@ class VisitorPasswordDialog extends StatelessWidget { barrierDismissible: false, builder: (BuildContext context) { return AddDeviceDialog( - selectedDeviceIds: visitorBloc.selectedDevices, + selectedDeviceIds: + visitorBloc.selectedDevices, ); }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = listDevice; + visitorBloc.selectedDevices = + listDevice; } }); }, borderRadius: 8, child: Text( '+ Add Device', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.whiteColors, - fontSize: 12), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: + ColorsManager.whiteColors, + fontSize: 12), ), ), ), @@ -525,30 +495,37 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.selectedDevices.isNotEmpty) { - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { setPasswordFunction(context, size, visitorBloc); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && - visitorBloc.effectiveTime != 'Start Time' ) { + visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); - }else{ + } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - } else if( - visitorBloc.endTimeAccess.toString()!='End Time' - &&visitorBloc.startTimeAccess.toString()!='Start Time') { + } else if (visitorBloc.endTimeAccess.toString() != + 'End Time' && + visitorBloc.startTimeAccess.toString() != + 'Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time' && visitorBloc.selectedDays.isNotEmpty) { - setPasswordFunction(context, size, visitorBloc); + setPasswordFunction( + context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, @@ -562,14 +539,16 @@ class VisitorPasswordDialog extends StatelessWidget { } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - }else{ - visitorBloc.stateDialog( - context: context, - message: 'Please select Access Period to continue', - title: 'Access Period'); + } else { + visitorBloc.stateDialog( + context: context, + message: + 'Please select Access Period to continue', + title: 'Access Period'); } } else { visitorBloc.stateDialog( @@ -615,7 +594,8 @@ class VisitorPasswordDialog extends StatelessWidget { content: SizedBox( height: size.height * 0.25, child: Center( - child: CircularProgressIndicator(), // Display a loading spinner + child: + CircularProgressIndicator(), // Display a loading spinner ), ), ); @@ -639,7 +619,10 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Set Password', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( fontSize: 30, fontWeight: FontWeight.w400, color: Colors.black, @@ -689,37 +672,45 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { Navigator.pop(context); if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); - } - else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); } }, From 7e5825de45aa610348c91e45e94b11aac7284758 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:45 +0300 Subject: [PATCH 62/86] Fixed typo in occupancy sidebar. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..555841ca 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -23,7 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'Presnce Sensor'), + const AnalyticsSidebarHeader(title: 'Presence Sensor'), Expanded( child: SizedBox( // height: MediaQuery.sizeOf(context).height * 0.2, From 6e6ef79ed0c8f9c99cbdb3be66577e50305c7f4d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:44:56 +0300 Subject: [PATCH 63/86] enhanced heat map tooltip's message. --- .../analytics/modules/occupancy/widgets/heat_map_tooltip.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart index c7695064..66612a3e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart @@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget { ), const Divider(height: 2, thickness: 1), Text( - '$value Occupants', + 'Occupancy detected: $value', style: context.textTheme.bodySmall?.copyWith( fontSize: 10, fontWeight: FontWeight.w500, From ee1ebeae2e904f06221bbc9411d9094a78f3718e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:45:15 +0300 Subject: [PATCH 64/86] Changed energy management charts titles for a more clear name. --- .../widgets/energy_consumption_per_device_chart_box.dart | 2 +- .../widgets/total_energy_consumption_chart_box.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index be5faf57..06b6c529 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( - title: Text('Energy Consumption per Device'), + title: Text('Device energy consumed'), ), ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index e197c297..4d88471d 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { child: FittedBox( alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, - child: ChartTitle(title: Text('Total Energy Consumption')), + child: ChartTitle(title: Text('Space energy consumed')), ), ), const Spacer(flex: 4), From 010403f1fa83ad82432a6933c29631258f5d5ce8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 14:50:22 +0300 Subject: [PATCH 65/86] Added day of month axis name to all charts. --- .../widgets/aqi_distribution_chart.dart | 2 ++ .../energy_management_charts_helper.dart | 11 ++++----- .../occupancy/widgets/occupancy_chart.dart | 8 ++++--- .../widgets/charts_x_axis_title.dart | 23 +++++++++++++++++++ 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 lib/pages/analytics/widgets/charts_x_axis_title.dart diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..63e1d0d9 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -139,6 +140,7 @@ class AqiDistributionChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index b1af85c8..6b44e125 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -1,6 +1,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper { return FlTitlesData( show: true, bottomTitles: AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), drawBelowEverything: true, sideTitles: SideTitles( interval: 1, @@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper { ); } - static String getToolTipLabel(num month, double value) { - final monthLabel = month.toString(); - final valueLabel = value.formatNumberToKwh; - final labels = [monthLabel, valueLabel]; - return labels.where((element) => element.isNotEmpty).join(', '); - } + static String getToolTipLabel(double value) => value.formatNumberToKwh; static List getTooltipItems(List touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( - getToolTipLabel(spot.x, spot.y), + getToolTipLabel(spot.y), const TextStyle( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w600, diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 70087c46..1205a66e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget { }) { final data = chartData; - final occupancyValue = double.parse(data[group.x.toInt()].occupancy); - final percentage = '${(occupancyValue).toStringAsFixed(0)}%'; + final occupancyValue = double.parse(data[group.x].occupancy); + final percentage = '${occupancyValue.toStringAsFixed(0)}%'; return BarTooltipItem( percentage, @@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget { alignment: AlignmentDirectional.centerStart, fit: BoxFit.scaleDown, child: Text( - '${(value).toStringAsFixed(0)}%', + '${value.toStringAsFixed(0)}%', style: context.textTheme.bodySmall?.copyWith( fontSize: 12, color: ColorsManager.greyColor, @@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) => FittedBox( diff --git a/lib/pages/analytics/widgets/charts_x_axis_title.dart b/lib/pages/analytics/widgets/charts_x_axis_title.dart new file mode 100644 index 00000000..746a8cbb --- /dev/null +++ b/lib/pages/analytics/widgets/charts_x_axis_title.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ChartsXAxisTitle extends StatelessWidget { + const ChartsXAxisTitle({ + this.label = 'Day of month', + super.key, + }); + + final String label; + + @override + Widget build(BuildContext context) { + return Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ); + } +} From f901983aa57296c8339ee3a6a9eccb17c10c2eed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 24 Jun 2025 15:01:25 +0300 Subject: [PATCH 66/86] Implemented ranges for the values in the AQI chart based on the selected pollutant. --- .../helpers/range_of_aqi_charts_helper.dart | 11 ++++++-- .../widgets/range_of_aqi_chart.dart | 28 +++++++++++++++++-- .../widgets/range_of_aqi_chart_box.dart | 7 ++++- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..17b00506 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -18,7 +18,11 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context, List data) { + static FlTitlesData titlesData( + BuildContext context, + List data, { + double leftSideInterval = 50, + }) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( @@ -38,10 +42,11 @@ abstract final class RangeOfAqiChartsHelper { leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 50, + interval: leftSideInterval, maxIncluded: false, + minIncluded: true, getTitlesWidget: (value, meta) { - final text = value >= 300 ? '301+' : value.toInt().toString(); + final text = value.toInt().toString(); return Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: FittedBox( diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 5e731d90..0914eab3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RangeOfAqiChart extends StatelessWidget { final List chartData; + final AqiType selectedAqiType; const RangeOfAqiChart({ super.key, required this.chartData, + required this.selectedAqiType, }); List<(List values, Color color, Color? dotColor)> get _lines { @@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget { ]; } + (double maxY, double interval) get _maxYForAqiType { + const aqiMaxValues = { + AqiType.aqi: (401, 100), + AqiType.pm25: (351, 50), + AqiType.pm10: (501, 100), + AqiType.hcho: (301, 50), + AqiType.tvoc: (501, 50), + AqiType.co2: (1251, 250), + }; + + return aqiMaxValues[selectedAqiType]!; + } + @override Widget build(BuildContext context) { return LineChart( LineChartData( minY: 0, - maxY: 301, + maxY: _maxYForAqiType.$1, clipData: const FlClipData.vertical(), - gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: _maxYForAqiType.$2, + ), + titlesData: RangeOfAqiChartsHelper.titlesData( + context, + chartData, + leftSideInterval: _maxYForAqiType.$2, + ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), betweenBarsData: [ diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 6548c696..cb189dce 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), + Expanded( + child: RangeOfAqiChart( + chartData: state.filteredRangeOfAqi, + selectedAqiType: state.selectedAqiType, + ), + ), ], ), ); From 277a9ce4f007e472571c10fcbb909f837620f892 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 15:38:16 +0300 Subject: [PATCH 67/86] Add countdown seconds to schedule management --- .../schedule_device/bloc/schedule_bloc.dart | 14 +++- .../schedule_device/bloc/schedule_event.dart | 4 +- .../schedule_device/bloc/schedule_state.dart | 13 +++- .../count_down_inching_view.dart | 66 +++++++++++++++++-- .../schedule_widgets/schedual_view.dart | 4 +- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart index 62213205..fbf7ae64 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc { emit(currentState.copyWith( scheduleMode: event.scheduleMode, countdownRemaining: Duration.zero, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } @@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( + countdownSeconds: event.seconds, countdownHours: event.hours, countdownMinutes: event.minutes, inchingHours: 0, @@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc { inchingHours: event.hours, inchingMinutes: event.minutes, countdownRemaining: Duration.zero, + inchingSeconds: 0, // Add this )); } } @@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: countdownDuration.inMinutes % 60, countdownRemaining: countdownDuration, isCountdownActive: true, + countdownSeconds: countdownDuration.inSeconds, ), ); @@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: 0, countdownRemaining: Duration.zero, isCountdownActive: false, + countdownSeconds: 0, ), ); } @@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc { inchingMinutes: inchingDuration.inMinutes % 60, isInchingActive: true, countdownRemaining: inchingDuration, + countdownSeconds: inchingDuration.inSeconds, ), ); } @@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc { } String extractTime(String isoDateTime) { - // Example input: "2025-06-19T15:45:00.000" - return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + return isoDateTime.split('T')[1].split('.')[0]; } int? getTimeStampWithoutSeconds(DateTime? dateTime) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 7ec144fe..0b9ec581 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -146,14 +146,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent { class UpdateCountdownTimeEvent extends ScheduleEvent { final int hours; final int minutes; + final int seconds; const UpdateCountdownTimeEvent({ required this.hours, required this.minutes, + required this.seconds, }); @override - List get props => [hours, minutes]; + List get props => [hours, minutes, seconds]; } class UpdateInchingTimeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 10cd7611..63551c3a 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState { final bool isCountdownActive; final int inchingHours; final int inchingMinutes; + final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; final Duration? countdownRemaining; + final int? countdownSeconds; const ScheduleLoaded({ + this.countdownSeconds = 0, + this.inchingSeconds = 0, required this.schedules, this.selectedTime, required this.selectedDays, @@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState { bool? isInchingActive, ScheduleModes? scheduleMode, Duration? countdownRemaining, + String? deviceId, + int? countdownSeconds, + int? inchingSeconds, }) { return ScheduleLoaded( schedules: schedules ?? this.schedules, @@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState { selectedDays: selectedDays ?? this.selectedDays, functionOn: functionOn ?? this.functionOn, isEditing: isEditing ?? this.isEditing, - deviceId: deviceId, + deviceId: deviceId ?? this.deviceId, countdownHours: countdownHours ?? this.countdownHours, countdownMinutes: countdownMinutes ?? this.countdownMinutes, isCountdownActive: isCountdownActive ?? this.isCountdownActive, @@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive: isInchingActive ?? this.isInchingActive, scheduleMode: scheduleMode ?? this.scheduleMode, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + countdownSeconds: countdownSeconds ?? this.countdownSeconds, + inchingSeconds: inchingSeconds ?? this.inchingSeconds, ); } @@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive, scheduleMode, countdownRemaining, + countdownSeconds, + inchingSeconds, ]; } diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart index d45073ec..418bab6c 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownInchingView extends StatefulWidget { - const CountdownInchingView({super.key}); + final String deviceId; + const CountdownInchingView({super.key, required this.deviceId}); @override State createState() => _CountdownInchingViewState(); @@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget { class _CountdownInchingViewState extends State { late FixedExtentScrollController _hoursController; late FixedExtentScrollController _minutesController; + late FixedExtentScrollController _secondsController; int _lastHours = -1; int _lastMinutes = -1; + int _lastSeconds = -1; @override void initState() { super.initState(); _hoursController = FixedExtentScrollController(); _minutesController = FixedExtentScrollController(); + _secondsController = FixedExtentScrollController(); } @override void dispose() { _hoursController.dispose(); _minutesController.dispose(); + _secondsController.dispose(); super.dispose(); } - void _updateControllers(int displayHours, int displayMinutes) { + void _updateControllers( + int displayHours, int displayMinutes, int displaySeconds) { if (_lastHours != displayHours) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_hoursController.hasClients) { @@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State { }); _lastMinutes = displayMinutes; } + // Update seconds controller + if (_lastSeconds != displaySeconds) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_secondsController.hasClients) { + _secondsController.jumpToItem(displaySeconds); + } + }); + _lastSeconds = displaySeconds; + } } @override @@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State { return BlocBuilder( builder: (context, state) { if (state is! ScheduleLoaded) return const SizedBox.shrink(); - final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isActive = isCountDown ? state.isCountdownActive : state.isInchingActive; @@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State { final displayMinutes = isActive && state.countdownRemaining != null ? state.countdownRemaining!.inMinutes.remainder(60) : (isCountDown ? state.countdownMinutes : state.inchingMinutes); + final displaySeconds = isActive && state.countdownRemaining != null + ? state.countdownRemaining!.inSeconds.remainder(60) + : (isCountDown ? state.countdownSeconds : state.inchingSeconds); + + _updateControllers(displayHours, displayMinutes, displaySeconds!); + + if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) { + context.read().add( + StopScheduleEvent( + mode: ScheduleModes.countdown, + deviceId: widget.deviceId, + ), + ); + } - _updateControllers(displayHours, displayMinutes); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: value, minutes: displayMinutes)); + hours: value, + minutes: displayMinutes, + seconds: displaySeconds, + )); } }, isActive: isActive, @@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: displayHours, minutes: value)); + hours: displayHours, + minutes: value, + seconds: displaySeconds, + )); } }, isActive: isActive, ), + const SizedBox(width: 10), + if (isActive) + _buildPickerColumn( + context, + 's', + displaySeconds, + 60, + _secondsController, + (value) { + if (!isActive) { + context + .read() + .add(UpdateCountdownTimeEvent( + hours: displayHours, + minutes: displayMinutes, + seconds: value, + )); + } + }, + isActive: isActive, + ), ], ), ], diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart index 2fa34559..47534d37 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -74,7 +74,9 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - const CountdownInchingView(), + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( From c6e98fa24550d5452494e7c23a4d7a06d7201ccd Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:06:53 +0300 Subject: [PATCH 68/86] Refactor visitor password dialog navigation to return specific values on pop --- .../visitor_password/view/visitor_password_dialog.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4b5cb0e2..978b425a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -102,7 +102,7 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(true); + Navigator.of(context).pop(v); }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -476,7 +476,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(true); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -651,7 +651,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( From c649044a1fc3c98422cf9ba1a8ef15bdf4e3c19e Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 24 Jun 2025 16:40:42 +0300 Subject: [PATCH 69/86] Enhance navigation buttons in SmartPowerDeviceControl for better user experience --- .../view/smart_power_device_control.dart | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 67313802..11d1cc8f 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; //Smart Power Clamp -class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout { +class SmartPowerDeviceControl extends StatelessWidget + with HelperResponsiveLayout { final String deviceId; const SmartPowerDeviceControl({super.key, required this.deviceId}); @@ -145,13 +146,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou children: [ IconButton( icon: const Icon(Icons.arrow_left), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(-1)); - pageController.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage <= 0 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(-1)); + pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), Text( currentPage == 0 @@ -165,13 +169,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou ), IconButton( icon: const Icon(Icons.arrow_right), - onPressed: () { - blocProvider.add(SmartPowerArrowPressedEvent(1)); - pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + onPressed: blocProvider.currentPage >= 3 + ? null + : () { + blocProvider + .add(SmartPowerArrowPressedEvent(1)); + pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, ), ], ), @@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou blocProvider.add(SelectDateEvent(context: context)); blocProvider.add(FilterRecordsByDateEvent( selectedDate: blocProvider.dateTime!, - viewType: - blocProvider.views[blocProvider.currentIndex])); + viewType: blocProvider + .views[blocProvider.currentIndex])); }, widget: blocProvider.dateSwitcher(), chartData: blocProvider.energyDataList.isNotEmpty From 52b843d51429935fbd2cd4b5290b34804fe06575 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 09:53:09 +0300 Subject: [PATCH 70/86] SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when-selecting-space-from-sidebar. --- lib/pages/space_tree/bloc/space_tree_bloc.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index e8c2e015..7d1a4d96 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc { selectedSpaces: updatedSelectedSpaces, soldCheck: updatedSoldChecks, selectedCommunityAndSpaces: communityAndSpaces)); - emit(state.copyWith(selectedSpaces: updatedSelectedSpaces)); } catch (e) { emit(const SpaceTreeErrorState('Something went wrong')); } @@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc { List _getThePathToChild(String communityId, String selectedSpaceId) { List ids = []; - for (var community in state.communityList) { + final communityDataSource = + state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + for (final community in communityDataSource) { if (community.uuid == communityId) { - for (var space in community.spaces) { - List list = []; + for (final space in community.spaces) { + final list = []; list.add(space.uuid!); ids = _getAllParentsIds(space, selectedSpaceId, List.from(list)); if (ids.isNotEmpty) { From 562c67a958dbb3526ba677e4fc8f129a0b01af69 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 12:24:09 +0300 Subject: [PATCH 71/86] Add deviceName field to FailedOperation and SuccessOperation models --- .../visitor_password/model/failed_operation.dart | 13 ++++++++----- .../view/visitor_password_dialog.dart | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/visitor_password/model/failed_operation.dart b/lib/pages/visitor_password/model/failed_operation.dart index 223f9ac5..120f6d89 100644 --- a/lib/pages/visitor_password/model/failed_operation.dart +++ b/lib/pages/visitor_password/model/failed_operation.dart @@ -2,11 +2,13 @@ class FailedOperation { final bool success; final dynamic deviceUuid; final dynamic error; + final String deviceName; FailedOperation({ required this.success, required this.deviceUuid, required this.error, + required this.deviceName, }); factory FailedOperation.fromJson(Map json) { @@ -14,6 +16,7 @@ class FailedOperation { success: json['success'], deviceUuid: json['deviceUuid'], error: json['error'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -22,21 +25,22 @@ class FailedOperation { 'success': success, 'deviceUuid': deviceUuid, 'error': error, + 'deviceName': deviceName, }; } } - - class SuccessOperation { final bool success; // final Result result; final String deviceUuid; + final String deviceName; SuccessOperation({ required this.success, // required this.result, required this.deviceUuid, + required this.deviceName, }); factory SuccessOperation.fromJson(Map json) { @@ -44,6 +48,7 @@ class SuccessOperation { success: json['success'], // result: Result.fromJson(json['result']), deviceUuid: json['deviceUuid'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -52,6 +57,7 @@ class SuccessOperation { 'success': success, // 'result': result.toJson(), 'deviceUuid': deviceUuid, + 'deviceName': deviceName, }; } } @@ -92,8 +98,6 @@ class SuccessOperation { // } // } - - class PasswordStatus { final List successOperations; final List failedOperations; @@ -121,4 +125,3 @@ class PasswordStatus { }; } } - diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 978b425a..d1fb172a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .failedOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), @@ -92,7 +92,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: Text(visitorBloc .passwordStatus! .successOperations[index] - .deviceUuid)), + .deviceName)), ); }, ), From 7397486e7ac3c948cc5e3de1e40b9267c3e25509 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:11:49 +0300 Subject: [PATCH 72/86] SP-368-Clarification-on-Default-Value-for-Start-Date-in-Door-Lock-Online-Tile-Limited-Password-repeat-section --- .../bloc/visitor_password_bloc.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 438b1abf..6dc20cfd 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -68,7 +68,7 @@ class VisitorPasswordBloc DateTime? startTime = DateTime.now(); DateTime? endTime; - String startTimeAccess = 'Start Time'; + String startTimeAccess = DateTime.now().toString().split('.').first; String endTimeAccess = 'End Time'; PasswordStatus? passwordStatus; selectAccessType( @@ -136,6 +136,27 @@ class VisitorPasswordBloc ); return; } + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text('Effective Time cannot be earlier than current time.'), + actionsAlignment: MainAxisAlignment.center, + content: + FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); + }, + child: const Text('OK'), + ), + + ), + ); + } + return; + } effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { From 5f5958369647da17bb4e346a56cae85bdcdd18f8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:12 +0300 Subject: [PATCH 73/86] Supported `NCPS` device type in occupancy devices dropdown. --- .../modules/occupancy/helpers/fetch_occupancy_data_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart index 0b01fda2..3bd96bce 100644 --- a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart +++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart @@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper { param: GetAnalyticsDevicesParam( communityUuid: communityUuid, spaceUuid: spaceUuid, - deviceTypes: ['WPS', 'CPS'], + deviceTypes: ['WPS', 'CPS', 'NCPS'], requestType: AnalyticsDeviceRequestType.occupancy, ), onSuccess: (device) { From 3b4952db0ae650b0af5aac028bc0ffb26ec3d238 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:30 +0300 Subject: [PATCH 74/86] fixed thrown exceptions in`AnalyticsDevice`. --- lib/pages/analytics/models/analytics_device.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 3340a41d..869de23f 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -25,8 +25,8 @@ class AnalyticsDevice { factory AnalyticsDevice.fromJson(Map json) { return AnalyticsDevice( - uuid: json['uuid'] as String, - name: json['name'] as String, + uuid: json['uuid'] as String? ?? '', + name: json['name'] as String? ?? '', createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null, @@ -39,8 +39,8 @@ class AnalyticsDevice { ? ProductDevice.fromJson(json['productDevice'] as Map) : null, spaceUuid: json['spaceUuid'] as String?, - latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null, - longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null, + latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null, + longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null, ); } } From 6d667af7dcf099b48709cbbc7618cc067358e7ef Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:25:46 +0300 Subject: [PATCH 75/86] increased size of `OccupancyEndSideBar` in medium sized screens. --- .../modules/occupancy/views/analytics_occupancy_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart index 3a025254..56f8ce08 100644 --- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget { child: Column( spacing: 32, children: [ - SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()), + SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()), SizedBox(height: height * 0.5, child: const OccupancyChartBox()), SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()), ], From 22070ca04a385817093b90880f8ddea48fba1539 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 13:26:07 +0300 Subject: [PATCH 76/86] removed unused comment. --- .../modules/occupancy/widgets/occupancy_end_side_bar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index 3dd01bee..75455a9b 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -26,7 +26,6 @@ class OccupancyEndSideBar extends StatelessWidget { const AnalyticsSidebarHeader(title: 'Presnce Sensor'), Expanded( child: SizedBox( - // height: MediaQuery.sizeOf(context).height * 0.2, child: PowerClampEnergyStatusWidget( status: [ PowerClampEnergyStatus( From 5e0df09cb6be5c8c8807a40a5cede8518cc2135f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:25:53 +0300 Subject: [PATCH 77/86] Changed tvoc unit to match the device. --- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 5d482d9c..457bf610 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -7,7 +7,7 @@ enum AqiType { pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), hcho('HCHO', 'mg/m³', 'cho2'), - tvoc('TVOC', 'µg/m³', 'voc'), + tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); const AqiType(this.value, this.unit, this.code); From e1bb67d7bd8074ffe2d0adb6e21d72cd0c7dfe4e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:26:05 +0300 Subject: [PATCH 78/86] reads correct value for TVOC. --- .../analytics/modules/air_quality/widgets/aqi_device_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index ebe88614..23ae874e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget { ); final tvocValue = _getValueForStatus( status, - 'tvoc_value', + 'voc_value', formatter: (value) => (value / 100).toStringAsFixed(2), ); From 520b73717ad886d3179179cedd3aa91d83c355bc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:07 +0300 Subject: [PATCH 79/86] Doesnt load devices when date changes. --- .../air_quality/helpers/fetch_air_quality_data_helper.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..d7fbc279 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper { }) { final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; + if (shouldFetchAnalyticsDevices) { loadAnalyticsDevices( context, communityUuid: communityUuid, spaceUuid: spaceUuid, ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, From 30e940fdfc2fda32205c76ea0f4735ab28ea829f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 25 Jun 2025 14:34:23 +0300 Subject: [PATCH 80/86] Reads the correct date to load aqi data. --- .../helpers/fetch_air_quality_data_helper.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index d7fbc279..5c63e397 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; @@ -22,14 +21,13 @@ abstract final class FetchAirQualityDataHelper { required String spaceUuid, bool shouldFetchAnalyticsDevices = true, }) { - final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; if (shouldFetchAnalyticsDevices) { - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); } loadRangeOfAqi( context, From f38ac58442deb9172cdc702ee2dde1983749a0cb Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 14:45:10 +0300 Subject: [PATCH 81/86] Add bloc closure handling and improve device status updates in AcBloc --- .../device_managment/ac/bloc/ac_bloc.dart | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..eaababe1 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,6 +16,7 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; + bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -45,7 +46,8 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { final totalMinutes = deviceStatus.countdown1 * 6; @@ -68,12 +70,13 @@ class AcBloc extends Bloc { } } - void _listenToChanges(deviceId) { + StreamSubscription? _deviceStatusSubscription; + + void _listenToChanges(String deviceId) { try { final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final stream = ref.onValue; - - stream.listen((DatabaseEvent event) async { + _deviceStatusSubscription = + ref.onValue.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; Map usersMap = @@ -82,11 +85,15 @@ class AcBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList.add(Status(code: element['code'], value: element['value'])); + statusList + .add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); + print('Device status updated: ${deviceStatus.acSwitch}'); + + if (!_isBlocClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -106,15 +113,14 @@ class AcBloc extends Bloc { Emitter emit, ) async { emit(AcsLoadingState()); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); try { final success = await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); if (!success) { emit(const AcsFailedState(error: 'Failed to control device')); } @@ -129,8 +135,10 @@ class AcBloc extends Bloc { ) async { emit(AcsLoadingState()); try { - final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = + AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -293,13 +301,17 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } else { _countdownTimer?.cancel(); timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - add(TimerCompletedEvent()); + if (!_isBlocClosed) { + add(TimerCompletedEvent()); + } } }); } @@ -326,7 +338,9 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - add(UpdateTimerEvent()); + if (!_isBlocClosed) { + add(UpdateTimerEvent()); + } } } @@ -370,6 +384,9 @@ class AcBloc extends Bloc { @override Future close() { add(OnClose()); + _countdownTimer?.cancel(); + _deviceStatusSubscription?.cancel(); + _isBlocClosed = true; return super.close(); } } From 3c9494963d60a12767f559d84629fc6eee356079 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 25 Jun 2025 15:58:58 +0300 Subject: [PATCH 82/86] Add generated configuration files for Flutter integration across platforms --- lib/pages/device_managment/ac/bloc/ac_bloc.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index eaababe1..38d11a46 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -16,7 +16,6 @@ class AcBloc extends Bloc { final ControlDeviceService controlDeviceService; final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - bool _isBlocClosed = false; AcBloc({ required this.deviceId, @@ -93,7 +92,7 @@ class AcBloc extends Bloc { AcStatusModel.fromJson(usersMap['productUuid'], statusList); print('Device status updated: ${deviceStatus.acSwitch}'); - if (!_isBlocClosed) { + if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } }); @@ -301,7 +300,7 @@ class AcBloc extends Bloc { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } else { @@ -309,7 +308,7 @@ class AcBloc extends Bloc { timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - if (!_isBlocClosed) { + if (!isClosed) { add(TimerCompletedEvent()); } } @@ -338,7 +337,7 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - if (!_isBlocClosed) { + if (!isClosed) { add(UpdateTimerEvent()); } } @@ -386,7 +385,6 @@ class AcBloc extends Bloc { add(OnClose()); _countdownTimer?.cancel(); _deviceStatusSubscription?.cancel(); - _isBlocClosed = true; return super.close(); } } From 11e285340376b90f0d169b8eb31e66219a8ddb64 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 11:56:43 +0300 Subject: [PATCH 83/86] Enhance AnalyticsDeviceDropdown to show loading indicator during loading state. --- .../widgets/analytics_device_dropdown.dart | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart index f7b33309..055e9675 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget { ), ), child: Visibility( - visible: state.devices.isNotEmpty, - replacement: _buildNoDevicesFound(context), - child: _buildDevicesDropdown(context, state), + visible: state.status != AnalyticsDevicesStatus.loading, + replacement: _buildLoadingIndicator(), + child: Visibility( + visible: state.devices.isNotEmpty, + replacement: _buildNoDevicesFound(context), + child: _buildDevicesDropdown(context, state), + ), ), ); }, ); } + Widget _buildLoadingIndicator() { + return const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ); + } + static const _defaultPadding = EdgeInsetsDirectional.symmetric( horizontal: 20, vertical: 2, From 7fda564ee43256336723fec9b0e1fc2bf059a08c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 14:07:24 +0300 Subject: [PATCH 84/86] hotfixes. --- lib/pages/analytics/models/occupancy_heat_map_model.dart | 4 +++- .../modules/air_quality/widgets/aqi_type_dropdown.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 73e7d5d7..cd332745 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -19,7 +19,9 @@ class OccupancyHeatMapModel extends Equatable { eventDate: DateTime.parse( json['event_date'] as String? ?? '${DateTime.now()}', ), - countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + countTotalPresenceDetected: num.parse( + json['count_total_presence_detected']?.toString() ?? '900', + ).toInt(), ); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 457bf610..6640c717 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -6,7 +6,7 @@ enum AqiType { aqi('AQI', '', 'aqi'), pm25('PM2.5', 'µg/m³', 'pm25'), pm10('PM10', 'µg/m³', 'pm10'), - hcho('HCHO', 'mg/m³', 'cho2'), + hcho('HCHO', 'mg/m³', 'ch2o'), tvoc('TVOC', 'mg/m³', 'voc'), co2('CO2', 'ppm', 'co2'); From 475462301f8ff72b2b666d8ddadbcf25700cbab9 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 15:29:11 +0300 Subject: [PATCH 85/86] manually parse event date for heatmap date object. --- .../analytics/models/occupancy_heat_map_model.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index cd332745..60d1a453 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,13 +14,16 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { + final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; + final year = eventDate.split('-')[0]; + final month = eventDate.split('-')[1]; + final day = eventDate.split('-')[2]; + return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime.parse( - json['event_date'] as String? ?? '${DateTime.now()}', - ), + eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), countTotalPresenceDetected: num.parse( - json['count_total_presence_detected']?.toString() ?? '900', + json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), ); } From ca41aa622479d28c10478620546e8447bfcbac9f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Fri, 27 Jun 2025 16:37:09 +0300 Subject: [PATCH 86/86] all dates in heatmap are utc. --- .../analytics/models/occupancy_heat_map_model.dart | 14 +++++++++----- .../occupancy/widgets/interactive_heat_map.dart | 2 +- .../occupancy/widgets/occupancy_heat_map.dart | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 60d1a453..e4b7d730 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,14 +14,18 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map json) { - final eventDate = json['event_date'] as String? ?? '${DateTime.now()}'; - final year = eventDate.split('-')[0]; - final month = eventDate.split('-')[1]; - final day = eventDate.split('-')[2]; + final eventDate = json['event_date'] as String?; + final year = eventDate?.split('-')[0]; + final month = eventDate?.split('-')[1]; + final day = eventDate?.split('-')[2]; return OccupancyHeatMapModel( uuid: json['uuid'] as String? ?? '', - eventDate: DateTime(int.parse(year), int.parse(month), int.parse(day)), + eventDate: DateTime( + int.parse(year ?? '2025'), + int.parse(month ?? '1'), + int.parse(day ?? '1'), + ).toUtc(), countTotalPresenceDetected: num.parse( json['count_total_presence_detected']?.toString() ?? '0', ).toInt(), diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart index a652ae73..514ebb65 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart @@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State { color: Colors.transparent, child: Transform.translate( offset: Offset(-(widget.cellSize * 2.5), -50), - child: HeatMapTooltip(date: item.date, value: item.value), + child: HeatMapTooltip(date: item.date.toUtc(), value: item.value), ), ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..0809a990 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.add(Duration(days: index)); + final date = startDate.toUtc().add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); });